aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/ClientRecommender.scss12
-rw-r--r--src/client/ClientRecommender.tsx425
-rw-r--r--src/client/DocServer.ts39
-rw-r--r--src/client/apis/IBM_Recommender.ts40
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts201
-rw-r--r--src/client/documents/DocumentTypes.ts4
-rw-r--r--src/client/documents/Documents.ts75
-rw-r--r--src/client/util/DocumentManager.ts30
-rw-r--r--src/client/util/DragManager.ts22
-rw-r--r--src/client/util/DropConverter.ts14
-rw-r--r--src/client/util/InteractionUtils.tsx30
-rw-r--r--src/client/util/RichTextMenu.tsx2
-rw-r--r--src/client/util/RichTextRules.ts20
-rw-r--r--src/client/util/RichTextSchema.tsx147
-rw-r--r--src/client/util/SearchUtil.ts25
-rw-r--r--src/client/util/SelectionManager.ts1
-rw-r--r--src/client/util/TooltipTextMenu.scss372
-rw-r--r--src/client/views/DocumentButtonBar.tsx4
-rw-r--r--src/client/views/DocumentDecorations.scss13
-rw-r--r--src/client/views/DocumentDecorations.tsx16
-rw-r--r--src/client/views/GestureOverlay.scss44
-rw-r--r--src/client/views/GestureOverlay.tsx398
-rw-r--r--src/client/views/GlobalKeyHandler.ts2
-rw-r--r--src/client/views/InkingStroke.tsx4
-rw-r--r--src/client/views/KeyphraseQueryView.scss8
-rw-r--r--src/client/views/KeyphraseQueryView.tsx35
-rw-r--r--src/client/views/Main.tsx1
-rw-r--r--src/client/views/MainView.tsx18
-rw-r--r--src/client/views/OCRUtils.ts7
-rw-r--r--src/client/views/Palette.scss13
-rw-r--r--src/client/views/Palette.tsx1
-rw-r--r--src/client/views/RecommendationsBox.scss68
-rw-r--r--src/client/views/RecommendationsBox.tsx199
-rw-r--r--src/client/views/TouchScrollableMenu.tsx59
-rw-r--r--src/client/views/Touchable.tsx65
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx105
-rw-r--r--src/client/views/collections/CollectionLinearView.tsx5
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx37
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx13
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx30
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx15
-rw-r--r--src/client/views/collections/CollectionSubView.tsx9
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx14
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx2
-rw-r--r--src/client/views/collections/CollectionView.tsx2
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss18
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx16
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx4
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss7
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx170
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx8
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx102
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx9
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx2
-rw-r--r--src/client/views/nodes/AudioBox.scss7
-rw-r--r--src/client/views/nodes/AudioBox.tsx111
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx23
-rw-r--r--src/client/views/nodes/DocumentView.tsx358
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx93
-rw-r--r--src/client/views/nodes/ImageBox.tsx7
-rw-r--r--src/client/views/nodes/PDFBox.tsx22
-rw-r--r--src/client/views/nodes/PresBox.tsx6
-rw-r--r--src/client/views/nodes/RadialMenu.tsx83
-rw-r--r--src/client/views/nodes/VideoBox.tsx3
-rw-r--r--src/client/views/nodes/WebBox.scss15
-rw-r--r--src/client/views/nodes/WebBox.tsx138
-rw-r--r--src/client/views/pdf/PDFViewer.scss8
-rw-r--r--src/client/views/pdf/PDFViewer.tsx70
68 files changed, 3333 insertions, 593 deletions
diff --git a/src/client/ClientRecommender.scss b/src/client/ClientRecommender.scss
new file mode 100644
index 000000000..3f9102f15
--- /dev/null
+++ b/src/client/ClientRecommender.scss
@@ -0,0 +1,12 @@
+// @import "/views/globalCssVariables.scss";
+
+.space{
+ border: 1px dashed blue;
+ border-spacing: 25px;
+ border-collapse: separate;
+ align-content: center;
+}
+
+.wrapper{
+ text-align: -webkit-center;
+} \ No newline at end of file
diff --git a/src/client/ClientRecommender.tsx b/src/client/ClientRecommender.tsx
new file mode 100644
index 000000000..cb1674943
--- /dev/null
+++ b/src/client/ClientRecommender.tsx
@@ -0,0 +1,425 @@
+import { Doc, FieldResult } from "../new_fields/Doc";
+import { StrCast, Cast } from "../new_fields/Types";
+import { List } from "../new_fields/List";
+import { CognitiveServices, Confidence, Tag, Service } from "./cognitive_services/CognitiveServices";
+import React = require("react");
+import { observer } from "mobx-react";
+import { observable, action, computed, reaction } from "mobx";
+var assert = require('assert');
+var sw = require('stopword');
+var FeedParser = require('feedparser');
+var https = require('https');
+import "./ClientRecommender.scss";
+import { JSXElement } from "babel-types";
+import { RichTextField } from "../new_fields/RichTextField";
+import { ToPlainText } from "../new_fields/FieldSymbols";
+import { listSpec } from "../new_fields/Schema";
+import { ComputedField } from "../new_fields/ScriptField";
+import { ImageField } from "../new_fields/URLField";
+import { KeyphraseQueryView } from "./views/KeyphraseQueryView";
+import { Networking } from "./Network";
+
+export interface RecommenderProps {
+ title: string;
+}
+
+/**
+ * actualDoc: datadoc
+ * vectorDoc: mean vector of text
+ * score: similarity score to main doc
+ */
+
+export interface RecommenderDocument {
+ actualDoc: Doc;
+ vectorDoc: number[];
+ score: number;
+}
+
+const fieldkey = "data";
+
+@observer
+export class ClientRecommender extends React.Component<RecommenderProps> {
+
+
+
+ static Instance: ClientRecommender;
+ private mainDoc?: RecommenderDocument;
+ private docVectors: Set<RecommenderDocument> = new Set();
+ public _queries: string[] = [];
+
+ @observable private corr_matrix = [[0, 0], [0, 0]]; // for testing
+
+ constructor(props: RecommenderProps) {
+ //console.log("creating client recommender...");
+ super(props);
+ if (!ClientRecommender.Instance) ClientRecommender.Instance = this;
+ ClientRecommender.Instance.docVectors = new Set();
+ //ClientRecommender.Instance.corr_matrix = [[0, 0], [0, 0]];
+ }
+
+ @action
+ public reset_docs() {
+ ClientRecommender.Instance.docVectors = new Set();
+ ClientRecommender.Instance.mainDoc = undefined;
+ ClientRecommender.Instance.corr_matrix = [[0, 0], [0, 0]];
+ }
+
+ /***
+ * Computes the cosine similarity between two vectors in Euclidean space.
+ */
+
+ private distance(vector1: number[], vector2: number[], metric: string = "cosine") {
+ assert(vector1.length === vector2.length, "Vectors are not the same length");
+ let similarity: number;
+ switch (metric) {
+ case "cosine":
+ var dotproduct = 0;
+ var mA = 0;
+ var mB = 0;
+ for (let i = 0; i < vector1.length; i++) { // here you missed the i++
+ dotproduct += (vector1[i] * vector2[i]);
+ mA += (vector1[i] * vector1[i]);
+ mB += (vector2[i] * vector2[i]);
+ }
+ mA = Math.sqrt(mA);
+ mB = Math.sqrt(mB);
+ similarity = (dotproduct) / ((mA) * (mB)); // here you needed extra brackets
+ return similarity;
+ case "euclidian":
+ var sum = 0;
+ for (let i = 0; i < vector1.length; i++) {
+ sum += Math.pow(vector1[i] - vector2[i], 2);
+ }
+ similarity = Math.sqrt(sum);
+ return similarity;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Returns list of {doc, similarity (to main doc)} in increasing score
+ */
+
+ public computeSimilarities(distance_metric: string) {
+ const parameters: any = {};
+ Networking.PostToServer("/IBMAnalysis", parameters).then(response => {
+ console.log("ANALYSIS RESULTS! ", response);
+ });
+ ClientRecommender.Instance.docVectors.forEach((doc: RecommenderDocument) => {
+ if (ClientRecommender.Instance.mainDoc) {
+ const distance = ClientRecommender.Instance.distance(ClientRecommender.Instance.mainDoc.vectorDoc, doc.vectorDoc, distance_metric);
+ doc.score = distance;
+ }
+ }
+ );
+ let doclist = Array.from(ClientRecommender.Instance.docVectors);
+ if (distance_metric === "euclidian") {
+ doclist.sort((a: RecommenderDocument, b: RecommenderDocument) => a.score - b.score);
+ }
+ else {
+ doclist.sort((a: RecommenderDocument, b: RecommenderDocument) => b.score - a.score);
+ }
+ return doclist;
+ }
+
+ /***
+ * Computes the mean of a set of vectors
+ */
+
+ public mean(paragraph: Set<number[]>) {
+ const n = 512;
+ const num_words = paragraph.size;
+ let meanVector = new Array<number>(n).fill(0); // mean vector
+ if (num_words > 0) { // check to see if paragraph actually was vectorized
+ paragraph.forEach((wordvec: number[]) => {
+ for (let i = 0; i < n; i++) {
+ meanVector[i] += wordvec[i];
+ }
+ });
+ meanVector = meanVector.map(x => x / num_words);
+ }
+ return meanVector;
+ }
+
+ /***
+ * Processes sentence vector as Recommender Document, adds to Doc Set.
+ */
+
+ public processVector(vector: number[], dataDoc: Doc, isMainDoc: boolean) {
+ if (vector.length > 0) {
+ const internalDoc: RecommenderDocument = { actualDoc: dataDoc, vectorDoc: vector, score: 0 };
+ ClientRecommender.Instance.addToDocSet(internalDoc, isMainDoc);
+ }
+ }
+
+ /***
+ * Adds to Doc set. Updates mainDoc (one clicked) if necessary.
+ */
+
+ private addToDocSet(internalDoc: RecommenderDocument, isMainDoc: boolean) {
+ if (ClientRecommender.Instance.docVectors) {
+ if (isMainDoc) ClientRecommender.Instance.mainDoc = internalDoc;
+ ClientRecommender.Instance.docVectors.add(internalDoc);
+ }
+ }
+
+ /***
+ * Generates tags for an image using Cognitive Services
+ */
+
+ generateMetadata = async (dataDoc: Doc, extDoc: Doc, threshold: Confidence = Confidence.Excellent) => {
+ let converter = (results: any) => {
+ let tagDoc = new Doc;
+ let tagsList = new List();
+ results.tags.map((tag: Tag) => {
+ tagsList.push(tag.name);
+ let sanitized = tag.name.replace(" ", "_");
+ tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
+ });
+ extDoc.generatedTags = tagsList;
+ tagDoc.title = "Generated Tags Doc";
+ tagDoc.confidence = threshold;
+ return tagDoc;
+ };
+ const url = this.url(dataDoc);
+ if (url) {
+ return CognitiveServices.Image.Appliers.ProcessImage(extDoc, ["generatedTagsDoc"], url, Service.ComputerVision, converter);
+ }
+ }
+
+ /***
+ * Gets URL of image
+ */
+
+ private url(dataDoc: Doc) {
+ let data = Cast(Doc.GetProto(dataDoc)[fieldkey], ImageField);
+ return data ? data.url.href : undefined;
+ }
+
+ /***
+ * Uses Cognitive Services to extract keywords from a document
+ */
+
+ public async extractText(dataDoc: Doc, extDoc: Doc, internal: boolean = true, api: string = "bing", isMainDoc: boolean = false, image: boolean = false) {
+ // STEP 1. Consolidate data of document. Depends on type of document.
+ let data: string = "";
+ let taglist: FieldResult<List<string>> = undefined;
+ if (image) {
+ if (!extDoc.generatedTags) await this.generateMetadata(dataDoc, extDoc); // TODO: Automatically generate tags. Need to ask Sam about this.
+ if (extDoc.generatedTags) {
+ taglist = Cast(extDoc.generatedTags, listSpec("string"));
+ taglist!.forEach(tag => {
+ data += tag + ", ";
+ });
+ }
+ }
+ else {
+ let fielddata = Cast(dataDoc.data, RichTextField);
+ fielddata ? data = fielddata[ToPlainText]() : data = "";
+ }
+
+ // STEP 2. Upon receiving response from Text Cognitive Services, do additional processing on keywords.
+ // Currently we are still using Cognitive Services for internal recommendations, but in the future this might not be necessary.
+
+ let converter = async (results: any, data: string, isImage: boolean = false) => {
+ let keyterms = new List<string>(); // raw keywords
+ let kp_string: string = ""; // keywords*frequency concatenated into a string. input into TF
+ let highKP: string[] = [""]; // most frequent keyphrase
+ let high = 0;
+
+ if (isImage) { // no keyphrase processing necessary
+ kp_string = data;
+ if (taglist) {
+ keyterms = taglist;
+ highKP = [taglist[0]];
+ }
+ }
+ else { // text processing
+ results.documents.forEach((doc: any) => {
+ let keyPhrases = doc.keyPhrases; // returned by Cognitive Services
+ keyPhrases.map((kp: string) => {
+ keyterms.push(kp);
+ const frequency = this.countFrequencies(kp, data); // frequency of keyphrase in paragraph
+ kp_string += kp + ", "; // ensures that if frequency is 0 for some reason kp is still added
+ for (let i = 0; i < frequency - 1; i++) {
+ kp_string += kp + ", "; // weights repeated keywords higher
+ }
+ // replaces highKP with new one
+ if (frequency > high) {
+ high = frequency;
+ highKP = [kp];
+ }
+ // appends to current highKP phrase
+ else if (frequency === high) {
+ highKP.push(kp);
+ }
+ });
+ });
+ }
+ if (kp_string.length > 2) kp_string = kp_string.substring(0, kp_string.length - 2); // strips extra comma and space if there are a lot of keywords
+ console.log("kp_string: ", kp_string);
+
+ let ext_recs = "";
+ // Pushing keyword extraction to IBM for external recommendations. Should shift to internal eventually.
+ if (!internal) {
+ const parameters: any = {
+ 'language': 'en',
+ 'text': data,
+ 'features': {
+ 'keywords': {
+ 'sentiment': true,
+ 'emotion': true,
+ 'limit': 3
+ }
+ }
+ };
+ await Networking.PostToServer("/IBMAnalysis", parameters).then(response => {
+ const sorted_keywords = response.result.keywords;
+ if (sorted_keywords.length > 0) {
+ console.log("IBM keyphrase", sorted_keywords[0]);
+ highKP = [];
+ for (let i = 0; i < 5; i++) {
+ if (sorted_keywords[i]) {
+ highKP.push(sorted_keywords[i].text);
+ }
+ }
+ keyterms = new List<string>(highKP);
+ }
+ });
+ //let kpqv = new KeyphraseQueryView({ keyphrases: ["hello"] });
+ ext_recs = await this.sendRequest([highKP[0]], api);
+ }
+
+ // keyterms: list for extDoc, kp_string: input to TF, ext_recs: {titles, urls} of retrieved results from highKP query
+ return { keyterms: keyterms, external_recommendations: ext_recs, kp_string: [kp_string] };
+ };
+
+ // STEP 3: Start recommendation pipeline. Branches off into internal and external in Cognitive Services
+ if (data !== "") {
+ return CognitiveServices.Text.Appliers.analyzer(dataDoc, extDoc, ["key words"], data, converter, isMainDoc, internal);
+ }
+ return;
+ }
+
+ /**
+ *
+ * Counts frequencies of keyphrase in paragraph.
+ */
+
+ private countFrequencies(keyphrase: string, paragraph: string) {
+ let data = paragraph.split(/ |\n/); // splits by new lines and spaces
+ let kp_array = keyphrase.split(" ");
+ let num_keywords = kp_array.length;
+ let par_length = data.length;
+ let frequency = 0;
+ // slides keyphrase windows across paragraph and checks if it matches with corresponding paragraph slice
+ for (let i = 0; i <= par_length - num_keywords; i++) {
+ const window = data.slice(i, i + num_keywords);
+ if (JSON.stringify(window).toLowerCase() === JSON.stringify(kp_array).toLowerCase() || kp_array.every(val => window.includes(val))) {
+ frequency++;
+ }
+ }
+ return frequency;
+ }
+
+ /**
+ *
+ * API for sending arXiv request.
+ */
+
+ private async sendRequest(keywords: string[], api: string) {
+ let query = "";
+ keywords.forEach((kp: string) => query += " " + kp);
+ if (api === "arxiv") {
+ return new Promise<any>(resolve => {
+ this.arxivrequest(query).then(resolve);
+ });
+ }
+ else if (api === "bing") {
+ return new Promise<any>(resolve => {
+ this.bingWebSearch(query).then(resolve);
+ });
+ }
+ else {
+ console.log("no api specified :(");
+ }
+
+ }
+
+ /**
+ * Request to Bing API. Most of code is in Cognitive Services.
+ */
+
+ bingWebSearch = async (query: string) => {
+ const converter = async (results: any) => {
+ let title_vals: string[] = [];
+ let url_vals: string[] = [];
+ results.webPages.value.forEach((doc: any) => {
+ title_vals.push(doc.name);
+ url_vals.push(doc.url);
+ });
+ return { title_vals, url_vals };
+ };
+ return CognitiveServices.BingSearch.Appliers.analyzer(query, converter);
+ }
+
+ /**
+ * Actual request to the arXiv server for ML articles.
+ */
+
+ arxivrequest = async (query: string) => {
+ let xhttp = new XMLHttpRequest();
+ let serveraddress = "http://export.arxiv.org/api";
+ const maxresults = 5;
+ let endpoint = serveraddress + "/query?search_query=all:" + query + "&start=0&max_results=" + maxresults.toString();
+ let promisified = (resolve: any, reject: any) => {
+ xhttp.onreadystatechange = function () {
+ if (this.readyState === 4) {
+ let result = xhttp.response;
+ let xml = xhttp.responseXML;
+ console.log("arXiv Result: ", xml);
+ switch (this.status) {
+ case 200:
+ let title_vals: string[] = [];
+ let url_vals: string[] = [];
+ //console.log(result);
+ if (xml) {
+ let titles = xml.getElementsByTagName("title");
+ let counter = 1;
+ if (titles && titles.length > 1) {
+ while (counter <= maxresults) {
+ const title = titles[counter].childNodes[0].nodeValue!;
+ title_vals.push(title);
+ counter++;
+ }
+ }
+ let ids = xml.getElementsByTagName("id");
+ counter = 1;
+ if (ids && ids.length > 1) {
+ while (counter <= maxresults) {
+ const url = ids[counter].childNodes[0].nodeValue!;
+ url_vals.push(url);
+ counter++;
+ }
+ }
+ }
+ return resolve({ title_vals, url_vals });
+ case 400:
+ default:
+ return reject(result);
+ }
+ }
+ };
+ xhttp.open("GET", endpoint, true);
+ xhttp.send();
+ };
+ return new Promise<any>(promisified);
+ }
+
+ render() {
+ return (<div className="wrapper">
+ </div>);
+ }
+
+} \ No newline at end of file
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 7fbf30af8..0c9d5f75c 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,10 +1,12 @@
import * as OpenSocket from 'socket.io-client';
-import { MessageStore, YoutubeQueryTypes } from "./../server/Message";
+import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message";
import { Opt, Doc } from '../new_fields/Doc';
import { Utils, emptyFunction } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
import { RefField } from '../new_fields/RefField';
import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
+import GestureOverlay from './views/GestureOverlay';
+import MobileInkOverlay from '../mobile/MobileInkOverlay';
/**
* This class encapsulates the transfer and cross-client synchronization of
@@ -64,6 +66,26 @@ export namespace DocServer {
}
}
+ export namespace Mobile {
+
+ export function dispatchGesturePoints(content: GestureContent) {
+ Utils.Emit(_socket, MessageStore.GesturePoints, content);
+ }
+
+ export function dispatchOverlayTrigger(content: MobileInkOverlayContent) {
+ // _socket.emit("dispatchBoxTrigger");
+ Utils.Emit(_socket, MessageStore.MobileInkOverlayTrigger, content);
+ }
+
+ export function dispatchOverlayPositionUpdate(content: UpdateMobileInkOverlayPositionContent) {
+ Utils.Emit(_socket, MessageStore.UpdateMobileInkOverlayPosition, content);
+ }
+
+ export function dispatchMobileDocumentUpload(content: MobileDocumentUploadContent) {
+ Utils.Emit(_socket, MessageStore.MobileDocumentUpload, content);
+ }
+ }
+
const instructions = "This page will automatically refresh after this alert is closed. Expect to reconnect after about 30 seconds.";
function alertUser(connectionTerminationReason: string) {
switch (connectionTerminationReason) {
@@ -101,6 +123,21 @@ export namespace DocServer {
Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
Utils.AddServerHandler(_socket, MessageStore.ConnectionTerminated, alertUser);
+
+ // mobile ink overlay socket events to communicate between mobile view and desktop view
+ _socket.addEventListener("receiveGesturePoints", (content: GestureContent) => {
+ MobileInkOverlay.Instance.drawStroke(content);
+ });
+ _socket.addEventListener("receiveOverlayTrigger", (content: MobileInkOverlayContent) => {
+ GestureOverlay.Instance.enableMobileInkOverlay(content);
+ MobileInkOverlay.Instance.initMobileInkOverlay(content);
+ });
+ _socket.addEventListener("receiveUpdateOverlayPosition", (content: UpdateMobileInkOverlayPositionContent) => {
+ MobileInkOverlay.Instance.updatePosition(content);
+ });
+ _socket.addEventListener("receiveMobileDocumentUpload", (content: MobileDocumentUploadContent) => {
+ MobileInkOverlay.Instance.uploadDocument(content);
+ });
}
function errorFunc(): never {
diff --git a/src/client/apis/IBM_Recommender.ts b/src/client/apis/IBM_Recommender.ts
new file mode 100644
index 000000000..da6257f28
--- /dev/null
+++ b/src/client/apis/IBM_Recommender.ts
@@ -0,0 +1,40 @@
+import { Opt } from "../../new_fields/Doc";
+
+const NaturalLanguageUnderstandingV1 = require('ibm-watson/natural-language-understanding/v1');
+const { IamAuthenticator } = require('ibm-watson/auth');
+
+export namespace IBM_Recommender {
+
+ // pass to IBM account is Browngfx1
+
+ const naturalLanguageUnderstanding = new NaturalLanguageUnderstandingV1({
+ version: '2019-07-12',
+ authenticator: new IamAuthenticator({
+ apikey: 'tLiYwbRim3CnBcCO4phubpf-zEiGcub1uh0V-sD9OKhw',
+ }),
+ url: 'https://gateway-wdc.watsonplatform.net/natural-language-understanding/api'
+ });
+
+ const analyzeParams = {
+ 'text': 'this is a test of the keyword extraction feature I am integrating into the program',
+ 'features': {
+ 'keywords': {
+ 'sentiment': true,
+ 'emotion': true,
+ 'limit': 3
+ },
+ }
+ };
+
+ export const analyze = async (_parameters: any): Promise<Opt<string>> => {
+ try {
+ const response = await naturalLanguageUnderstanding.analyze(_parameters);
+ console.log(response);
+ return (JSON.stringify(response, null, 2));
+ } catch (err) {
+ console.log('error: ', err);
+ return undefined;
+ }
+ };
+
+} \ No newline at end of file
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 9e2ceac62..ce829eb1e 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -5,12 +5,18 @@ import { Docs } from "../documents/Documents";
import { Utils } from "../../Utils";
import { InkData } from "../../new_fields/InkField";
import { UndoManager } from "../util/UndoManager";
+import requestPromise = require("request-promise");
+import { List } from "../../new_fields/List";
+import { ClientRecommender } from "../ClientRecommender";
+import { ImageBox } from "../views/nodes/ImageBox";
type APIManager<D> = { converter: BodyConverter<D>, requester: RequestExecutor };
type RequestExecutor = (apiKey: string, body: string, service: Service) => Promise<string>;
type AnalysisApplier<D> = (target: Doc, relevantKeys: string[], data: D, ...args: any) => any;
type BodyConverter<D> = (data: D) => string;
type Converter = (results: any) => Field;
+type TextConverter = (results: any, data: string) => Promise<{ keyterms: Field, external_recommendations: any, kp_string: string[] }>;
+type BingConverter = (results: any) => Promise<{ title_vals: string[], url_vals: string[] }>;
export type Tag = { name: string, confidence: number };
export type Rectangle = { top: number, left: number, width: number, height: number };
@@ -18,7 +24,9 @@ export type Rectangle = { top: number, left: number, width: number, height: numb
export enum Service {
ComputerVision = "vision",
Face = "face",
- Handwriting = "handwriting"
+ Handwriting = "handwriting",
+ Text = "text",
+ Bing = "bing"
}
export enum Confidence {
@@ -47,7 +55,8 @@ export namespace CognitiveServices {
let results: any;
try {
results = await manager.requester(apiKey, manager.converter(data), service).then(json => JSON.parse(json));
- } catch {
+ } catch (e) {
+ throw e;
results = undefined;
}
return results;
@@ -193,6 +202,13 @@ export namespace CognitiveServices {
batch.end();
};
+ export const InterpretStrokes = async (strokes: InkData[]) => {
+ let results = await ExecuteQuery(Service.Handwriting, Manager, strokes);
+ if (results) {
+ results.recognitionUnits && (results = results.recognitionUnits);
+ }
+ return results;
+ }
}
export interface AzureStrokeData {
@@ -210,4 +226,185 @@ export namespace CognitiveServices {
}
+ export namespace BingSearch {
+ export const Manager: APIManager<string> = {
+ converter: (data: string) => {
+ return data;
+ },
+ requester: async (apiKey: string, query: string) => {
+ let xhttp = new XMLHttpRequest();
+ let serverAddress = "https://api.cognitive.microsoft.com";
+ let endpoint = serverAddress + '/bing/v5.0/search?q=' + encodeURIComponent(query);
+ let promisified = (resolve: any, reject: any) => {
+ xhttp.onreadystatechange = function () {
+ if (this.readyState === 4) {
+ let result = xhttp.responseText;
+ switch (this.status) {
+ case 200:
+ return resolve(result);
+ case 400:
+ default:
+ return reject(result);
+ }
+ }
+ };
+
+ if (apiKey) {
+ xhttp.open("GET", endpoint, true);
+ xhttp.setRequestHeader('Ocp-Apim-Subscription-Key', apiKey);
+ xhttp.setRequestHeader('Content-Type', 'application/json');
+ xhttp.send();
+ }
+ else {
+ console.log("API key for BING unavailable");
+ }
+ };
+ return new Promise<any>(promisified);
+ }
+
+ };
+
+ export namespace Appliers {
+ export const analyzer = async (query: string, converter: BingConverter) => {
+ let results = await ExecuteQuery(Service.Bing, Manager, query);
+ console.log("Bing results: ", results);
+ const { title_vals, url_vals } = await converter(results);
+ return { title_vals, url_vals };
+ };
+ }
+
+ }
+
+ export namespace HathiTrust {
+ export const Manager: APIManager<string> = {
+ converter: (data: string) => {
+ return data;
+ },
+ requester: async (apiKey: string, query: string) => {
+ let xhttp = new XMLHttpRequest();
+ let serverAddress = "https://babel.hathitrust.org/cgi/htd/​";
+ let endpoint = serverAddress + '/bing/v5.0/search?q=' + encodeURIComponent(query);
+ let promisified = (resolve: any, reject: any) => {
+ xhttp.onreadystatechange = function () {
+ if (this.readyState === 4) {
+ let result = xhttp.responseText;
+ switch (this.status) {
+ case 200:
+ return resolve(result);
+ case 400:
+ default:
+ return reject(result);
+ }
+ }
+ };
+
+ if (apiKey) {
+ xhttp.open("GET", endpoint, true);
+ xhttp.setRequestHeader('Ocp-Apim-Subscription-Key', apiKey);
+ xhttp.setRequestHeader('Content-Type', 'application/json');
+ xhttp.send();
+ }
+ else {
+ console.log("API key for BING unavailable");
+ }
+ };
+ return new Promise<any>(promisified);
+ }
+
+ };
+
+ export namespace Appliers {
+ export const analyzer = async (query: string, converter: BingConverter) => {
+ let results = await ExecuteQuery(Service.Bing, Manager, query);
+ console.log("Bing results: ", results);
+ const { title_vals, url_vals } = await converter(results);
+ return { title_vals, url_vals };
+ };
+ }
+
+ }
+
+
+ export namespace Text {
+ export const Manager: APIManager<string> = {
+ converter: (data: string) => {
+ return JSON.stringify({
+ documents: [{
+ id: 1,
+ language: "en",
+ text: data
+ }]
+ });
+ },
+ requester: async (apiKey: string, body: string, service: Service) => {
+ let serverAddress = "https://eastus.api.cognitive.microsoft.com";
+ let endpoint = serverAddress + "/text/analytics/v2.1/keyPhrases";
+ let sampleBody = {
+ "documents": [
+ {
+ "language": "en",
+ "id": 1,
+ "text": "Hello world. This is some input text that I love."
+ }
+ ]
+ };
+ let actualBody = body;
+ const options = {
+ uri: endpoint,
+ body: actualBody,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Ocp-Apim-Subscription-Key': apiKey
+ }
+
+ };
+ return request.post(options);
+ }
+ };
+
+ export namespace Appliers {
+
+ export async function vectorize(keyterms: any, dataDoc: Doc, mainDoc: boolean = false) {
+ console.log("vectorizing...");
+ //keyterms = ["father", "king"];
+
+ let args = { method: 'POST', uri: Utils.prepend("/recommender"), body: { keyphrases: keyterms }, json: true };
+ await requestPromise.post(args).then(async (wordvecs) => {
+ if (wordvecs) {
+ let indices = Object.keys(wordvecs);
+ console.log("successful vectorization!");
+ var vectorValues = new List<number>();
+ indices.forEach((ind: any) => {
+ //console.log(wordvec.word);
+ vectorValues.push(wordvecs[ind]);
+ });
+ ClientRecommender.Instance.processVector(vectorValues, dataDoc, mainDoc);
+ } // adds document to internal doc set
+ else {
+ console.log("unsuccessful :( word(s) not in vocabulary");
+ }
+ //console.log(vectorValues.size);
+ }
+ );
+ }
+
+ export const analyzer = async (dataDoc: Doc, target: Doc, keys: string[], data: string, converter: TextConverter, isMainDoc: boolean = false, isInternal: boolean = true) => {
+ let results = await ExecuteQuery(Service.Text, Manager, data);
+ console.log("Cognitive Services keyphrases: ", results);
+ let { keyterms, external_recommendations, kp_string } = await converter(results, data);
+ target[keys[0]] = keyterms;
+ if (isInternal) {
+ //await vectorize([data], dataDoc, isMainDoc);
+ await vectorize(kp_string, dataDoc, isMainDoc);
+ } else {
+ return { recs: external_recommendations, keyterms: keyterms };
+ }
+ };
+
+ // export async function countFrequencies()
+ }
+
+ }
+
+
} \ No newline at end of file
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 06b15d78c..e3ce8e798 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -11,7 +11,7 @@ export enum DocumentType {
PDF = "pdf",
IMPORT = "import",
LINK = "link",
- LINKDOC = "linkdoc",
+ LINKDB = "linkdb",
BUTTON = "button",
SLIDER = "slider",
EXTENSION = "extension",
@@ -19,6 +19,8 @@ export enum DocumentType {
WEBCAM = "webcam",
FONTICON = "fonticonbox",
PRES = "presentation",
+ RECOMMENDATION = "recommendation",
+ LINKFOLLOW = "linkfollow",
PRESELEMENT = "preselement",
QUERY = "search",
COLOR = "color",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b16e03f66..5f0e63b56 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -40,6 +40,9 @@ import { PresBox } from "../views/nodes/PresBox";
import { ComputedField, ScriptField } from "../../new_fields/ScriptField";
import { ProxyField } from "../../new_fields/Proxy";
import { DocumentType } from "./DocumentTypes";
+import { RecommendationsBox } from "../views/RecommendationsBox";
+//import { PresBox } from "../views/nodes/PresBox";
+//import { PresField } from "../../new_fields/PresField";
import { PresElementBox } from "../views/presentationview/PresElementBox";
import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
import { QueryBox } from "../views/nodes/QueryBox";
@@ -72,6 +75,7 @@ export interface DocumentOptions {
_showTitleHover?: string; //
_showTitle?: string; // which field to display in the title area. leave empty to have no title
_showCaption?: string; // which field to display in the caption area. leave empty to have no caption
+ _scrollTop?: number; // scroll location for pdfs
_chromeStatus?: string;
_viewType?: number;
_gridGap?: number; // gap between items in masonry view
@@ -90,6 +94,7 @@ export interface DocumentOptions {
layoutKey?: string;
type?: string;
title?: string;
+ style?: string;
page?: number;
scale?: number;
isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents
@@ -103,6 +108,7 @@ export interface DocumentOptions {
_backgroundColor?: string | ScriptField; // background color for each template layout doc ( overrides backgroundColor )
color?: string; // foreground color data doc
_color?: string; // foreground color for each template layout doc (overrides color)
+ caption?: RichTextField;
ignoreClick?: boolean;
lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged
lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
@@ -117,7 +123,7 @@ export interface DocumentOptions {
displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
borderRounding?: string;
boxShadow?: string;
- sectionFilter?: string; // field key used to determine headings for sections in stacking and masonry views
+ _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views
schemaColumns?: List<SchemaHeaderField>;
dockingConfig?: string;
annotationOn?: Doc;
@@ -147,9 +153,12 @@ export interface DocumentOptions {
limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents
// [key: string]: Opt<Field>;
pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown
+ textTransform?: string; // is linear view expanded
+ letterSpacing?: string; // is linear view expanded
+ flexDirection?: "unset" | "row" | "column" | "row-reverse" | "column-reverse";
+ selectedIndex?: number;
+ syntaxColor?: string; // can be applied to text for syntax highlighting all matches in the text
linearViewIsExpanded?: boolean; // is linear view expanded
- textTransform?: string;
- letterSpacing?: string;
}
class EmptyBox {
@@ -176,7 +185,7 @@ export namespace Docs {
const TemplateMap: TemplateMap = new Map([
[DocumentType.TEXT, {
- layout: { view: FormattedTextBox, dataField: data },
+ layout: { view: FormattedTextBox, dataField: "text" },
options: { _height: 150, _xMargin: 10, _yMargin: 10 }
}],
[DocumentType.HIST, {
@@ -231,7 +240,7 @@ export namespace Docs {
layout: { view: LinkBox, dataField: data },
options: { _height: 150 }
}],
- [DocumentType.LINKDOC, {
+ [DocumentType.LINKDB, {
data: new List<Doc>(),
layout: { view: EmptyBox, dataField: data },
options: { childDropAction: "alias", title: "LINK DB" }
@@ -253,6 +262,10 @@ export namespace Docs {
layout: { view: FontIconBox, dataField: data },
options: { _width: 40, _height: 40, borderRounding: "100%" },
}],
+ [DocumentType.RECOMMENDATION, {
+ layout: { view: RecommendationsBox },
+ options: { width: 200, height: 200 },
+ }],
[DocumentType.WEBCAM, {
layout: { view: DashWebRTCVideo, dataField: data }
}],
@@ -315,7 +328,7 @@ export namespace Docs {
* A collection of all links in the database. Ideally, this would be a search, but for now all links are cached here.
*/
export function MainLinkDocument() {
- return Prototypes.get(DocumentType.LINKDOC);
+ return Prototypes.get(DocumentType.LINKDB);
}
/**
@@ -410,7 +423,7 @@ export namespace Docs {
const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "dropAction", "childDropAction", "_annotationOn",
"_chromeStatus", "_forceActive", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar", "_showTitle", "_showCaption", "_showTitleHover", "_backgroundColor",
- "_xMargin", "_yMargin", "_xPadding", "_yPadding", "_singleLine",
+ "_xMargin", "_yMargin", "_xPadding", "_yPadding", "_singleLine", "_scrollTop",
"_color", "isButton", "isBackground", "removeDropProperties", "treeViewOpen"];
/**
@@ -431,7 +444,7 @@ export namespace Docs {
* only when creating a DockDocument from the current user's already existing
* main document.
*/
- export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string) {
+ export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data") {
const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
if (!("author" in protoProps)) {
@@ -444,10 +457,10 @@ export namespace Docs {
protoProps.isPrototype = true;
- const dataDoc = MakeDataDelegate(proto, protoProps, data);
+ const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey);
const viewDoc = Doc.MakeDelegate(dataDoc, delegId);
- AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title));
+ viewDoc.type !== DocumentType.LINK && AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "audio timeline"));
return Doc.assign(viewDoc, delegateProps, true);
}
@@ -462,10 +475,10 @@ export namespace Docs {
* @param options initial values to apply to this new delegate
* @param value the data to store in this new delegate
*/
- function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D) {
+ function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D, fieldKey: string = "data") {
const deleg = Doc.MakeDelegate(proto);
if (value !== undefined) {
- deleg.data = value;
+ deleg[fieldKey] = value;
}
return Doc.assign(deleg, options);
}
@@ -508,7 +521,9 @@ export namespace Docs {
}
export function AudioDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options);
+ const instance = InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options);
+ Doc.GetProto(instance).backgroundColor = ComputedField.MakeFunction("this._audioState === 'playing' ? 'green':'gray'");
+ return instance;
}
export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
@@ -524,7 +539,7 @@ export namespace Docs {
}
export function TextDocument(text: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.TEXT), text, options);
+ return InstanceFromProto(Prototypes.get(DocumentType.TEXT), text, options, undefined, "text");
}
export function LinkDocument(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, options: DocumentOptions = {}, id?: string) {
@@ -534,14 +549,14 @@ export namespace Docs {
linkDocProto.anchor2 = target.doc;
linkDocProto.anchor1Context = source.ctx;
linkDocProto.anchor2Context = target.ctx;
- linkDocProto.anchor1Timecode = source.doc.currentTimecode;
- linkDocProto.anchor2Timecode = target.doc.currentTimecode;
+ linkDocProto.anchor1Timecode = source.doc.currentTimecode || source.doc.displayTimecode;
+ linkDocProto.anchor2Timecode = target.doc.currentTimecode || source.doc.displayTimecode;
if (linkDocProto.layout_key1 === undefined) {
- Cast(linkDocProto.proto, Doc, null)!.layout_key1 = DocuLinkBox.LayoutString("anchor1");
- Cast(linkDocProto.proto, Doc, null)!.layout_key2 = DocuLinkBox.LayoutString("anchor2");
- Cast(linkDocProto.proto, Doc, null)!.linkBoxExcludedKeys = new List(["treeViewExpandedView", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "proto", "aliasNumber", "isPrototype", "lastOpened", "creationDate", "author"]);
- Cast(linkDocProto.proto, Doc, null)!.layoutKey = undefined;
+ Cast(linkDocProto.proto, Doc, null).layout_key1 = DocuLinkBox.LayoutString("anchor1");
+ Cast(linkDocProto.proto, Doc, null).layout_key2 = DocuLinkBox.LayoutString("anchor2");
+ Cast(linkDocProto.proto, Doc, null).linkBoxExcludedKeys = new List(["treeViewExpandedView", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "proto", "aliasNumber", "isPrototype", "lastOpened", "creationDate", "author"]);
+ Cast(linkDocProto.proto, Doc, null).layoutKey = undefined;
}
LinkManager.Instance.addLink(doc);
@@ -673,6 +688,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.IMPORT), new List<Doc>(), options);
}
+ export function RecommendationsDocument(data: Doc[], options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.RECOMMENDATION), new List<Doc>(data), options);
+ }
+
export type DocConfig = {
doc: Doc,
initialWidth?: number,
@@ -896,13 +915,13 @@ export namespace DocUtils {
});
}
- export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", linkRelationship: string = "", id?: string) {
+ export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, linkRelationship: string = "", id?: string) {
const sv = DocumentManager.Instance.getDocumentView(source.doc);
if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
if (target.doc === CurrentUserUtils.UserDocument) return undefined;
- const linkDoc = Docs.Create.LinkDocument(source, target, { title, linkRelationship }, id);
- Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('this.anchor1.title +" " + (this.linkRelationship||"to") +" " + this.anchor2.title');
+ const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship }, id);
+ Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('this.anchor1.title +" (" + (this.linkRelationship||"to") +") " + this.anchor2.title');
Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(this)");
Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(this)");
@@ -914,7 +933,15 @@ export namespace DocUtils {
description: "Add Note ...",
subitems: DocListCast((Doc.UserDoc().noteTypes as Doc).data).map((note, i) => ({
description: ":" + StrCast(note.title),
- event: (args: { x: number, y: number }) => docTextAdder(Docs.Create.TextDocument("", { _width: 200, x, y, _autoHeight: note._autoHeight !== false, layout: note, title: StrCast(note.title) + "#" + (note.aliasCount = NumCast(note.aliasCount) + 1) })),
+ event: (args: { x: number, y: number }) => {
+ const textDoc = Docs.Create.TextDocument("", {
+ _width: 200, x, y, _autoHeight: note._autoHeight !== false,
+ title: StrCast(note.title) + "#" + (note.aliasCount = NumCast(note.aliasCount) + 1)
+ });
+ textDoc.layoutKey = "layout_" + note.title;
+ textDoc[textDoc.layoutKey] = note;
+ docTextAdder(textDoc);
+ },
icon: "eye"
})) as ContextMenuProps[],
icon: "eye"
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index c639f07f5..323d31af2 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -55,7 +55,7 @@ export class DocumentManager {
}
public getDocumentViewById(id: string, preferredCollection?: CollectionView): DocumentView | undefined {
-
+ if (!id) return undefined;
let toReturn: DocumentView | undefined;
const passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
@@ -134,7 +134,7 @@ export class DocumentManager {
finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document);
};
const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc, originatingDoc);
- let annotatedDoc = await Cast(docView?.props.Document.annotationOn, Doc);
+ let annotatedDoc = await Cast(targetDoc.annotationOn, Doc);
if (annotatedDoc) {
const first = DocumentManager.Instance.getFirstDocumentView(annotatedDoc);
if (first) annotatedDoc = first.props.Document;
@@ -158,16 +158,20 @@ export class DocumentManager {
targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom);
// now find the target document within the context
- setTimeout(() => {
- const retryDocView = DocumentManager.Instance.getDocumentView(targetDoc);
- if (retryDocView) {
- retryDocView.props.focus(targetDoc, willZoom); // focus on the target if it now exists in the context
- } else {
- if (closeContextIfNotFound && targetDocContextView.props.removeDocument) targetDocContextView.props.removeDocument(targetDocContextView.props.Document);
- targetDoc.layout && (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc))); // otherwise create a new view of the target
- }
- highlight();
- }, 0);
+ if (targetDoc.displayTimecode) { // the target should show up once the video scrubs to the display timecode;
+ targetDocContext.currentTimecode = targetDoc.displayTimecode;
+ } else {
+ setTimeout(() => {
+ const retryDocView = DocumentManager.Instance.getDocumentView(targetDoc);
+ if (retryDocView) {
+ retryDocView.props.focus(targetDoc, willZoom); // focus on the target if it now exists in the context
+ } else {
+ if (closeContextIfNotFound && targetDocContextView.props.removeDocument) targetDocContextView.props.removeDocument(targetDocContextView.props.Document);
+ targetDoc.layout && (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc))); // otherwise create a new view of the target
+ }
+ highlight();
+ }, 0);
+ }
} else { // there's no context view so we need to create one first and try again
(dockFunc || CollectionDockingView.AddRightSplit)(targetDocContext);
setTimeout(() => {
@@ -195,7 +199,7 @@ export class DocumentManager {
const linkFollowDocContexts = first.length ? [await first[0].anchor2Context as Doc, await first[0].anchor1Context as Doc] : second.length ? [await second[0].anchor1Context as Doc, await second[0].anchor2Context as Doc] : [undefined, undefined];
const linkFollowTimecodes = first.length ? [NumCast(first[0].anchor2Timecode), NumCast(first[0].anchor1Timecode)] : second.length ? [NumCast(second[0].anchor1Timecode), NumCast(second[0].anchor2Timecode)] : [undefined, undefined];
if (linkFollowDocs && linkDoc) {
- const maxLocation = StrCast(linkFollowDocs[0].maximizeLocation, "inTab");
+ const maxLocation = StrCast(linkDoc.maximizeLocation, "inTab");
const targetContext = !Doc.AreProtosEqual(linkFollowDocContexts[reverse ? 1 : 0], currentContext) ? linkFollowDocContexts[reverse ? 1 : 0] : undefined;
const target = linkFollowDocs[reverse ? 1 : 0];
target.currentTimecode !== undefined && (target.currentTimecode = linkFollowTimecodes[reverse ? 1 : 0]);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 2877d5fd7..8ddd59237 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -7,13 +7,15 @@ import { DocumentManager } from "./DocumentManager";
import { LinkManager } from "./LinkManager";
import { SelectionManager } from "./SelectionManager";
import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
-import { Docs } from "../documents/Documents";
+import { Docs, DocUtils } from "../documents/Documents";
import { ScriptField } from "../../new_fields/ScriptField";
import { List } from "../../new_fields/List";
import { PrefetchProxy } from "../../new_fields/Proxy";
import { listSpec } from "../../new_fields/Schema";
import { Scripting } from "./Scripting";
import { convertDropDataToButtons } from "./DropConverter";
+import { AudioBox } from "../views/nodes/AudioBox";
+import { DateField } from "../../new_fields/DateField";
export type dropActionType = "alias" | "copy" | undefined;
export function SetupDrag(
@@ -133,6 +135,7 @@ export namespace DragManager {
userDropAction: dropActionType;
embedDoc?: boolean;
moveDocument?: MoveFunction;
+ applyAsTemplate?: boolean;
isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts
}
export class LinkDragData {
@@ -179,7 +182,7 @@ export namespace DragManager {
);
}
element.dataset.canDrop = "true";
- const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail);
+ const handler = (e: Event) => { dropFunc(e, (e as CustomEvent<DropEvent>).detail); };
element.addEventListener("dashOnDrop", handler);
return () => {
element.removeEventListener("dashOnDrop", handler);
@@ -189,9 +192,14 @@ export namespace DragManager {
// drag a document and drop it (or make an alias/copy on drop)
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+ const addAudioTag = (dropDoc: any) => {
+ dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField);
+ dropDoc instanceof Doc && AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: dropDoc }, { doc: d }, "audio link", "audio timeline"));
+ return dropDoc;
+ }
const finishDrag = (e: DragCompleteEvent) => {
e.docDragData && (e.docDragData.droppedDocuments =
- dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? ScriptCast(d.onDragStart).script.run({ this: d }).result :
+ dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) :
dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) :
dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeCopy(d, true) : d)
);
@@ -268,6 +276,10 @@ export namespace DragManager {
StartDrag([ele], dragData, downX, downY, options);
}
+ export function StartImgDrag(ele: HTMLElement, downX: number, downY: number) {
+ StartDrag([ele], {}, downX, downY);
+ }
+
function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
eles = eles.filter(e => e);
if (!dragDiv) {
@@ -382,8 +394,8 @@ export namespace DragManager {
hideDragShowOriginalElements();
dispatchDrag(eles, e, dragData, options, finishDrag);
SelectionManager.SetIsDragging(false);
- options?.dragComplete?.(new DragCompleteEvent(false, dragData));
endDrag();
+ options?.dragComplete?.(new DragCompleteEvent(false, dragData));
};
document.addEventListener("pointermove", moveHandler, true);
document.addEventListener("pointerup", upHandler);
@@ -404,7 +416,7 @@ export namespace DragManager {
if (target) {
const complete = new DragCompleteEvent(false, dragData);
finishDrag?.(complete);
-
+ console.log(complete.aborted);
target.dispatchEvent(
new CustomEvent<DropEvent>("dashOnDrop", {
bubbles: true,
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 3c7caa60b..861cde5de 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -1,5 +1,5 @@
import { DragManager } from "./DragManager";
-import { Doc, DocListCast } from "../../new_fields/Doc";
+import { Doc, DocListCast, Opt } from "../../new_fields/Doc";
import { DocumentType } from "../documents/DocumentTypes";
import { ObjectField } from "../../new_fields/ObjectField";
import { StrCast } from "../../new_fields/Types";
@@ -8,7 +8,12 @@ import { ScriptField, ComputedField } from "../../new_fields/ScriptField";
import { RichTextField } from "../../new_fields/RichTextField";
import { ImageField } from "../../new_fields/URLField";
-export function makeTemplate(doc: Doc, first: boolean = true): boolean {
+//
+// converts 'doc' into a template that can be used to render other documents.
+// the title of doc is used to determine which field is being templated, so
+// passing a value for 'rename' allows the doc to be given a meangingful name
+// after it has been converted to
+export function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0];
const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, "");
@@ -29,16 +34,17 @@ export function makeTemplate(doc: Doc, first: boolean = true): boolean {
any = Doc.MakeMetadataFieldTemplate(layoutDoc, Doc.GetProto(layoutDoc));
}
}
+ rename && (doc.title = rename);
return any;
}
export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data && data.draggedDocuments.map((doc, i) => {
let dbox = doc;
// bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant
- if (!doc.onDragStart && !doc.onClick && !doc.isButtonBar) {
+ if (!doc.onDragStart && !doc.isButtonBar) {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
if (layoutDoc.type === DocumentType.COL || layoutDoc.type === DocumentType.TEXT || layoutDoc.type === DocumentType.IMG) {
- makeTemplate(layoutDoc);
+ !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
} else {
(layoutDoc.layout instanceof Doc) && !data.userDropAction;
}
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 7194feb2e..2eec02a42 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,3 +1,5 @@
+import React = require("react");
+
export namespace InteractionUtils {
export const MOUSETYPE = "mouse";
export const TOUCHTYPE = "touch";
@@ -37,7 +39,7 @@ export namespace InteractionUtils {
export function MakeMultiTouchTarget(
element: HTMLElement,
- startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void,
+ startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void
): MultiTouchEventDisposer {
const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
// const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
@@ -60,6 +62,17 @@ export namespace InteractionUtils {
};
}
+ export function MakeHoldTouchTarget(
+ element: HTMLElement,
+ func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void
+ ): MultiTouchEventDisposer {
+ const handler = (e: Event) => func(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
+ element.addEventListener("dashOnTouchHoldStart", handler);
+ return () => {
+ element.removeEventListener("dashOnTouchHoldStart", handler);
+ };
+ }
+
export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent<React.TouchEvent | TouchEvent>, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
const myTouches = new Array<React.Touch>();
for (const pt of mte.touches) {
@@ -79,6 +92,21 @@ export namespace InteractionUtils {
return myTouches;
}
+ // TODO: find a way to reference this function from InkingStroke instead of copy pastign here. copied bc of weird error when on mobile view
+ export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number) {
+ const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+ return (
+ <polyline
+ points={pts}
+ style={{
+ fill: "none",
+ stroke: color,
+ strokeWidth: width
+ }}
+ />
+ );
+ }
+
export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
switch (type) {
// pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 460f1fa37..3f0ec7aa5 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -146,7 +146,7 @@ export default class RichTextMenu extends AntimodeMenu {
public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => {
if (this.view) {
- const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, targetId: targetDocId });
+ const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId });
this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
addMark(this.view.state.selection.from, this.view.state.selection.to, link));
return this.view.state.selection.$from.nodeAfter?.text || "";
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index af3b1a81e..e5b20a79b 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -81,25 +81,39 @@ export class RichTextRules {
// create a text display of a metadata field on this or another document, or create a hyperlink portal to another document [[ <fieldKey> : <Doc>]] // [[:Doc]] => hyperlink [[fieldKey]] => show field [[fieldKey:Doc]] => show field of doc
new InputRule(
- new RegExp(/\[\[([a-zA-Z_#@\? \-0-9]*)(:[a-zA-Z_#@\? \-0-9]+)?\]\]$/),
+ new RegExp(/\[\[([a-zA-Z_#@\? \-0-9]*)(=[a-zA-Z_#@\? \-0-9]*)?(:[a-zA-Z_#@\? \-0-9]+)?\]\]$/),
(state, match, start, end) => {
const fieldKey = match[1];
- const docid = match[2]?.substring(1);
+ const docid = match[3]?.substring(1);
+ const value = match[2]?.substring(1);
if (!fieldKey) {
if (docid) {
DocServer.GetRefField(docid).then(docx => {
const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid);
DocUtils.Publish(target, docid, returnFalse, returnFalse);
- DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal link", "");
+ DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to");
});
const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid });
return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link);
}
return state.tr;
}
+ if (value !== "") {
+ this.Document[DataSym][fieldKey] = value;
+ }
const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid });
return state.tr.deleteRange(start, end).insert(start, fieldView);
}),
+ // create an inline view of a tag stored under the '#' field
+ new InputRule(
+ new RegExp(/#([a-zA-Z_\-0-9]+)\s$/),
+ (state, match, start, end) => {
+ const tag = match[1];
+ if (!tag) return state.tr;
+ this.Document[DataSym]["#"] = tag;
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" });
+ return state.tr.deleteRange(start, end).insert(start, fieldView);
+ }),
// create an inline view of a document {{ <layoutKey> : <Doc> }} // {{:Doc}} => show default view of document {{<layout>}} => show layout for this doc {{<layout> : Doc}} => show layout for another doc
new InputRule(
new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(:[a-zA-Z_ \-0-9]+)?\}\}$/),
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 4a80a1af8..0adf060ec 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -8,24 +8,25 @@ import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-s
import { StepMap } from "prosemirror-transform";
import { EditorView } from "prosemirror-view";
import * as ReactDOM from 'react-dom';
-import { Doc, Field, HeightSym, WidthSym, DocListCast } from "../../new_fields/Doc";
+import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../new_fields/Doc";
import { Id } from "../../new_fields/FieldSymbols";
+import { List } from "../../new_fields/List";
import { ObjectField } from "../../new_fields/ObjectField";
+import { listSpec } from "../../new_fields/Schema";
+import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
import { ComputedField } from "../../new_fields/ScriptField";
-import { BoolCast, NumCast, StrCast, Cast } from "../../new_fields/Types";
+import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../Utils";
import { DocServer } from "../DocServer";
+import { Docs } from "../documents/Documents";
+import { CollectionViewType } from "../views/collections/CollectionView";
+import { ContextMenu } from "../views/ContextMenu";
import { DocumentView } from "../views/nodes/DocumentView";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
import { DocumentManager } from "./DocumentManager";
import ParagraphNodeSpec from "./ParagraphNodeSpec";
import { Transform } from "./Transform";
import React = require("react");
-import { CollectionSchemaBooleanCell } from "../views/collections/CollectionSchemaCells";
-import { ContextMenu } from "../views/ContextMenu";
-import { ContextMenuProps } from "../views/ContextMenuItem";
-import { Docs } from "../documents/Documents";
-import { CollectionView } from "../views/collections/CollectionView";
const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -300,6 +301,7 @@ export const marks: { [index: string]: MarkSpec } = {
attrs: {
href: {},
targetId: { default: "" },
+ linkId: { default: "" },
showPreview: { default: true },
location: { default: null },
title: { default: null },
@@ -314,7 +316,7 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM(node: any) {
return node.attrs.docref && node.attrs.title ?
["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] :
- ["a", { ...node.attrs, id: node.attrs.targetId, title: `${node.attrs.title}` }, 0];
+ ["a", { ...node.attrs, id: node.attrs.linkId + node.attrs.targetId, title: `${node.attrs.title}` }, 0];
}
},
@@ -509,7 +511,7 @@ export const marks: { [index: string]: MarkSpec } = {
user_mark: {
attrs: {
userid: { default: "" },
- modified: { default: "when?" }, // 5 second intervals since 1970
+ modified: { default: "when?" }, // 1 second intervals since 1970
},
group: "inline",
toDOM(node: any) {
@@ -525,7 +527,7 @@ export const marks: { [index: string]: MarkSpec } = {
user_tag: {
attrs: {
userid: { default: "" },
- modified: { default: "when?" }, // 5 second intervals since 1970
+ modified: { default: "when?" }, // 1 second intervals since 1970
tag: { default: "" }
},
group: "inline",
@@ -796,15 +798,8 @@ export class DashDocView {
}
doRender(dashDoc: Doc, removeDoc: any, node: any, view: any, getPos: any) {
this._dashDoc = dashDoc;
- if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") {
- try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
- view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
- } catch (e) {
- console.log(e);
- }
- }
const self = this;
- const finalLayout = Doc.expandTemplateLayout(dashDoc, !Doc.AreProtosEqual(this._textBox.dataDoc, this._textBox.Document) ? this._textBox.dataDoc : undefined);
+ const finalLayout = this._textBox.props.Document instanceof Doc && (Doc.expandTemplateLayout(dashDoc, !Doc.AreProtosEqual(this._textBox.dataDoc, this._textBox.props.Document) ? this._textBox.dataDoc : undefined));
if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
else {
const layoutKey = StrCast(finalLayout.layoutKey);
@@ -842,13 +837,21 @@ export class DashDocView {
zoomToScale={emptyFunction}
getScale={returnOne}
dontRegisterView={false}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
+ ContainingCollectionView={this._textBox.props.ContainingCollectionView}
+ ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
ContentScaling={this.contentScaling}
/>, this._dashSpan);
+ if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") {
+ try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
+ } catch (e) {
+ console.log(e);
+ }
+ }
}
}
destroy() {
+ ReactDOM.unmountComponentAtNode(this._dashSpan);
this._reactionDisposer?.();
}
}
@@ -858,6 +861,7 @@ export class DashFieldView {
_fieldWrapper: HTMLDivElement; // container for label and value
_labelSpan: HTMLSpanElement; // field label
_fieldSpan: HTMLDivElement; // field value
+ _enumerables: HTMLDivElement; // field value
_reactionDisposer: IReactionDisposer | undefined;
_textBoxDoc: Doc;
@observable _dashDoc: Doc | undefined;
@@ -874,29 +878,60 @@ export class DashFieldView {
this._fieldWrapper.style.display = "inline-block";
const self = this;
+ this._enumerables = document.createElement("div");
+ this._enumerables.style.width = "10px";
+ this._enumerables.style.height = "10px";
+ this._enumerables.style.position = "relative";
+ this._enumerables.style.display = "none";
+ this._enumerables.style.background = "dimGray";
+
+ this._enumerables.onpointerdown = async (e) => {
+ e.stopPropagation();
+ const collview = await Doc.addFieldEnumerations(self._textBoxDoc, node.attrs.fieldKey, [{ title: self._fieldSpan.innerText }]);
+ collview instanceof Doc && tbox.props.addDocTab(collview, "onRight");
+ }
+ const updateText = (forceMatch: boolean) => {
+ self._enumerables.style.display = "none";
+ let newText = self._fieldSpan.innerText.startsWith(":=") || self._fieldSpan.innerText.startsWith("=:=") ? ":=-computed-" : self._fieldSpan.innerText;
+
+ // look for a document whose id === the fieldKey being displayed. If there's a match, then that document
+ // holds the different enumerated values for the field in the titles of its collected documents.
+ // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
+ DocServer.GetRefField(node.attrs.fieldKey).then(options => {
+ let modText = "";
+ (options instanceof Doc) && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title)));
+ if (modText) {
+ self._fieldSpan.innerHTML = self._dashDoc![self._fieldKey] = modText;
+ Doc.addFieldEnumerations(self._textBoxDoc, node.attrs.fieldKey, []);
+ } else if (!self._fieldSpan.innerText.startsWith(":=") && !self._fieldSpan.innerText.startsWith("=:=")) {
+ self._dashDoc![self._fieldKey] = newText;
+ }
+
+ // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key
+ if (self._fieldSpan.innerText.startsWith(":=") && self._dashDoc) {
+ self._dashDoc![self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(2));
+ } else if (self._fieldSpan.innerText.startsWith("=:=") && self._dashDoc) {
+ Doc.Layout(tbox.props.Document)[self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(3));
+ }
+ });
+ }
+
this._fieldSpan = document.createElement("div");
this._fieldSpan.id = Utils.GenerateGuid();
this._fieldSpan.contentEditable = "true";
this._fieldSpan.style.position = "relative";
this._fieldSpan.style.display = "inline-block";
- this._fieldSpan.style.minWidth = "50px";
+ this._fieldSpan.style.minWidth = "12px";
this._fieldSpan.style.backgroundColor = "rgba(155, 155, 155, 0.24)";
this._fieldSpan.onkeypress = function (e: any) { e.stopPropagation(); };
this._fieldSpan.onkeyup = function (e: any) { e.stopPropagation(); };
- this._fieldSpan.onmousedown = function (e: any) { e.stopPropagation(); };
- this._fieldSpan.oncontextmenu = function (e: any) {
- ContextMenu.Instance.addItem({
- description: "Show Enumeration Templates", event: () => {
- e.stopPropagation();
- DocServer.GetRefField(node.attrs.fieldKey).then(collview => collview instanceof Doc && tbox.props.addDocTab(collview, "onRight"));
- }, icon: "expand-arrows-alt"
- });
- };
+ this._fieldSpan.onmousedown = function (e: any) { e.stopPropagation(); self._enumerables.style.display = "inline-block"; };
+ this._fieldSpan.onblur = function (e: any) { updateText(false); }
const setDashDoc = (doc: Doc) => {
self._dashDoc = doc;
- if (this._dashDoc && self._options?.length && !this._dashDoc[node.attrs.fieldKey]) {
- this._dashDoc[node.attrs.fieldKey] = StrCast(self._options[0].title);
+ if (self._dashDoc && self._options?.length && !self._dashDoc[node.attrs.fieldKey]) {
+ self._dashDoc[node.attrs.fieldKey] = StrCast(self._options[0].title);
}
}
this._fieldSpan.onkeydown = function (e: any) {
@@ -912,30 +947,36 @@ export class DashFieldView {
}
if (e.key === "Enter") {
e.preventDefault();
- if (e.ctrlKey) {
- Doc.addEnumerationToTextField(self._textBoxDoc, node.attrs.fieldKey, [Docs.Create.TextDocument(self._fieldSpan.innerText, { title: self._fieldSpan.innerText })]);
- }
- let newText = self._fieldSpan.innerText.startsWith(":=") ? ":=-computed-" : self._fieldSpan.innerText;
- // look for a document whose id === the fieldKey being displayed. If there's a match, then that document
- // holds the different enumerated values for the field in the titles of its collected documents.
- // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
-
- // alternatively, if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key
- DocServer.GetRefField(node.attrs.fieldKey).then(options => {
- (options instanceof Doc) && DocListCast(options.data).forEach(opt => StrCast(opt.title).startsWith(newText) && (newText = StrCast(opt.title)));
- self._fieldSpan.innerHTML = self._dashDoc![self._fieldKey] = newText;
- if (newText.startsWith(":=") && self._dashDoc && e.data === null && !e.inputType.includes("delete")) {
- Doc.Layout(tbox.props.Document)[self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(2));
- }
- });
+ e.ctrlKey && Doc.addFieldEnumerations(self._textBoxDoc, node.attrs.fieldKey, [{ title: self._fieldSpan.innerText }]);
+ updateText(true);
}
};
this._labelSpan = document.createElement("span");
+ this._labelSpan.style.backgroundColor = "rgba(155, 155, 155, 0.44)";
this._labelSpan.style.position = "relative";
- this._labelSpan.style.display = "inline";
- this._labelSpan.style.fontWeight = "bold";
- this._labelSpan.style.fontSize = "larger";
+ this._labelSpan.style.display = "inline-block";
+ this._labelSpan.style.fontSize = "small";
+ this._labelSpan.title = "click to see related tags";
+ this._labelSpan.onpointerdown = function (e: any) {
+ e.stopPropagation();
+ let container = tbox.props.ContainingCollectionView;
+ while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
+ container = container.props.ContainingCollectionView;
+ }
+ if (container) {
+ const alias = Doc.MakeAlias(container.props.Document);
+ alias.viewType = CollectionViewType.Time;
+ let list = Cast(alias.schemaColumns, listSpec(SchemaHeaderField));
+ if (!list) {
+ alias.schemaColumns = list = new List<SchemaHeaderField>();
+ }
+ list.map(c => c.heading).indexOf(self._fieldKey) === -1 && list.push(new SchemaHeaderField(self._fieldKey, "#f1efeb"));
+ list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb"));
+ alias._pivotField = self._fieldKey;
+ tbox.props.addDocTab(alias, "onRight");
+ }
+ }
this._labelSpan.innerHTML = `${node.attrs.fieldKey}: `;
if (node.attrs.docid) {
DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && runInAction(() => setDashDoc(dashDoc)));
@@ -946,11 +987,13 @@ export class DashFieldView {
this._reactionDisposer = reaction(() => { // this reaction will update the displayed text whenever the document's fieldKey's value changes
const dashVal = this._dashDoc?.[node.attrs.fieldKey];
return StrCast(dashVal).startsWith(":=") || !dashVal ? Doc.Layout(tbox.props.Document)[this._fieldKey] : dashVal;
- }, fval => this._fieldSpan.innerHTML = Field.toString(fval as Field) || "(null)", { fireImmediately: true });
+ }, fval => this._fieldSpan.innerHTML = Field.toString(fval as Field) || "", { fireImmediately: true });
this._fieldWrapper.appendChild(this._labelSpan);
this._fieldWrapper.appendChild(this._fieldSpan);
+ this._fieldWrapper.appendChild(this._enumerables);
(this as any).dom = this._fieldWrapper;
+ updateText(false);
}
destroy() {
this._reactionDisposer?.();
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 8ff54d052..64874b994 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -118,4 +118,27 @@ export namespace SearchUtil {
aliasContexts.forEach(result => contexts.aliasContexts.push(...result.ids));
return contexts;
}
-} \ No newline at end of file
+
+ export async function GetAllDocs() {
+ const query = "*";
+ let response = await rp.get(Utils.prepend('/search'), {
+ qs:
+ { start: 0, rows: 10000, q: query },
+
+ });
+ let result: IdSearchResult = JSON.parse(response);
+ const { ids, numFound, highlighting } = result;
+ //console.log(ids.length);
+ const docMap = await DocServer.GetRefFields(ids);
+ const docs: Doc[] = [];
+ for (const id of ids) {
+ const field = docMap[id];
+ if (field instanceof Doc) {
+ docs.push(field);
+ }
+ }
+ return docs;
+ // const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc);
+ // return docs as Doc[];
+ }
+}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 4612f10f4..0e281e77e 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -54,6 +54,7 @@ export namespace SelectionManager {
export function SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
manager.SelectDoc(docView, ctrlPressed);
}
+
export function IsSelected(doc: DocumentView, outsideReaction?: boolean): boolean {
return outsideReaction ?
diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss
new file mode 100644
index 000000000..e2149e9c1
--- /dev/null
+++ b/src/client/util/TooltipTextMenu.scss
@@ -0,0 +1,372 @@
+@import "../views/globalCssVariables";
+.ProseMirror-menu-dropdown-wrap {
+ display: inline-block;
+ position: relative;
+}
+
+.ProseMirror-menu-dropdown {
+ vertical-align: 1px;
+ cursor: pointer;
+ position: relative;
+ padding: 0 15px 0 4px;
+ background: white;
+ border-radius: 2px;
+ text-align: left;
+ font-size: 12px;
+ white-space: nowrap;
+ margin-right: 4px;
+
+ &:after {
+ content: "";
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid currentColor;
+ opacity: .6;
+ position: absolute;
+ right: 4px;
+ top: calc(50% - 2px);
+ }
+}
+
+.ProseMirror-menu-submenu-wrap {
+ position: relative;
+ margin-right: -4px;
+}
+
+.ProseMirror-menu-dropdown-menu,
+.ProseMirror-menu-submenu {
+ font-size: 12px;
+ background: white;
+ border: 1px solid rgb(223, 223, 223);
+ min-width: 40px;
+ z-index: 50000;
+ position: absolute;
+ box-sizing: content-box;
+
+ .ProseMirror-menu-dropdown-item {
+ cursor: pointer;
+ z-index: 100000;
+ text-align: left;
+ padding: 3px;
+
+ &:hover {
+ background-color: $light-color-secondary;
+ }
+ }
+}
+
+
+.ProseMirror-menu-submenu-label:after {
+ content: "";
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+ border-left: 4px solid currentColor;
+ opacity: .6;
+ position: absolute;
+ right: 4px;
+ top: calc(50% - 4px);
+}
+
+ .ProseMirror-icon {
+ display: inline-block;
+ // line-height: .8;
+ // vertical-align: -2px; /* Compensate for padding */
+ // padding: 2px 8px;
+ cursor: pointer;
+
+ &.ProseMirror-menu-disabled {
+ cursor: default;
+ }
+
+ svg {
+ fill:white;
+ height: 1em;
+ }
+
+ span {
+ vertical-align: text-top;
+ }
+ }
+
+.wrapper {
+ position: absolute;
+ pointer-events: all;
+ display: flex;
+ align-items: center;
+ transform: translateY(-85px);
+
+ height: 35px;
+ background: #323232;
+ border-radius: 6px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+
+}
+
+.tooltipMenu, .basic-tools {
+ z-index: 20000;
+ pointer-events: all;
+ padding: 3px;
+ padding-bottom: 5px;
+ display: flex;
+ align-items: center;
+
+ .ProseMirror-example-setup-style hr {
+ padding: 2px 10px;
+ border: none;
+ margin: 1em 0;
+ }
+
+ .ProseMirror-example-setup-style hr:after {
+ content: "";
+ display: block;
+ height: 1px;
+ background-color: silver;
+ line-height: 2px;
+ }
+}
+
+.menuicon {
+ width: 25px;
+ height: 25px;
+ cursor: pointer;
+ text-align: center;
+ line-height: 25px;
+ margin: 0 2px;
+ border-radius: 3px;
+
+ &:hover {
+ background-color: black;
+
+ #link-drag {
+ background-color: black;
+ }
+ }
+
+ &> * {
+ margin-top: 50%;
+ margin-left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ svg {
+ fill: white;
+ width: 18px;
+ height: 18px;
+ }
+}
+
+.menuicon-active {
+ width: 25px;
+ height: 25px;
+ cursor: pointer;
+ text-align: center;
+ line-height: 25px;
+ margin: 0 2px;
+ border-radius: 3px;
+
+ &:hover {
+ background-color: black;
+ }
+
+ &> * {
+ margin-top: 50%;
+ margin-left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ svg {
+ fill: greenyellow;
+ width: 18px;
+ height: 18px;
+ }
+}
+
+#colorPicker {
+ position: relative;
+
+ svg {
+ width: 18px;
+ height: 18px;
+ // margin-top: 11px;
+ }
+
+ .buttonColor {
+ position: absolute;
+ top: 24px;
+ left: 1px;
+ width: 24px;
+ height: 4px;
+ margin-top: 0;
+ }
+}
+
+#link-drag {
+ background-color: #323232;
+}
+
+.underline svg {
+ margin-top: 13px;
+}
+
+ .font-size-indicator {
+ font-size: 12px;
+ padding-right: 0px;
+ }
+ .summarize{
+ color: white;
+ height: 20px;
+ text-align: center;
+ }
+
+
+.brush{
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ stroke-width: 0;
+ stroke: currentColor;
+ fill: currentColor;
+ margin-right: 15px;
+}
+
+.brush-active{
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ stroke-width: 3;
+ fill: greenyellow;
+ margin-right: 15px;
+}
+
+.dragger-wrapper {
+ color: #eee;
+ height: 22px;
+ padding: 0 5px;
+ box-sizing: content-box;
+ cursor: grab;
+
+ .dragger {
+ width: 18px;
+ height: 100%;
+ display: flex;
+ justify-content: space-evenly;
+ }
+
+ .dragger-line {
+ width: 2px;
+ height: 100%;
+ background-color: black;
+ }
+}
+
+.button-dropdown-wrapper {
+ display: flex;
+ align-content: center;
+
+ &:hover {
+ background-color: black;
+ }
+}
+
+.buttonSettings-dropdown {
+
+ &.ProseMirror-menu-dropdown {
+ width: 10px;
+ height: 25px;
+ margin: 0;
+ padding: 0 2px;
+ background-color: #323232;
+ text-align: center;
+
+ &:after {
+ border-top: 4px solid white;
+ right: 2px;
+ }
+
+ &:hover {
+ background-color: black;
+ }
+ }
+
+ &.ProseMirror-menu-dropdown-menu {
+ min-width: 150px;
+ left: -27px;
+ top: 31px;
+ background-color: #323232;
+ border: 1px solid #4d4d4d;
+ color: $light-color-secondary;
+ // border: none;
+ // border: 1px solid $light-color-secondary;
+ border-radius: 0 6px 6px 6px;
+ padding: 3px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+
+ .ProseMirror-menu-dropdown-item{
+ cursor: default;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ background-color: #323232;
+ }
+
+ .button-setting, .button-setting-disabled {
+ padding: 2px;
+ border-radius: 2px;
+ }
+
+ .button-setting:hover {
+ cursor: pointer;
+ background-color: black;
+ }
+
+ .separated-button {
+ border-top: 1px solid $light-color-secondary;
+ padding-top: 6px;
+ }
+
+ input {
+ color: black;
+ border: none;
+ border-radius: 1px;
+ padding: 3px;
+ }
+
+ button {
+ padding: 6px;
+ background-color: #323232;
+ border: 1px solid black;
+ border-radius: 1px;
+
+ &:hover {
+ background-color: black;
+ }
+ }
+ }
+
+
+ }
+}
+
+.colorPicker-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ margin-top: 3px;
+ margin-left: -3px;
+ width: calc(100% + 6px);
+}
+
+button.colorPicker {
+ width: 20px;
+ height: 20px;
+ border-radius: 15px !important;
+ margin: 3px;
+ border: none !important;
+
+ &.active {
+ border: 2px solid white !important;
+ }
+}
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index a3d313224..52544d3c9 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -122,13 +122,11 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
if (this.view0 && linkDoc) {
const proto = Doc.GetProto(linkDoc);
proto.anchor1Context = this.view0.props.ContainingCollectionDoc;
+ Doc.GetProto(linkDoc).linkRelationship = "hyperlink";
const anchor2Title = linkDoc.anchor2 instanceof Doc ? StrCast(linkDoc.anchor2.title) : "-untitled-";
const anchor2Id = linkDoc.anchor2 instanceof Doc ? linkDoc.anchor2[Id] : "";
const text = RichTextMenu.Instance.MakeLinkToSelection(linkDoc[Id], anchor2Title, e.ctrlKey ? "onRight" : "inTab", anchor2Id);
- if (linkDoc.anchor2 instanceof Doc && !proto.title) {
- proto.title = Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('this.anchor1.title +" " + (this.linkRelationship||"to") +" " + this.anchor2.title');
- }
}
linkDrag?.end();
},
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 1992c5efa..8d6002f8f 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -27,6 +27,17 @@ $linkGap : 3px;
opacity: 1;
}
+ .documentDecorations-selector {
+ pointer-events: auto;
+ height: 15px;
+ width: 15px;
+ left: -20px;
+ top: 20px;
+ display: inline-block;
+ position: absolute;
+ opacity: 0.5;
+ }
+
.documentDecorations-radius {
pointer-events: auto;
background: black;
@@ -177,7 +188,7 @@ $linkGap : 3px;
height: auto;
display: flex;
flex-direction: row;
- z-index: 5;
+ z-index: 998;
position: absolute;
}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4922411e8..c98be0d4a 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -8,7 +8,7 @@ import { PositionDocument } from '../../new_fields/documentSchemas';
import { ScriptField } from '../../new_fields/ScriptField';
import { Cast, StrCast, NumCast } from "../../new_fields/Types";
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { Utils, setupMoveUpEvents } from "../../Utils";
+import { Utils, setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils";
import { DocUtils } from "../documents/Documents";
import { DocumentType } from '../documents/DocumentTypes';
import { DragManager } from "../util/DragManager";
@@ -245,6 +245,16 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
@action
+ onSelectorUp = (e: React.PointerEvent): void => {
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e) => {
+ const selDoc = SelectionManager.SelectedDocuments()?.[0];
+ if (selDoc) {
+ selDoc.props.ContainingCollectionView?.props.select(false);
+ }
+ }));
+ }
+
+ @action
onRadiusDown = (e: React.PointerEvent): void => {
setupMoveUpEvents(this, e, this.onRadiusMove, (e) => this._resizeUndo?.end(), (e) => { });
if (e.button === 0) {
@@ -496,6 +506,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer"
onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : <div id="documentDecorations-levelSelector" className="documentDecorations-selector" title="tap to select containing document"
+ onPointerDown={this.onSelectorUp} onContextMenu={(e) => e.preventDefault()}>
+ <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
+ </div>}
<div id="documentDecorations-borderRadius" className="documentDecorations-radius"
onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div>
diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss
index d980b0a91..107077792 100644
--- a/src/client/views/GestureOverlay.scss
+++ b/src/client/views/GestureOverlay.scss
@@ -5,6 +5,21 @@
top: 0;
left: 0;
touch-action: none;
+
+ .pointerBubbles {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ pointer-events: none;
+
+ .bubble {
+ position: absolute;
+ width: 15px;
+ height: 15px;
+ border-radius: 100%;
+ border: .5px solid grey;
+ }
+ }
}
.clipboardDoc-cont {
@@ -13,6 +28,35 @@
height: 300px;
}
+.inkToTextDoc-cont {
+ position: absolute;
+ width: 300px;
+ overflow: hidden;
+ pointer-events: none;
+
+ .inkToTextDoc-scroller {
+ overflow: visible;
+ position: absolute;
+ width: 100%;
+
+ .menuItem-cont {
+ width: 100%;
+ height: 25px;
+ padding: 2.5px;
+ border-bottom: .5px solid black;
+ }
+ }
+
+ .shadow {
+ width: 100%;
+ height: calc(100% - 25px);
+ position: absolute;
+ top: 25px;
+ background-color: black;
+ opacity: 0.2;
+ }
+}
+
.filter-cont {
position: absolute;
background-color: transparent;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index a8cf8c197..1eff58948 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -2,49 +2,72 @@ import React = require("react");
import { Touchable } from "./Touchable";
import { observer } from "mobx-react";
import "./GestureOverlay.scss";
-import { computed, observable, action, runInAction, IReactionDisposer, reaction } from "mobx";
+import { computed, observable, action, runInAction, IReactionDisposer, reaction, flow, trace } from "mobx";
import { GestureUtils } from "../../pen-gestures/GestureUtils";
import { InteractionUtils } from "../util/InteractionUtils";
import { InkingControl } from "./InkingControl";
-import { InkTool } from "../../new_fields/InkField";
+import { InkTool, InkData } from "../../new_fields/InkField";
import { Doc } from "../../new_fields/Doc";
import { LinkManager } from "../util/LinkManager";
-import { DocUtils } from "../documents/Documents";
+import { DocUtils, Docs } from "../documents/Documents";
import { undoBatch } from "../util/UndoManager";
import { Scripting } from "../util/Scripting";
import { FieldValue, Cast, NumCast, BoolCast } from "../../new_fields/Types";
import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
-import Palette from "./Palette";
+import HorizontalPalette from "./Palette";
import { Utils, emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, numberRange } from "../../Utils";
import { DocumentView } from "./nodes/DocumentView";
import { Transform } from "../util/Transform";
import { DocumentContentsView } from "./nodes/DocumentContentsView";
+import { CognitiveServices } from "../cognitive_services/CognitiveServices";
+import { DocServer } from "../DocServer";
+import htmlToImage from "html-to-image";
+import { ScriptField } from "../../new_fields/ScriptField";
+import { listSpec } from "../../new_fields/Schema";
+import { List } from "../../new_fields/List";
+import { CollectionViewType } from "./collections/CollectionView";
+import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
+import MobileInterface from "../../mobile/MobileInterface";
+import { MobileInkOverlayContent } from "../../server/Message";
+import MobileInkOverlay from "../../mobile/MobileInkOverlay";
+import { RadialMenu } from "./nodes/RadialMenu";
+import { SelectionManager } from "../util/SelectionManager";
+
@observer
export default class GestureOverlay extends Touchable {
static Instance: GestureOverlay;
- @observable public Color: string = "rgb(244, 67, 54)";
- @observable public Width: number = 5;
+ @observable public Color: string = "rgb(0, 0, 0)";
+ @observable public Width: number = 2;
@observable public SavedColor?: string;
@observable public SavedWidth?: number;
@observable public Tool: ToolglassTools = ToolglassTools.None;
@observable private _thumbX?: number;
@observable private _thumbY?: number;
+ @observable private _selectedIndex: number = -1;
+ @observable private _menuX: number = -300;
+ @observable private _menuY: number = -300;
@observable private _pointerY?: number;
@observable private _points: { X: number, Y: number }[] = [];
+ @observable private _strokes: InkData[] = [];
@observable private _palette?: JSX.Element;
@observable private _clipboardDoc?: JSX.Element;
+ @observable private _possibilities: JSX.Element[] = [];
- @computed private get height(): number { return Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 300, 300); }
+ @computed private get height(): number { return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 100, 100); }
@computed private get showBounds() { return this.Tool !== ToolglassTools.None; }
+ @observable private showMobileInkOverlay: boolean = false;
+
private _d1: Doc | undefined;
+ private _inkToTextDoc: Doc | undefined;
private _thumbDoc: Doc | undefined;
private thumbIdentifier?: number;
private pointerIdentifier?: number;
private _hands: Map<number, React.Touch[]> = new Map<number, React.Touch[]>();
+ private _holdTimer: NodeJS.Timeout | undefined;
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
@@ -54,6 +77,11 @@ export default class GestureOverlay extends Touchable {
GestureOverlay.Instance = this;
}
+ componentDidMount = () => {
+ this._thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc));
+ this._inkToTextDoc = FieldValue(Cast(this._thumbDoc?.inkToTextDoc, Doc));
+ }
+
getNewTouches(e: React.TouchEvent | TouchEvent) {
const ntt: (React.Touch | Touch)[] = Array.from(e.targetTouches);
const nct: (React.Touch | Touch)[] = Array.from(e.changedTouches);
@@ -84,6 +112,15 @@ export default class GestureOverlay extends Touchable {
}
onReactTouchStart = (te: React.TouchEvent) => {
+ document.removeEventListener("touchmove", this.onReactHoldTouchMove);
+ document.removeEventListener("touchend", this.onReactHoldTouchEnd);
+ if (RadialMenu.Instance._display === true) {
+ te.preventDefault();
+ te.stopPropagation();
+ RadialMenu.Instance.closeMenu();
+ return;
+ }
+
const actualPts: React.Touch[] = [];
for (let i = 0; i < te.touches.length; i++) {
const pt: any = te.touches.item(i);
@@ -107,8 +144,6 @@ export default class GestureOverlay extends Touchable {
ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
const nts = this.getNewTouches(te);
- console.log(nts.nt.length);
-
if (nts.nt.length < 5) {
const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY);
target?.dispatchEvent(
@@ -125,6 +160,41 @@ export default class GestureOverlay extends Touchable {
}
)
);
+ if (nts.nt.length === 1) {
+ console.log("started");
+ this._holdTimer = setTimeout(() => {
+ console.log("hold");
+ const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY);
+ let pt: any = te.touches[te.touches.length - 1];
+ if (nts.nt.length === 1 && pt.radiusX > 1 && pt.radiusY > 1) {
+ target?.dispatchEvent(
+ new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchHoldStart",
+ {
+ bubbles: true,
+ detail: {
+ fingers: this.prevPoints.size,
+ targetTouches: nts.ntt,
+ touches: nts.nt,
+ changedTouches: nts.nct,
+ touchEvent: te
+ }
+ }
+ )
+ );
+ this._holdTimer = undefined;
+ document.removeEventListener("touchmove", this.onReactTouchMove);
+ document.removeEventListener("touchend", this.onReactTouchEnd);
+ document.removeEventListener("touchmove", this.onReactHoldTouchMove);
+ document.removeEventListener("touchend", this.onReactHoldTouchEnd);
+ document.addEventListener("touchmove", this.onReactHoldTouchMove);
+ document.addEventListener("touchend", this.onReactHoldTouchEnd);
+ }
+
+ }, (500));
+ }
+ else {
+ clearTimeout(this._holdTimer);
+ }
document.removeEventListener("touchmove", this.onReactTouchMove);
document.removeEventListener("touchend", this.onReactTouchEnd);
document.addEventListener("touchmove", this.onReactTouchMove);
@@ -137,8 +207,72 @@ export default class GestureOverlay extends Touchable {
}
}
+ onReactHoldTouchMove = (e: TouchEvent) => {
+ document.removeEventListener("touchmove", this.onReactTouchMove);
+ document.removeEventListener("touchend", this.onReactTouchEnd);
+ document.removeEventListener("touchmove", this.onReactHoldTouchMove);
+ document.removeEventListener("touchend", this.onReactHoldTouchEnd);
+ document.addEventListener("touchmove", this.onReactHoldTouchMove);
+ document.addEventListener("touchend", this.onReactHoldTouchEnd);
+ const nts: any = this.getNewTouches(e);
+ if (this.prevPoints.size === 1 && this._holdTimer) {
+ clearTimeout(this._holdTimer);
+ }
+ document.dispatchEvent(
+ new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchHoldMove",
+ {
+ bubbles: true,
+ detail: {
+ fingers: this.prevPoints.size,
+ targetTouches: nts.ntt,
+ touches: nts.nt,
+ changedTouches: nts.nct,
+ touchEvent: e
+ }
+ })
+ );
+ }
+
+ onReactHoldTouchEnd = (e: TouchEvent) => {
+ const nts: any = this.getNewTouches(e);
+ if (this.prevPoints.size === 1 && this._holdTimer) {
+ clearTimeout(this._holdTimer);
+ this._holdTimer = undefined;
+ }
+ document.dispatchEvent(
+ new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchHoldEnd",
+ {
+ bubbles: true,
+ detail: {
+ fingers: this.prevPoints.size,
+ targetTouches: nts.ntt,
+ touches: nts.nt,
+ changedTouches: nts.nct,
+ touchEvent: e
+ }
+ })
+ );
+ for (let i = 0; i < e.changedTouches.length; i++) {
+ const pt = e.changedTouches.item(i);
+ if (pt) {
+ if (this.prevPoints.has(pt.identifier)) {
+ this.prevPoints.delete(pt.identifier);
+ }
+ }
+ }
+
+ document.removeEventListener("touchmove", this.onReactHoldTouchMove);
+ document.removeEventListener("touchend", this.onReactHoldTouchEnd);
+
+ e.stopPropagation();
+ }
+
+
onReactTouchMove = (e: TouchEvent) => {
const nts: any = this.getNewTouches(e);
+ clearTimeout(this._holdTimer);
+ this._holdTimer = undefined;
+
document.dispatchEvent(
new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchMove",
{
@@ -156,6 +290,9 @@ export default class GestureOverlay extends Touchable {
onReactTouchEnd = (e: TouchEvent) => {
const nts: any = this.getNewTouches(e);
+ clearTimeout(this._holdTimer);
+ this._holdTimer = undefined;
+
document.dispatchEvent(
new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchEnd",
{
@@ -186,6 +323,7 @@ export default class GestureOverlay extends Touchable {
}
handleHandDown = async (e: React.TouchEvent) => {
+ clearTimeout(this._holdTimer!);
const fingers = new Array<React.Touch>();
for (let i = 0; i < e.touches.length; i++) {
const pt: any = e.touches.item(i);
@@ -216,13 +354,16 @@ export default class GestureOverlay extends Touchable {
console.log("not hand");
}
this.pointerIdentifier = pointer?.identifier;
- runInAction(() => this._pointerY = pointer?.clientY);
- if (thumb.identifier === this.thumbIdentifier) {
- this._thumbX = thumb.clientX;
- this._thumbY = thumb.clientY;
- this._hands.set(thumb.identifier, fingers);
- return;
- }
+ runInAction(() => {
+ this._pointerY = pointer?.clientY;
+ if (thumb.identifier === this.thumbIdentifier) {
+ this._thumbX = thumb.clientX;
+ this._thumbY = thumb.clientY;
+ this._hands.set(thumb.identifier, fingers);
+ return;
+ }
+ });
+
this.thumbIdentifier = thumb?.identifier;
this._hands.set(thumb.identifier, fingers);
const others = fingers.filter(f => f !== thumb);
@@ -232,10 +373,14 @@ export default class GestureOverlay extends Touchable {
const thumbDoc = await Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc);
if (thumbDoc) {
runInAction(() => {
+ RadialMenu.Instance._display = false;
+ this._inkToTextDoc = FieldValue(Cast(thumbDoc.inkToTextDoc, Doc));
this._thumbDoc = thumbDoc;
this._thumbX = thumb.clientX;
this._thumbY = thumb.clientY;
- this._palette = <Palette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
+ this._menuX = thumb.clientX + 50;
+ this._menuY = thumb.clientY;
+ this._palette = <HorizontalPalette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
});
}
@@ -273,11 +418,33 @@ export default class GestureOverlay extends Touchable {
for (let i = 0; i < e.changedTouches.length; i++) {
const pt = e.changedTouches.item(i);
- if (pt && pt.identifier === this.thumbIdentifier && this._thumbX && this._thumbDoc) {
- if (Math.abs(pt.clientX - this._thumbX) > 20) {
- this._thumbDoc.selectedIndex = Math.max(0, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX));
- this._thumbX = pt.clientX;
+ if (pt && pt.identifier === this.thumbIdentifier && this._thumbY) {
+ if (this._thumbX && this._thumbY) {
+ const yOverX = Math.abs(pt.clientX - this._thumbX) < Math.abs(pt.clientY - this._thumbY);
+ if ((yOverX && this._inkToTextDoc) || this._selectedIndex > -1) {
+ if (Math.abs(pt.clientY - this._thumbY) > (10 * window.devicePixelRatio)) {
+ this._selectedIndex = Math.min(Math.max(-1, (-Math.ceil((pt.clientY - this._thumbY) / (10 * window.devicePixelRatio)) - 1)), this._possibilities.length - 1);
+ }
+ }
+ else if (this._thumbDoc) {
+ if (Math.abs(pt.clientX - this._thumbX) > (15 * window.devicePixelRatio)) {
+ this._thumbDoc.selectedIndex = Math.max(-1, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX));
+ this._thumbX = pt.clientX;
+ }
+ }
}
+
+ // if (this._thumbX && this._thumbDoc) {
+ // if (Math.abs(pt.clientX - this._thumbX) > 30) {
+ // this._thumbDoc.selectedIndex = Math.max(0, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX));
+ // this._thumbX = pt.clientX;
+ // }
+ // }
+ // if (this._thumbY && this._inkToTextDoc) {
+ // if (Math.abs(pt.clientY - this._thumbY) > 20) {
+ // this._selectedIndex = Math.min(Math.max(0, -Math.ceil((pt.clientY - this._thumbY) / 20)), this._possibilities.length - 1);
+ // }
+ // }
}
if (pt && pt.identifier === this.pointerIdentifier) {
this._pointerY = pt.clientY;
@@ -293,6 +460,24 @@ export default class GestureOverlay extends Touchable {
this._palette = undefined;
this.thumbIdentifier = undefined;
this._thumbDoc = undefined;
+
+ let scriptWorked = false;
+ if (NumCast(this._inkToTextDoc?.selectedIndex) > -1) {
+ const selectedButton = this._possibilities[this._selectedIndex];
+ if (selectedButton) {
+ selectedButton.props.onClick();
+ scriptWorked = true;
+ }
+ }
+
+ if (!scriptWorked) {
+ this._strokes.forEach(s => {
+ this.dispatchGesture(GestureUtils.Gestures.Stroke, s);
+ });
+ }
+ this._strokes = [];
+ this._points = [];
+ this._possibilities = [];
document.removeEventListener("touchend", this.handleHandUp);
}
}
@@ -317,6 +502,22 @@ export default class GestureOverlay extends Touchable {
this._points.push({ X: e.clientX, Y: e.clientY });
e.stopPropagation();
e.preventDefault();
+
+
+ if (this._points.length > 1) {
+ const B = this.svgBounds;
+ const initialPoint = this._points[0.];
+ const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height;
+ const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER);
+ if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) {
+ switch (this.Tool) {
+ case ToolglassTools.RadialMenu:
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ //this.handle1PointerHoldStart(e);
+ }
+ }
+ }
}
}
@@ -333,8 +534,10 @@ export default class GestureOverlay extends Touchable {
this._d1 = doc;
}
else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) {
- DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link");
- actionPerformed = true;
+ if (this._d1.type !== "ink" && doc.type !== "ink") {
+ DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link");
+ actionPerformed = true;
+ }
}
};
const ge = new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
@@ -358,12 +561,51 @@ export default class GestureOverlay extends Touchable {
const B = this.svgBounds;
const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
- const xInGlass = points[0].X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && points[0].X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height;
- const yInGlass = points[0].Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && points[0].Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER);
+ if (MobileInterface.Instance && MobileInterface.Instance.drawingInk) {
+ const { selectedColor, selectedWidth } = InkingControl.Instance;
+ DocServer.Mobile.dispatchGesturePoints({
+ points: this._points,
+ bounds: B,
+ color: selectedColor,
+ width: selectedWidth
+ });
+ }
+
+ const initialPoint = this._points[0.];
+ const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + (this.height);
+ const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - (this.height) && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER);
if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) {
switch (this.Tool) {
case ToolglassTools.InkToText:
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ this._strokes.push(new Array(...this._points));
+ this._points = [];
+ CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then((results) => {
+ console.log(results);
+ const wordResults = results.filter((r: any) => r.category === "line");
+ const possibilities: string[] = [];
+ for (const wR of wordResults) {
+ console.log(wR);
+ if (wR?.recognizedText) {
+ possibilities.push(wR?.recognizedText)
+ }
+ possibilities.push(...wR?.alternates?.map((a: any) => a.recognizedString));
+ }
+ console.log(possibilities);
+ const r = Math.max(this.svgBounds.right, ...this._strokes.map(s => this.getBounds(s).right));
+ const l = Math.min(this.svgBounds.left, ...this._strokes.map(s => this.getBounds(s).left));
+ const t = Math.min(this.svgBounds.top, ...this._strokes.map(s => this.getBounds(s).top));
+ runInAction(() => {
+ this._possibilities = possibilities.map(p =>
+ <TouchScrollableMenuItem text={p} onClick={() => GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Text, [{ X: l, Y: t }], p)} />);
+ });
+ });
+ break;
+ case ToolglassTools.IgnoreGesture:
+ this.dispatchGesture(GestureUtils.Gestures.Stroke);
+ this._points = [];
break;
}
}
@@ -373,16 +615,15 @@ export default class GestureOverlay extends Touchable {
if (result && result.Score > 0.7) {
switch (result.Name) {
case GestureUtils.Gestures.Box:
- const target = document.elementFromPoint(this._points[0].X, this._points[0].Y);
- target?.dispatchEvent(new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
- {
- bubbles: true,
- detail: {
- points: this._points,
- gesture: GestureUtils.Gestures.Box,
- bounds: B
- }
- }));
+ this.dispatchGesture(GestureUtils.Gestures.Box);
+ actionPerformed = true;
+ break;
+ case GestureUtils.Gestures.StartBracket:
+ this.dispatchGesture(GestureUtils.Gestures.StartBracket);
+ actionPerformed = true;
+ break;
+ case GestureUtils.Gestures.EndBracket:
+ this.dispatchGesture(GestureUtils.Gestures.EndBracket);
actionPerformed = true;
break;
case GestureUtils.Gestures.Line:
@@ -398,19 +639,7 @@ export default class GestureOverlay extends Touchable {
}
if (!actionPerformed) {
- const target = document.elementFromPoint(this._points[0].X, this._points[0].Y);
- target?.dispatchEvent(
- new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
- {
- bubbles: true,
- detail: {
- points: this._points,
- gesture: GestureUtils.Gestures.Stroke,
- bounds: B
- }
- }
- )
- );
+ this.dispatchGesture(GestureUtils.Gestures.Stroke);
this._points = [];
}
}
@@ -419,9 +648,26 @@ export default class GestureOverlay extends Touchable {
document.removeEventListener("pointerup", this.onPointerUp);
}
- @computed get svgBounds() {
- const xs = this._points.map(p => p.X);
- const ys = this._points.map(p => p.Y);
+ dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, data?: any) => {
+ const target = document.elementFromPoint((stroke ?? this._points)[0].X, (stroke ?? this._points)[0].Y);
+ target?.dispatchEvent(
+ new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
+ {
+ bubbles: true,
+ detail: {
+ points: stroke ?? this._points,
+ gesture: gesture,
+ bounds: this.getBounds(stroke ?? this._points),
+ text: data
+ }
+ }
+ )
+ );
+ }
+
+ getBounds = (stroke: InkData) => {
+ const xs = stroke.map(p => p.X);
+ const ys = stroke.map(p => p.Y);
const right = Math.max(...xs);
const left = Math.min(...xs);
const bottom = Math.max(...ys);
@@ -429,25 +675,24 @@ export default class GestureOverlay extends Touchable {
return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top };
}
- @computed get currentStroke() {
- if (this._points.length <= 1) {
- return (null);
- }
-
- const B = this.svgBounds;
-
- return (
- <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}>
- {InteractionUtils.CreatePolyline(this._points, B.left, B.top, this.Color, this.Width)}
- </svg>
- );
+ @computed get svgBounds() {
+ return this.getBounds(this._points);
}
@computed get elements() {
+ const B = this.svgBounds;
return [
this.props.children,
this._palette,
- this.currentStroke
+ [this._strokes.map(l => {
+ const b = this.getBounds(l);
+ return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}>
+ {InteractionUtils.CreatePolyline(l, b.left, b.top, GestureOverlay.Instance.Color, GestureOverlay.Instance.Width)}
+ </svg>;
+ }),
+ this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}>
+ {InteractionUtils.CreatePolyline(this._points, B.left, B.top, GestureOverlay.Instance.Color, GestureOverlay.Instance.Width)}
+ </svg>]
];
}
@@ -485,10 +730,18 @@ export default class GestureOverlay extends Touchable {
this._clipboardDoc = undefined;
}
+ @action
+ enableMobileInkOverlay = (content: MobileInkOverlayContent) => {
+ this.showMobileInkOverlay = content.enableOverlay;
+ }
+
render() {
+ trace();
return (
<div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
+ {this.showMobileInkOverlay ? <MobileInkOverlay /> : <></>}
{this.elements}
+
<div className="clipboardDoc-cont" style={{
transform: `translate(${this._thumbX}px, ${(this._thumbY ?? 0) - this.height}px)`,
height: this.height,
@@ -507,12 +760,20 @@ export default class GestureOverlay extends Touchable {
display: this.showBounds ? "unset" : "none",
}}>
</div>
- </div >);
+ <TouchScrollableMenu options={this._possibilities} bounds={this.svgBounds} selectedIndex={this._selectedIndex} x={this._menuX} y={this._menuY} />
+ {/* <div className="pointerBubbles">
+ {this._pointers.map(p => <div className="bubble" style={{ translate: `transform(${p.clientX}px, ${p.clientY}px)` }}></div>)}
+ </div> */}
+ </div>);
}
}
+// export class
+
export enum ToolglassTools {
InkToText = "inktotext",
+ IgnoreGesture = "ignoregesture",
+ RadialMenu = "radialmenu",
None = "none",
}
@@ -530,7 +791,10 @@ Scripting.addGlobal(function setPen(width: any, color: any) {
});
Scripting.addGlobal(function resetPen() {
runInAction(() => {
- GestureOverlay.Instance.Color = GestureOverlay.Instance.SavedColor ?? "rgb(244, 67, 54)";
- GestureOverlay.Instance.Width = GestureOverlay.Instance.SavedWidth ?? 5;
+ GestureOverlay.Instance.Color = GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)";
+ GestureOverlay.Instance.Width = GestureOverlay.Instance.SavedWidth ?? 2;
});
+});
+Scripting.addGlobal(function createText(text: any, x: any, y: any) {
+ GestureOverlay.Instance.dispatchGesture("text", [{ X: x, Y: y }], text);
}); \ No newline at end of file
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d0900251d..af7675119 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -6,6 +6,7 @@ import { DragManager } from "../util/DragManager";
import { action, runInAction } from "mobx";
import { Doc } from "../../new_fields/Doc";
import { DictationManager } from "../util/DictationManager";
+import { RecommendationsBox } from "./RecommendationsBox";
import SharingManager from "../util/SharingManager";
import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
import { Cast, PromiseValue } from "../../new_fields/Types";
@@ -79,6 +80,7 @@ export default class KeyManager {
}
SelectionManager.DeselectAll();
DictationManager.Controls.stop();
+ // RecommendationsBox.Instance.closeMenu();
SharingManager.Instance.close();
break;
case "delete":
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index f315ce12a..a791eed40 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -29,13 +29,13 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
@computed get PanelHeight() { return this.props.PanelHeight(); }
private analyzeStrokes = () => {
- const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
+ const data: InkData = Cast(this.Document.data, InkField) ?.inkData ?? [];
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.Document, ["inkAnalysis", "handwriting"], [data]);
}
render() {
TraceMobx();
- const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
+ const data: InkData = Cast(this.Document.data, InkField) ?.inkData ?? [];
const xs = data.map(p => p.X);
const ys = data.map(p => p.Y);
const left = Math.min(...xs);
diff --git a/src/client/views/KeyphraseQueryView.scss b/src/client/views/KeyphraseQueryView.scss
new file mode 100644
index 000000000..ac715e5e7
--- /dev/null
+++ b/src/client/views/KeyphraseQueryView.scss
@@ -0,0 +1,8 @@
+.fading {
+ animation: fanOut 1s
+}
+
+@keyframes fanOut {
+ from {opacity: 0;}
+ to {opacity: 1;}
+} \ No newline at end of file
diff --git a/src/client/views/KeyphraseQueryView.tsx b/src/client/views/KeyphraseQueryView.tsx
new file mode 100644
index 000000000..a9dafc4a4
--- /dev/null
+++ b/src/client/views/KeyphraseQueryView.tsx
@@ -0,0 +1,35 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import "./KeyphraseQueryView.scss";
+
+// tslint:disable-next-line: class-name
+export interface KP_Props {
+ keyphrases: string;
+}
+
+@observer
+export class KeyphraseQueryView extends React.Component<KP_Props>{
+ constructor(props: KP_Props) {
+ super(props);
+ console.log("FIRST KEY PHRASE: ", props.keyphrases[0]);
+ }
+
+ render() {
+ let kps = this.props.keyphrases.toString();
+ let keyterms = this.props.keyphrases.split(',');
+ return (
+ <div>
+ <h5>Select queries to send:</h5>
+ <form>
+ {keyterms.map((kp: string) => {
+ //return (<p>{"-" + kp}</p>);
+ return (<p><label>
+ <input name="query" type="radio" />
+ <span>{kp}</span>
+ </label></p>);
+ })}
+ </form>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index b21eb9c8f..6d705aa44 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -5,6 +5,7 @@ import * as ReactDOM from 'react-dom';
import * as React from 'react';
import { DocServer } from "../DocServer";
import { AssignAllExtensions } from "../../extensions/General/Extensions";
+process.env.HANDWRITING = "61088486d76c4b12ba578775a5f55422";
AssignAllExtensions();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ff35593fe..a81e0cc2c 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -32,9 +32,14 @@ import KeyManager from './GlobalKeyHandler';
import "./MainView.scss";
import { MainViewNotifs } from './MainViewNotifs';
import { DocumentView } from './nodes/DocumentView';
-import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
import { PreviewCursor } from './PreviewCursor';
+import { FilterBox } from './search/FilterBox';
+import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField';
+//import { DocumentManager } from '../util/DocumentManager';
+import { RecommendationsBox } from './RecommendationsBox';
+import { PresBox } from './nodes/PresBox';
+import { OverlayView } from './OverlayView';
import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu';
import GestureOverlay from './GestureOverlay';
import { Scripting } from '../util/Scripting';
@@ -52,6 +57,7 @@ export class MainView extends React.Component {
private _flyoutSizeOnDown = 0;
private _urlState: HistoryUtil.DocUrl;
private _docBtnRef = React.createRef<HTMLDivElement>();
+ private _mainViewRef = React.createRef<HTMLDivElement>();
@observable private _panelWidth: number = 0;
@observable private _panelHeight: number = 0;
@@ -540,8 +546,16 @@ export class MainView extends React.Component {
return (null);
}
+ get mainViewElement() {
+ return document.getElementById("mainView-container");
+ }
+
+ get mainViewRef() {
+ return this._mainViewRef;
+ }
+
render() {
- return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")}>
+ return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} ref={this._mainViewRef}>
<DictationOverlay />
<SharingManager />
<SettingsManager />
diff --git a/src/client/views/OCRUtils.ts b/src/client/views/OCRUtils.ts
new file mode 100644
index 000000000..282ec770e
--- /dev/null
+++ b/src/client/views/OCRUtils.ts
@@ -0,0 +1,7 @@
+// import tesseract from "node-tesseract-ocr";
+// const tesseract = require("node-tesseract");
+
+
+export namespace OCRUtils {
+
+}
diff --git a/src/client/views/Palette.scss b/src/client/views/Palette.scss
index 4513de2b0..0ec879288 100644
--- a/src/client/views/Palette.scss
+++ b/src/client/views/Palette.scss
@@ -1,13 +1,14 @@
.palette-container {
.palette-thumb {
touch-action: pan-x;
- overflow: scroll;
position: absolute;
- width: 90px;
height: 70px;
+ overflow: hidden;
.palette-thumbContent {
transition: transform .3s;
+ width: max-content;
+ overflow: hidden;
.collectionView {
overflow: visible;
@@ -17,5 +18,13 @@
}
}
}
+
+ .palette-cover {
+ width: 50px;
+ height: 50px;
+ position: absolute;
+ bottom: 0;
+ border: 1px solid black;
+ }
}
} \ No newline at end of file
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
index 10aac96a0..e04f814d1 100644
--- a/src/client/views/Palette.tsx
+++ b/src/client/views/Palette.tsx
@@ -72,6 +72,7 @@ export default class Palette extends React.Component<PaletteProps> {
zoomToScale={emptyFunction}
getScale={returnOne}>
</DocumentView>
+ <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div>
</div>
</div>
</div>
diff --git a/src/client/views/RecommendationsBox.scss b/src/client/views/RecommendationsBox.scss
new file mode 100644
index 000000000..dd8a105f6
--- /dev/null
+++ b/src/client/views/RecommendationsBox.scss
@@ -0,0 +1,68 @@
+@import "globalCssVariables";
+
+.rec-content *{
+ display: inline-block;
+ margin: auto;
+ width: 50;
+ height: 150px;
+ border: 1px dashed grey;
+ padding: 10px 10px;
+}
+
+.rec-content {
+ float: left;
+ width: inherit;
+ align-content: center;
+}
+
+.rec-scroll {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ position: absolute;
+ pointer-events: all;
+ // display: flex;
+ z-index: 10000;
+ box-shadow: gray 0.2vw 0.2vw 0.4vw;
+ // flex-direction: column;
+ background: whitesmoke;
+ padding-bottom: 10px;
+ padding-top: 20px;
+ // border-radius: 15px;
+ border: solid #BBBBBBBB 1px;
+ width: 100%;
+ text-align: center;
+ // max-height: 250px;
+ height: 100%;
+ text-transform: uppercase;
+ color: grey;
+ letter-spacing: 2px;
+}
+
+.content {
+ padding: 10px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+}
+
+.image-background {
+ pointer-events: none;
+ background-color: transparent;
+ width: 50%;
+ text-align: center;
+ margin-left: 5px;
+}
+
+img{
+ width: 100%;
+ height: 100%;
+}
+
+.score {
+ // margin-left: 15px;
+ width: 50%;
+ height: 100%;
+ text-align: center;
+ margin-left: 10px;
+}
diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx
new file mode 100644
index 000000000..262226bac
--- /dev/null
+++ b/src/client/views/RecommendationsBox.tsx
@@ -0,0 +1,199 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { observable, action, computed, runInAction } from "mobx";
+import Measure from "react-measure";
+import "./RecommendationsBox.scss";
+import { Doc, DocListCast, WidthSym, HeightSym } from "../../new_fields/Doc";
+import { DocumentIcon } from "./nodes/DocumentIcon";
+import { StrCast, NumCast } from "../../new_fields/Types";
+import { returnFalse, emptyFunction, returnEmptyString, returnOne } from "../../Utils";
+import { Transform } from "../util/Transform";
+import { ObjectField } from "../../new_fields/ObjectField";
+import { DocumentView } from "./nodes/DocumentView";
+import { DocumentType } from '../documents/DocumentTypes';
+import { ClientRecommender } from "../ClientRecommender";
+import { DocServer } from "../DocServer";
+import { Id } from "../../new_fields/FieldSymbols";
+import { FieldView, FieldViewProps } from "./nodes/FieldView";
+import { DocumentManager } from "../util/DocumentManager";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faBullseye, faLink } from "@fortawesome/free-solid-svg-icons";
+import { DocUtils } from "../documents/Documents";
+
+export interface RecProps {
+ documents: { preview: Doc, similarity: number }[];
+ node: Doc;
+}
+
+library.add(faBullseye, faLink);
+
+@observer
+export class RecommendationsBox extends React.Component<FieldViewProps> {
+
+ public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(RecommendationsBox, fieldKey); }
+
+ // @observable private _display: boolean = false;
+ @observable private _pageX: number = 0;
+ @observable private _pageY: number = 0;
+ @observable private _width: number = 0;
+ @observable private _height: number = 0;
+ @observable.shallow private _docViews: JSX.Element[] = [];
+ // @observable private _documents: { preview: Doc, score: number }[] = [];
+ private previewDocs: Doc[] = [];
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ }
+
+ @action
+ private DocumentIcon(doc: Doc) {
+ let layoutresult = StrCast(doc.type);
+ let renderDoc = doc;
+ //let box: number[] = [];
+ if (layoutresult.indexOf(DocumentType.COL) !== -1) {
+ renderDoc = Doc.MakeDelegate(renderDoc);
+ }
+ let returnXDimension = () => 150;
+ let returnYDimension = () => 150;
+ let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension());
+ //let scale = () => 1;
+ let newRenderDoc = Doc.MakeAlias(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt
+ newRenderDoc.height = NumCast(this.props.Document.documentIconHeight);
+ newRenderDoc.autoHeight = false;
+ const docview = <div>
+ <DocumentView
+ fitToBox={StrCast(doc.type).indexOf(DocumentType.COL) !== -1}
+ Document={newRenderDoc}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ ruleProvider={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ addDocTab={returnFalse}
+ pinToPres={returnFalse}
+ renderDepth={1}
+ PanelWidth={returnXDimension}
+ PanelHeight={returnYDimension}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ ContentScaling={scale}
+ />
+ </div>;
+ return docview;
+
+ }
+
+ // @action
+ // closeMenu = () => {
+ // this._display = false;
+ // this.previewDocs.forEach(doc => DocServer.DeleteDocument(doc[Id]));
+ // this.previewDocs = [];
+ // }
+
+ // @action
+ // resetDocuments = () => {
+ // this._documents = [];
+ // }
+
+ // @action
+ // displayRecommendations(x: number, y: number) {
+ // this._pageX = x;
+ // this._pageY = y;
+ // this._display = true;
+ // }
+
+ static readonly buffer = 20;
+
+ // get pageX() {
+ // const x = this._pageX;
+ // if (x < 0) {
+ // return 0;
+ // }
+ // const width = this._width;
+ // if (x + width > window.innerWidth - RecommendationsBox.buffer) {
+ // return window.innerWidth - RecommendationsBox.buffer - width;
+ // }
+ // return x;
+ // }
+
+ // get pageY() {
+ // const y = this._pageY;
+ // if (y < 0) {
+ // return 0;
+ // }
+ // const height = this._height;
+ // if (y + height > window.innerHeight - RecommendationsBox.buffer) {
+ // return window.innerHeight - RecommendationsBox.buffer - height;
+ // }
+ // return y;
+ // }
+
+ // get createDocViews() {
+ // return DocListCast(this.props.Document.data).map(doc => {
+ // return (
+ // <div className="content">
+ // <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background">
+ // {this.DocumentIcon(doc)}
+ // </span>
+ // <span className="score">{NumCast(doc.score).toFixed(4)}</span>
+ // <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}>
+ // <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" />
+ // </div>
+ // <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "User Selected Link", "Generated from Recommender", undefined)}>
+ // <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" />
+ // </div>
+ // </div>
+ // );
+ // });
+ // }
+
+ componentDidMount() { //TODO: invoking a computedFn from outside an reactive context won't be memoized, unless keepAlive is set
+ runInAction(() => {
+ if (this._docViews.length === 0) {
+ this._docViews = DocListCast(this.props.Document.data).map(doc => {
+ return (
+ <div className="content">
+ <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background">
+ {this.DocumentIcon(doc)}
+ </span>
+ <span className="score">{NumCast(doc.score).toFixed(4)}</span>
+ <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" />
+ </div>
+ <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "Recommender", undefined)}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" />
+ </div>
+ </div>
+ );
+ });
+ }
+ });
+ }
+
+ render() { //TODO: Invariant violation: max depth exceeded error. Occurs when images are rendered.
+ // if (!this._display) {
+ // return null;
+ // }
+ // let style = { left: this.pageX, top: this.pageY };
+ //const transform = "translate(" + (NumCast(this.props.node.x) + 350) + "px, " + NumCast(this.props.node.y) + "px"
+ let title = StrCast((this.props.Document.sourceDoc as Doc).title);
+ if (title.length > 15) {
+ title = title.substring(0, 15) + "...";
+ }
+ return (
+ <div className="rec-scroll">
+ <p>Recommendations for "{title}"</p>
+ {this._docViews}
+ </div>
+ );
+ }
+ //
+ //
+} \ No newline at end of file
diff --git a/src/client/views/TouchScrollableMenu.tsx b/src/client/views/TouchScrollableMenu.tsx
new file mode 100644
index 000000000..4bda0818e
--- /dev/null
+++ b/src/client/views/TouchScrollableMenu.tsx
@@ -0,0 +1,59 @@
+import React = require("react");
+import { computed } from "mobx";
+import { observer } from "mobx-react";
+
+export interface TouchScrollableMenuProps {
+ options: JSX.Element[];
+ bounds: {
+ right: number;
+ left: number;
+ bottom: number;
+ top: number;
+ width: number;
+ height: number;
+ };
+ selectedIndex: number;
+ x: number;
+ y: number;
+}
+
+export interface TouchScrollableMenuItemProps {
+ text: string;
+ onClick: () => any;
+}
+
+@observer
+export default class TouchScrollableMenu extends React.Component<TouchScrollableMenuProps> {
+
+ @computed
+ private get possibilities() { return this.props.options; }
+
+ @computed
+ private get selectedIndex() { return this.props.selectedIndex; }
+
+ render() {
+ return (
+ <div className="inkToTextDoc-cont" style={{
+ transform: `translate(${this.props.x}px, ${this.props.y}px)`,
+ width: 300,
+ height: this.possibilities.length * 25
+ }}>
+ <div className="inkToTextDoc-scroller" style={{ transform: `translate(0, ${-this.selectedIndex * 25}px)` }}>
+ {this.possibilities}
+ </div>
+ <div className="shadow" style={{ height: `calc(100% - 25px - ${this.selectedIndex * 25}px)` }}>
+ </div>
+ </div>
+ )
+ }
+}
+
+export class TouchScrollableMenuItem extends React.Component<TouchScrollableMenuItemProps>{
+ render() {
+ return (
+ <div className="menuItem-cont" onClick={this.props.onClick}>
+ {this.props.text}
+ </div>
+ )
+ }
+} \ No newline at end of file
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 7800c4019..bc3d07130 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -8,9 +8,11 @@ const HOLD_DURATION = 1000;
export abstract class Touchable<T = {}> extends React.Component<T> {
//private holdTimer: NodeJS.Timeout | undefined;
- private holdTimer: NodeJS.Timeout | undefined;
private moveDisposer?: InteractionUtils.MultiTouchEventDisposer;
private endDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ private holdMoveDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ private holdEndDisposer?: InteractionUtils.MultiTouchEventDisposer;
+
protected abstract multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
protected _touchDrag: boolean = false;
@@ -26,6 +28,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
*/
@action
protected onTouchStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
+
const actualPts: React.Touch[] = [];
const te = me.touchEvent;
// loop through all touches on screen
@@ -65,8 +68,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
// clearTimeout(this.holdTimer)
// this.holdTimer = undefined;
// }
- this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(te, me), HOLD_DURATION);
- // e.stopPropagation();
+ // console.log(this.holdTimer);
// console.log(this.holdTimer);
break;
case 2:
@@ -91,11 +93,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
// if we're not actually moving a lot, don't consider it as dragging yet
if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return;
this._touchDrag = true;
- if (this.holdTimer) {
- console.log("CLEAR");
- clearTimeout(this.holdTimer);
- // this.holdTimer = undefined;
- }
// console.log(myTouches.length);
switch (myTouches.length) {
case 1:
@@ -127,10 +124,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
}
}
}
- if (this.holdTimer) {
- clearTimeout(this.holdTimer);
- console.log("clear");
- }
this._touchDrag = false;
te.stopPropagation();
@@ -174,10 +167,16 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
this.addEndListeners();
}
- handle1PointerHoldStart = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
+ handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
e.stopPropagation();
- e.preventDefault();
+ me.touchEvent.stopPropagation();
this.removeMoveListeners();
+ this.removeEndListeners();
+ this.removeHoldMoveListeners();
+ this.removeHoldEndListeners();
+ this.addHoldMoveListeners();
+ this.addHoldEndListeners();
+
}
addMoveListeners = () => {
@@ -200,6 +199,44 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
this.endDisposer && this.endDisposer();
}
+ addHoldMoveListeners = () => {
+ const handler = (e: Event) => this.handle1PointerHoldMove(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
+ document.addEventListener("dashOnTouchHoldMove", handler);
+ this.holdMoveDisposer = () => document.removeEventListener("dashOnTouchHoldMove", handler);
+ }
+
+ addHoldEndListeners = () => {
+ const handler = (e: Event) => this.handle1PointerHoldEnd(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
+ document.addEventListener("dashOnTouchHoldEnd", handler);
+ this.holdEndDisposer = () => document.removeEventListener("dashOnTouchHoldEnd", handler);
+ }
+
+ removeHoldMoveListeners = () => {
+ this.holdMoveDisposer && this.holdMoveDisposer();
+ }
+
+ removeHoldEndListeners = () => {
+ this.holdEndDisposer && this.holdEndDisposer();
+ }
+
+
+ handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
+ // e.stopPropagation();
+ // me.touchEvent.stopPropagation();
+ }
+
+
+ handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
+ e.stopPropagation();
+ me.touchEvent.stopPropagation();
+ this.removeHoldMoveListeners();
+ this.removeHoldEndListeners();
+
+ me.touchEvent.stopPropagation();
+ me.touchEvent.preventDefault();
+ }
+
+
handleHandDown = (e: React.TouchEvent) => {
// e.stopPropagation();
// e.preventDefault();
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 1e38a8927..db8f7d5e4 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -127,36 +127,22 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@undoBatch
@action
public static CloseRightSplit(document: Opt<Doc>): boolean {
- if (!CollectionDockingView.Instance) return false;
const instance = CollectionDockingView.Instance;
- let retVal = false;
- if (instance._goldenLayout.root.contentItems[0].isRow) {
- retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
- if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
- DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId) &&
- ((!document && DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document.isDisplayPanel) ||
- (document && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)))) {
- child.contentItems[0].remove();
+ const tryClose = (childItem: any) => {
+ if (childItem.config?.component === "DocumentFrameRenderer") {
+ const docView = DocumentManager.Instance.getDocumentViewById(childItem.config.props.documentId);
+ if (docView && ((!document && docView.Document.isDisplayPanel) || (document && Doc.AreProtosEqual(docView.props.Document, document)))) {
+ childItem.remove();
instance.layoutChanged(document);
return true;
- } else {
- Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
- if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId) &&
- ((!document && DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document.isDisplayPanel) ||
- (document && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)))) {
- child.contentItems[j].remove();
- child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
- return true;
- }
- return false;
- });
}
- return false;
- });
- }
- if (retVal) {
- instance.stateChanged();
+ }
+ return false;
}
+ let retVal = !instance?._goldenLayout.root.contentItems[0].isRow ? false :
+ Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => Array.from(child.contentItems).some(tryClose));
+
+ retVal && instance.stateChanged();
return retVal;
}
@@ -238,6 +224,75 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
return true;
}
+
+ //
+ // Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side
+ //
+ @undoBatch
+ @action
+ public static AddSplit(document: Doc, pullSide: string, libraryPath?: Doc[]) {
+ if (!CollectionDockingView.Instance) return false;
+ const instance = CollectionDockingView.Instance;
+ const newItemStackConfig = {
+ type: 'stack',
+ content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)]
+ };
+
+ const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
+
+ if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns
+ instance._goldenLayout.root.addChild(newContentItem);
+ } else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row
+ if (pullSide === "left") {
+ instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0);
+ } else if (pullSide === "right") {
+ instance._goldenLayout.root.contentItems[0].addChild(newContentItem);
+ } else if (pullSide === "top" || pullSide === "bottom") {
+ // if not going in a row layout, must add already existing content into column
+ const rowlayout = instance._goldenLayout.root.contentItems[0];
+ const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout);
+ rowlayout.parent.replaceChild(rowlayout, newColumn);
+ if (pullSide === "top") {
+ newColumn.addChild(rowlayout, undefined, true);
+ newColumn.addChild(newContentItem, 0, true);
+ } else if (pullSide === "bottom") {
+ newColumn.addChild(newContentItem, undefined, true);
+ newColumn.addChild(rowlayout, 0, true);
+ }
+
+ rowlayout.config.height = 50;
+ newContentItem.config.height = 50;
+ }
+ } else if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column
+ if (pullSide === "top") {
+ instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0);
+ } else if (pullSide === "bottom") {
+ instance._goldenLayout.root.contentItems[0].addChild(newContentItem);
+ } else if (pullSide === "left" || pullSide === "right") {
+ // if not going in a row layout, must add already existing content into column
+ const collayout = instance._goldenLayout.root.contentItems[0];
+ const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout);
+ collayout.parent.replaceChild(collayout, newRow);
+
+ if (pullSide === "left") {
+ newRow.addChild(collayout, undefined, true);
+ newRow.addChild(newContentItem, 0, true);
+ } else if (pullSide === "right") {
+ newRow.addChild(newContentItem, undefined, true);
+ newRow.addChild(collayout, 0, true);
+ }
+
+ collayout.config.width = 50;
+ newContentItem.config.width = 50;
+ }
+ }
+
+ newContentItem.callDownwards('_$init');
+ instance.layoutChanged();
+ return true;
+ }
+
+
//
// Creates a vertical split on the right side of the docking view, and then adds the Document to that split
//
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index 4a14a30cd..79ec6d518 100644
--- a/src/client/views/collections/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -77,18 +77,19 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
getTransform = (ele: React.RefObject<HTMLDivElement>) => () => {
if (!ele.current) return Transform.Identity();
const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current);
- return new Transform(-translateX, -translateY, 1 / scale);
+ return new Transform(-translateX, -translateY, 1);
}
render() {
const guid = Utils.GenerateGuid();
+ const flexDir: any = StrCast(this.Document.flexDirection);
return <div className="collectionLinearView-outer">
<div className="collectionLinearView" ref={this.createDashEventsTarget} >
<input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle}
onChange={action((e: any) => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} />
<label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label>
- <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document._width, 25) }}>
+ <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document._width, 25), flexDirection: flexDir }}>
{this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => {
const nested = pair.layout._viewType === CollectionViewType.Linear;
const dref = React.createRef<HTMLDivElement>();
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index c9b7ca221..af3e18a4b 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -8,7 +8,7 @@ import Measure from "react-measure";
import { Doc } from "../../../new_fields/Doc";
import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { StrCast } from "../../../new_fields/Types";
+import { StrCast, NumCast } from "../../../new_fields/Types";
import { numberRange } from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
@@ -75,11 +75,12 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@undoBatch
rowDrop = action((e: Event, de: DragManager.DropEvent) => {
+ console.log("masronry row drop");
this._createAliasSelected = false;
if (de.complete.docDragData) {
(this.props.parent.Document.dropConverter instanceof ScriptField) &&
this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData });
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const castedValue = this.getValue(this._heading);
de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue);
this.props.parent.onInternalDrop(e, de);
@@ -98,7 +99,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@action
headingChanged = (value: string, shiftDown?: boolean) => {
this._createAliasSelected = false;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const castedValue = this.getValue(value);
if (castedValue) {
if (this.props.parent.sectionHeaders) {
@@ -137,15 +138,16 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@action
addDocument = (value: string, shiftDown?: boolean) => {
this._createAliasSelected = false;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
- const newDoc = Docs.Create.TextDocument("", { _height: 18, _width: 200, title: value });
+ const key = StrCast(this.props.parent.props.Document._pivotField);
+ const newDoc = Docs.Create.TextDocument(value, { _autoHeight: true, _width: 200, title: value });
newDoc[key] = this.getValue(this.props.heading);
- return this.props.parent.props.addDocument(newDoc);
+ const docs = this.props.parent.childDocList;
+ return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this.props.parent.props.addDocument(newDoc);
}
deleteRow = undoBatch(action(() => {
this._createAliasSelected = false;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
this.props.docList.forEach(d => d[key] = undefined);
if (this.props.parent.sectionHeaders && this.props.headingObject) {
const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
@@ -167,7 +169,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
const alias = Doc.MakeAlias(this.props.parent.props.Document);
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
let value = this.getValue(this._heading);
value = typeof value === "string" ? `"${value}"` : value;
const script = `return doc.${key} === ${value}`;
@@ -273,6 +275,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
};
return collapsed ? (null) :
<div style={{ position: "relative" }}>
+ {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
+ <div className="collectionStackingView-addDocumentButton"
+ style={{
+ width: style.columnWidth / style.numGroupColumns,
+ padding: NumCast(this.props.parent.layoutDoc._yPadding)
+ }}>
+ <EditableView {...newEditableViewProps} />
+ </div> : null
+ }
<div className={`collectionStackingView-masonryGrid`}
ref={this._contRef}
style={{
@@ -284,18 +295,12 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
{this.props.parent.children(this.props.docList)}
{this.props.parent.columnDragger}
</div>
- {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
- <div className="collectionStackingView-addDocumentButton"
- style={{ width: style.columnWidth / style.numGroupColumns }}>
- <EditableView {...newEditableViewProps} />
- </div> : null
- }
</div>;
}
@computed get headingView() {
const heading = this._heading;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
const headerEditableViewProps = {
GetValue: () => evContents,
@@ -316,7 +321,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
<div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
title={evContents === `NO ${key.toUpperCase()} VALUE` ?
`Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
- style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey", }}>
+ style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey" }}>
<EditableView {...headerEditableViewProps} />
{evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
<div className="collectionStackingView-sectionColor">
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index facde3648..df7abad61 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -23,6 +23,7 @@ import { faExpand } from '@fortawesome/free-solid-svg-icons';
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { KeyCodes } from "../../northstar/utils/KeyCodes";
import { undoBatch } from "../../util/UndoManager";
+import { List } from "lodash";
library.add(faExpand);
@@ -83,10 +84,20 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
}
@action
- onPointerDown = (e: React.PointerEvent): void => {
+ onPointerDown = async (e: React.PointerEvent): Promise<void> => {
this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
this.props.setPreviewDoc(this.props.rowProps.original);
+ let url: string;
+ if (url = StrCast(this.props.rowProps.row.href)) {
+ try {
+ new URL(url);
+ const temp = window.open(url)!;
+ temp.blur();
+ window.focus();
+ } catch { }
+ }
+
// this._isEditing = true;
// this.props.setIsEditing(true);
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index d1f45af90..3f7f0a352 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -31,21 +31,21 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
_masonryGridRef: HTMLDivElement | null = null;
_draggerRef = React.createRef<HTMLDivElement>();
_heightDisposer?: IReactionDisposer;
- _sectionFilterDisposer?: IReactionDisposer;
+ _pivotFieldDisposer?: IReactionDisposer;
_docXfs: any[] = [];
_columnStart: number = 0;
@observable _heightMap = new Map<string, number>();
@observable _cursor: CursorProperty = "grab";
@observable _scroll = 0; // used to force the document decoration to update when scrolling
@computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
- @computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); }
+ @computed get pivotField() { return StrCast(this.props.Document._pivotField); }
@computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); }
@computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); }
@computed get yMargin() { return Math.max(this.props.Document._showTitle && !this.props.Document._showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 0)); } // 2 * this.gridGap)); }
@computed get gridGap() { return NumCast(this.props.Document._gridGap, 10); }
@computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); }
@computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; }
- @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document._chromeStatus !== 'view-mode' && this.props.Document._chromeStatus !== 'disabled')); }
+ @computed get showAddAGroup() { return (this.pivotField && (this.props.Document._chromeStatus !== 'view-mode' && this.props.Document._chromeStatus !== 'disabled')); }
@computed get columnWidth() {
return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin,
this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250));
@@ -73,7 +73,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
get Sections() {
- if (!this.sectionFilter || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
+ if (!this.pivotField || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
if (this.sectionHeaders === undefined) {
setTimeout(() => this.props.Document.sectionHeaders = new List<SchemaHeaderField>(), 0);
@@ -83,18 +83,18 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
const fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
let changed = false;
this.filteredChildren.map(d => {
- const sectionValue = (d[this.sectionFilter] ? d[this.sectionFilter] : `NO ${this.sectionFilter.toUpperCase()} VALUE`) as object;
+ const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object;
// the next five lines ensures that floating point rounding errors don't create more than one section -syip
const parsed = parseInt(sectionValue.toString());
const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue;
// look for if header exists already
- const existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`));
+ const existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`));
if (existingHeader) {
fields.get(existingHeader)!.push(d);
}
else {
- const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`);
+ const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`);
fields.set(newSchemaHeader, [d]);
sectionHeaders.push(newSchemaHeader);
changed = true;
@@ -134,15 +134,15 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
);
// reset section headers when a new filter is inputted
- this._sectionFilterDisposer = reaction(
- () => this.sectionFilter,
+ this._pivotFieldDisposer = reaction(
+ () => this.pivotField,
() => this.props.Document.sectionHeaders = new List()
);
}
componentWillUnmount() {
super.componentWillUnmount();
- this._heightDisposer && this._heightDisposer();
- this._sectionFilterDisposer && this._sectionFilterDisposer();
+ this._heightDisposer?.();
+ this._pivotFieldDisposer?.();
}
@action
@@ -278,7 +278,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
headings = () => Array.from(this.Sections.keys());
sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
- const key = this.sectionFilter;
+ const key = this.pivotField;
let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
@@ -313,7 +313,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
- const key = this.sectionFilter;
+ const key = this.pivotField;
let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
@@ -341,7 +341,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
if (value && this.sectionHeaders) {
const schemaHdrField = new SchemaHeaderField(value);
this.sectionHeaders.push(schemaHdrField);
- Doc.addEnumerationToTextField(undefined, this.sectionFilter, [Docs.Create.TextDocument(value, { title: value, _backgroundColor: schemaHdrField.color })]);
+ Doc.addFieldEnumerations(undefined, this.pivotField, [{ title: value, _backgroundColor: schemaHdrField.color }]);
return true;
}
return false;
@@ -370,7 +370,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@computed get renderedSections() {
TraceMobx();
let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
- if (this.sectionFilter) {
+ if (this.pivotField) {
const entries = Array.from(this.Sections.entries());
sections = entries.sort(this.sortFunc);
}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 516e583d4..646b433bf 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -23,7 +23,6 @@ import { CollectionStackingView } from "./CollectionStackingView";
import { setupMoveUpEvents, emptyFunction } from "../../../Utils";
import "./CollectionStackingView.scss";
import { listSpec } from "../../../new_fields/Schema";
-import { Schema } from "prosemirror-model";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -62,7 +61,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@undoBatch
columnDrop = action((e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData) {
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const castedValue = this.getValue(this._heading);
de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, false));
this.props.parent.onInternalDrop(e, de);
@@ -85,7 +84,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
headingChanged = (value: string, shiftDown?: boolean) => {
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const castedValue = this.getValue(value);
if (castedValue) {
if (this.props.parent.sectionHeaders) {
@@ -126,7 +125,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
addDocument = (value: string, shiftDown?: boolean) => {
if (!value) return false;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, title: value, _autoHeight: true });
newDoc[key] = this.getValue(this.props.heading);
const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
@@ -137,7 +136,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
deleteColumn = () => {
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
this.props.docList.forEach(d => d[key] = undefined);
if (this.props.parent.sectionHeaders && this.props.headingObject) {
const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
@@ -161,8 +160,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
startDrag = (e: PointerEvent, down: number[], delta: number[]) => {
const alias = Doc.MakeAlias(this.props.parent.props.Document);
alias._width = this.props.parent.props.PanelWidth() / (Cast(this.props.parent.props.Document.sectionHeaders, listSpec(SchemaHeaderField))?.length || 1);
- alias.sectionFilter = undefined;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ alias._pivotField = undefined;
+ const key = StrCast(this.props.parent.props.Document._pivotField);
let value = this.getValue(this._heading);
value = typeof value === "string" ? `"${value}"` : value;
alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name });
@@ -277,7 +276,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
render() {
TraceMobx();
const cols = this.props.cols();
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
let templatecols = "";
const headings = this.props.headings();
const heading = this._heading;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 6ad187e5c..527623ad4 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -6,7 +6,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { Cast } from "../../../new_fields/Types";
+import { Cast, StrCast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
@@ -54,11 +54,13 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
private gestureDisposer?: GestureUtils.GestureEventDisposer;
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
private _childLayoutDisposer?: IReactionDisposer;
+ protected _mainCont?: HTMLDivElement;
protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this.dropDisposer?.();
this.gestureDisposer?.();
this.multiTouchDisposer?.();
if (ele) {
+ this._mainCont = ele;
this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this));
this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this));
this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this));
@@ -111,7 +113,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
return Cast(this.dataField, listSpec(Doc));
}
get childDocs() {
- const docs = DocListCast(this.dataField);
+ const dfield = this.dataField;
+ const rawdocs = (dfield instanceof Doc) ? [dfield] : Cast(dfield, listSpec(Doc), this.props.Document.expandedTemplate && !this.props.annotationsKey ? [Cast(this.props.Document.expandedTemplate, Doc, null)] : []);
+ const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc);
const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
}
@@ -150,7 +154,6 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@undoBatch
protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {
-
}
@undoBatch
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 50e297f0b..bcfbc7788 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -30,21 +30,25 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
_changing = false;
@observable _layoutEngine = "pivot";
+ componentWillUnmount() {
+ this.props.Document.onChildClick = undefined;
+ }
componentDidMount() {
this.props.Document._freezeOnDrop = true;
- const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked;
if (!this.props.Document._facetCollection) {
+ const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)";
const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true, treeViewHideHeaderFields: true });
facetCollection.target = this.props.Document;
+ facetCollection.onCheckedClick = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name });
this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilters"]);
- const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)";
- const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailView'); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
- facetCollection.onCheckedClick = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "boolean", checked: "boolean", containingTreeView: Doc.name });
- this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "boolean", containingCollection: Doc.name, shiftKey: "boolean" });
this.props.Document._facetCollection = facetCollection;
this.props.Document._fitToBox = true;
}
+ const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked;
+ const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailView'); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
+ this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "string", containingCollection: Doc.name, shiftKey: "boolean" });
+
if (!this.props.Document.onViewDefClick) {
this.props.Document.onViewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" });
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 7eeeb6ff1..28f620157 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -231,7 +231,7 @@ class TreeView extends React.Component<TreeViewProps> {
if (de.complete.linkDragData) {
const sourceDoc = de.complete.linkDragData.linkSourceDocument;
const destDoc = this.props.document;
- DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree drop link");
+ DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link");
e.stopPropagation();
}
if (de.complete.docDragData) {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 157229ec8..2d56f00d5 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -132,7 +132,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1);
index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
- ContextMenu.Instance.clearItems();
+ ContextMenu.Instance && ContextMenu.Instance.clearItems();
if (index !== -1) {
value.splice(index, 1);
targetDataDoc[this.props.fieldKey] = new List<Doc>(value);
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 0c96a662c..19a14976c 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -169,21 +169,21 @@
}
- .collectionStackingViewChrome-sectionFilter-cont,
- .collectionTreeViewChrome-sectionFilter-cont {
+ .collectionStackingViewChrome-pivotField-cont,
+ .collectionTreeViewChrome-pivotField-cont {
justify-self: right;
display: flex;
font-size: 75%;
letter-spacing: 2px;
- .collectionStackingViewChrome-sectionFilter-label,
- .collectionTreeViewChrome-sectionFilter-label {
+ .collectionStackingViewChrome-pivotField-label,
+ .collectionTreeViewChrome-pivotField-label {
vertical-align: center;
padding: 10px;
}
- .collectionStackingViewChrome-sectionFilter,
- .collectionTreeViewChrome-sectionFilter {
+ .collectionStackingViewChrome-pivotField,
+ .collectionTreeViewChrome-pivotField {
color: white;
width: 100px;
text-align: center;
@@ -210,8 +210,8 @@
}
}
- .collectionStackingViewChrome-sectionFilter:hover,
- .collectionTreeViewChrome-sectionFilter:hover {
+ .collectionStackingViewChrome-pivotField:hover,
+ .collectionTreeViewChrome-pivotField:hover {
cursor: text;
}
}
@@ -278,7 +278,7 @@
display:flex;
flex-direction: row;
width: 150px;
- margin: auto 0 auto auto;
+ margin: auto auto auto auto;
}
.react-autosuggest__container {
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 49a9e8200..57a73000d 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -472,7 +472,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
@observable private suggestions: string[] = [];
@computed private get descending() { return BoolCast(this.props.CollectionView.props.Document.stackingHeadersSortDescending); }
- @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); }
+ @computed get pivotField() { return StrCast(this.props.CollectionView.props.Document._pivotField); }
getKeySuggestions = async (value: string): Promise<string[]> => {
value = value.toLowerCase();
@@ -510,26 +510,26 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
}
setValue = (value: string) => {
- this.props.CollectionView.props.Document.sectionFilter = value;
+ this.props.CollectionView.props.Document._pivotField = value;
return true;
}
@action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; };
- @action resetValue = () => { this._currentKey = this.sectionFilter; };
+ @action resetValue = () => { this._currentKey = this.pivotField; };
render() {
return (
<div className="collectionStackingViewChrome-cont">
- <div className="collectionStackingViewChrome-sectionFilter-cont">
- <div className="collectionStackingViewChrome-sectionFilter-label">
+ <div className="collectionStackingViewChrome-pivotField-cont">
+ <div className="collectionStackingViewChrome-pivotField-label">
GROUP ITEMS BY:
</div>
<div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
<FontAwesomeIcon icon="caret-up" size="2x" color="white" />
</div>
- <div className="collectionStackingViewChrome-sectionFilter">
+ <div className="collectionStackingViewChrome-pivotField">
<EditableView
- GetValue={() => this.sectionFilter}
+ GetValue={() => this.pivotField}
autosuggestProps={
{
resetValue: this.resetValue,
@@ -551,7 +551,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
}}
oneLine
SetValue={this.setValue}
- contents={this.sectionFilter ? this.sectionFilter : "N/A"}
+ contents={this.pivotField ? this.pivotField : "N/A"}
/>
</div>
</div>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 1038347d4..a33146388 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -46,8 +46,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
// really hacky stuff to make the DocuLinkBox display where we want it to:
// if there's an element in the DOM with the id of the opposite anchor, then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right
// otherwise, we just use the computed nearest point on the document boundary to the target Document
- const targetAhyperlink = window.document.getElementById((this.props.LinkDocs[0][afield] as Doc)[Id]);
- const targetBhyperlink = window.document.getElementById((this.props.LinkDocs[0][bfield] as Doc)[Id]);
+ const targetAhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][afield] as Doc)[Id]);
+ const targetBhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][bfield] as Doc)[Id]);
if (!targetBhyperlink) {
this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100;
this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 0b5e44ccb..730392ab5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -36,6 +36,7 @@
height: 100%;
display: flex;
align-items: center;
+
.collectionfreeformview-placeholderSpan {
font-size: 32;
display: flex;
@@ -99,4 +100,10 @@
#prevCursor {
animation: blink 1s infinite;
+}
+
+.pullpane-indicator {
+ z-index: 99999;
+ background-color: rgba($color: #000000, $alpha: .4);
+ position: absolute;
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ca8d5e18b..a331f736f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,20 +1,22 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faEye } from "@fortawesome/free-regular-svg-icons";
import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from "../../../../new_fields/Doc";
+import { Doc, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
import { Id } from "../../../../new_fields/FieldSymbols";
-import { InkTool } from "../../../../new_fields/InkField";
+import { InkData, InkField, InkTool } from "../../../../new_fields/InkField";
+import { List } from "../../../../new_fields/List";
+import { RichTextField } from "../../../../new_fields/RichTextField";
import { createSchema, listSpec, makeInterface } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
-import { Cast, NumCast, ScriptCast, BoolCast, StrCast } from "../../../../new_fields/Types";
+import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../new_fields/Types";
import { TraceMobx } from "../../../../new_fields/util";
import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
-import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { aggregateBounds, intersectRect, returnOne, Utils } from "../../../../Utils";
+import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
import { DocServer } from "../../../DocServer";
import { Docs } from "../../../documents/Documents";
import { DocumentManager } from "../../../util/DocumentManager";
@@ -33,6 +35,7 @@ import { DocumentViewProps } from "../../nodes/DocumentView";
import { FormattedTextBox } from "../../nodes/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
import PDFMenu from "../../pdf/PDFMenu";
+import { CollectionDockingView } from "../CollectionDockingView";
import { CollectionSubView } from "../CollectionSubView";
import { computePivotLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
@@ -68,11 +71,16 @@ const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSch
export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private _lastX: number = 0;
private _lastY: number = 0;
+ private _inkToTextStartX: number | undefined;
+ private _inkToTextStartY: number | undefined;
+ private _wordPalette: Map<string, string> = new Map<string, string>();
private _clusterDistance: number = 75;
private _hitCluster = false;
private _layoutComputeReaction: IReactionDisposer | undefined;
- private _layoutPoolData = observable.map<string, any>();
+ private _layoutPoolData = new ObservableMap<string, any>();
private _cachedPool: Map<string, any> = new Map();
+ @observable private _pullCoords: number[] = [0, 0];
+ @observable private _pullDirection: string = "";
public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive
@observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
@@ -411,8 +419,91 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
});
this.addDocument(Docs.Create.FreeformDocument(sel, { title: "nested collection", x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 }));
sel.forEach(d => this.props.removeDocument(d));
+ e.stopPropagation();
break;
-
+ case GestureUtils.Gestures.StartBracket:
+ const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y)));
+ this._inkToTextStartX = start[0];
+ this._inkToTextStartY = start[1];
+ console.log("start");
+ break;
+ case GestureUtils.Gestures.EndBracket:
+ console.log("end");
+ if (this._inkToTextStartX && this._inkToTextStartY) {
+ const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
+ const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "text" && s.color);
+ const sets = setDocs.map((sd) => {
+ return Cast(sd.data, RichTextField)?.Text as string;
+ });
+ if (sets.length && sets[0]) {
+ this._wordPalette.clear();
+ const colors = setDocs.map(sd => FieldValue(sd.color) as string);
+ sets.forEach((st: string, i: number) => {
+ const words = st.split(",");
+ words.forEach(word => {
+ this._wordPalette.set(word, colors[i]);
+ });
+ });
+ }
+ const inks = this.getActiveDocuments().filter(doc => {
+ if (doc.type === "ink") {
+ const l = NumCast(doc.x);
+ const r = l + doc[WidthSym]();
+ const t = NumCast(doc.y);
+ const b = t + doc[HeightSym]();
+ const pass = !(this._inkToTextStartX! > r || end[0] < l || this._inkToTextStartY! > b || end[1] < t);
+ return pass;
+ }
+ return false;
+ });
+ // const inkFields = inks.map(i => Cast(i.data, InkField));
+ const strokes: InkData[] = [];
+ inks.forEach(i => {
+ const d = Cast(i.data, InkField);
+ const x = NumCast(i.x);
+ const y = NumCast(i.y);
+ const left = Math.min(...d?.inkData.map(pd => pd.X) ?? [0]);
+ const top = Math.min(...d?.inkData.map(pd => pd.Y) ?? [0]);
+ if (d) {
+ strokes.push(d.inkData.map(pd => ({ X: pd.X + x - left, Y: pd.Y + y - top })));
+ }
+ });
+
+ CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => {
+ console.log(results);
+ const wordResults = results.filter((r: any) => r.category === "inkWord");
+ for (const word of wordResults) {
+ const indices: number[] = word.strokeIds;
+ indices.forEach(i => {
+ const otherInks: Doc[] = [];
+ indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2]));
+ inks[i].relatedInks = new List<Doc>(otherInks);
+ const uniqueColors: string[] = [];
+ Array.from(this._wordPalette.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c));
+ inks[i].alternativeColors = new List<string>(uniqueColors);
+ if (this._wordPalette.has(word.recognizedText.toLowerCase())) {
+ inks[i].color = this._wordPalette.get(word.recognizedText.toLowerCase());
+ }
+ else if (word.alternates) {
+ for (const alt of word.alternates) {
+ if (this._wordPalette.has(alt.recognizedString.toLowerCase())) {
+ inks[i].color = this._wordPalette.get(alt.recognizedString.toLowerCase());
+ break;
+ }
+ }
+ }
+ });
+ }
+ });
+ this._inkToTextStartX = end[0];
+ }
+ break;
+ case GestureUtils.Gestures.Text:
+ if (ge.text) {
+ const B = this.getTransform().transformPoint(ge.points[0].X, ge.points[0].Y);
+ this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] }));
+ e.stopPropagation();
+ }
}
}
@@ -429,7 +520,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => {
// I think it makes sense for the marquee menu to go away when panned. -syip2
- MarqueeOptionsMenu.Instance.fadeOut(true);
+ MarqueeOptionsMenu.Instance && MarqueeOptionsMenu.Instance.fadeOut(true);
let x = this.Document._panX || 0;
let y = this.Document._panY || 0;
@@ -547,7 +638,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// use the centerx and centery as the "new mouse position"
const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
- this.pan({ clientX: centerX, clientY: centerY });
+ // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY);
+
+ if (!this._pullDirection) { // if we are not bezel movement
+ this.pan({ clientX: centerX, clientY: centerY });
+ } else {
+ this._pullCoords = [centerX, centerY];
+ }
+
this._lastX = centerX;
this._lastY = centerY;
}
@@ -572,6 +670,27 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
this._lastX = centerX;
this._lastY = centerY;
+ const screenBox = this._mainCont?.getBoundingClientRect();
+
+
+ // determine if we are using a bezel movement
+ if (screenBox) {
+ if ((screenBox.right - centerX) < 100) {
+ this._pullCoords = [centerX, centerY];
+ this._pullDirection = "right";
+ } else if (centerX - screenBox.left < 100) {
+ this._pullCoords = [centerX, centerY];
+ this._pullDirection = "left";
+ } else if (screenBox.bottom - centerY < 100) {
+ this._pullCoords = [centerX, centerY];
+ this._pullDirection = "bottom";
+ } else if (centerY - screenBox.top < 100) {
+ this._pullCoords = [centerX, centerY];
+ this._pullDirection = "top";
+ }
+ }
+
+
this.removeMoveListeners();
this.addMoveListeners();
this.removeEndListeners();
@@ -582,12 +701,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
cleanUpInteractions = () => {
+ switch (this._pullDirection) {
+ case "left":
+ case "right":
+ case "top":
+ case "bottom":
+ CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), this._pullDirection);
+ }
+
+ this._pullDirection = "";
+ this._pullCoords = [0, 0];
+
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
this.removeMoveListeners();
this.removeEndListeners();
}
+
@action
zoom = (pointX: number, pointY: number, deltaY: number): void => {
let deltaScale = deltaY > 0 ? (1 / 1.1) : 1.1;
@@ -670,6 +801,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const offset = annotOn && (contextHgt / 2 * 96 / 72);
this.props.Document.scrollY = NumCast(doc.y) - offset;
}
+
+ afterFocus && setTimeout(() => afterFocus?.(), 1000);
} else {
const layoutdoc = Doc.Layout(doc);
const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2;
@@ -687,7 +820,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
Doc.linkFollowHighlight(doc);
afterFocus && setTimeout(() => {
- if (afterFocus && afterFocus()) {
+ if (afterFocus?.()) {
this.Document._panX = savedState.px;
this.Document._panY = savedState.py;
this.Document.scale = savedState.s;
@@ -1035,6 +1168,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
</CollectionFreeFormViewPannableContents>
</MarqueeView>;
}
+
@computed get contentScaling() {
if (this.props.annotationsKey) return 0;
const hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1;
@@ -1043,6 +1177,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
render() {
TraceMobx();
+ const clientRect = this._mainCont?.getBoundingClientRect();
// update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
// this.Document.fitX = this.contentBounds && this.contentBounds.x;
// this.Document.fitY = this.contentBounds && this.contentBounds.y;
@@ -1064,7 +1199,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
{!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ?
this.placeholder : this.marqueeView}
<CollectionFreeFormOverlayView elements={this.elementFunc} />
- </div>;
+
+ <div className={"pullpane-indicator"}
+ style={{
+ display: this._pullDirection ? "block" : "none",
+ top: clientRect ? this._pullDirection === "bottom" ? this._pullCoords[1] - clientRect.y : 0 : "auto",
+ // left: clientRect ? this._pullDirection === "right" ? this._pullCoords[0] - clientRect.x - MainView.Instance.flyoutWidth : 0 : "auto",
+ left: clientRect ? this._pullDirection === "right" ? this._pullCoords[0] - clientRect.x : 0 : "auto",
+ width: clientRect ? this._pullDirection === "left" ? this._pullCoords[0] - clientRect.left : this._pullDirection === "right" ? clientRect.right - this._pullCoords[0] : clientRect.width : 0,
+ height: clientRect ? this._pullDirection === "top" ? this._pullCoords[1] - clientRect.top : this._pullDirection === "bottom" ? clientRect.bottom - this._pullCoords[1] : clientRect.height : 0,
+
+ }}>
+ </div>
+
+ </div >;
}
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index 71f265484..db4b674b5 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -11,6 +11,7 @@ export default class MarqueeOptionsMenu extends AntimodeMenu {
public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+ public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public showMarquee: () => void = unimplementedFunction;
public hideMarquee: () => void = unimplementedFunction;
@@ -43,6 +44,13 @@ export default class MarqueeOptionsMenu extends AntimodeMenu {
onPointerDown={this.delete}>
<FontAwesomeIcon icon="trash-alt" size="lg" />
</button>,
+ <button
+ className="antimodeMenu-button"
+ title="Change to Text"
+ key="inkToText"
+ onPointerDown={this.inkToText}>
+ <FontAwesomeIcon icon="font" size="lg" />
+ </button>,
];
return this.getElement(buttons);
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index d4fa22a7e..4bf3329eb 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,10 +1,10 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast } from "../../../../new_fields/Doc";
-import { InkField } from "../../../../new_fields/InkField";
+import { Doc, DocListCast, DataSym, WidthSym, HeightSym } from "../../../../new_fields/Doc";
+import { InkField, InkData } from "../../../../new_fields/InkField";
import { List } from "../../../../new_fields/List";
import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
-import { Cast, NumCast } from "../../../../new_fields/Types";
+import { Cast, NumCast, FieldValue } from "../../../../new_fields/Types";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { Utils } from "../../../../Utils";
import { Docs, DocUtils } from "../../../documents/Documents";
@@ -17,6 +17,8 @@ import { SubCollectionViewProps } from "../CollectionSubView";
import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
import "./MarqueeView.scss";
import React = require("react");
+import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
+import { RichTextField } from "../../../../new_fields/RichTextField";
import { CollectionView } from "../CollectionView";
import { FormattedTextBox } from "../../nodes/FormattedTextBox";
@@ -107,11 +109,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : "";
this.props.addLiveTextDocument(
Docs.Create.TextDocument("", { _width: NumCast((FormattedTextBox.DefaultLayout as Doc)?._width) || 200, _height: 100, layout: FormattedTextBox.DefaultLayout, x: x, y: y, _autoHeight: true, title: "-typed text-" }));
- } else if (e.keyCode > 48 && e.keyCode <= 57) {
- const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
- const text = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" });
- text.layout = notes[(e.keyCode - 49) % notes.length];
- this.props.addLiveTextDocument(text);
}
e.stopPropagation();
}
@@ -209,6 +206,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
MarqueeOptionsMenu.Instance.createCollection = this.collection;
MarqueeOptionsMenu.Instance.delete = this.delete;
MarqueeOptionsMenu.Instance.summarize = this.summary;
+ MarqueeOptionsMenu.Instance.inkToText = this.syntaxHighlight;
MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee;
MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee;
MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY);
@@ -348,6 +346,85 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}
@action
+ syntaxHighlight = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+ const selected = this.marqueeSelect(false);
+ if (e instanceof KeyboardEvent ? e.key === "i" : true) {
+ const inks = selected.filter(s => s.proto?.type === "ink");
+ const setDocs = selected.filter(s => s.proto?.type === "text" && s.color);
+ const sets = setDocs.map((sd) => {
+ return Cast(sd.data, RichTextField)?.Text as string;
+ });
+ const colors = setDocs.map(sd => FieldValue(sd.color) as string);
+ const wordToColor = new Map<string, string>();
+ sets.forEach((st: string, i: number) => {
+ const words = st.split(",");
+ words.forEach(word => {
+ wordToColor.set(word, colors[i]);
+ });
+ });
+ const strokes: InkData[] = [];
+ inks.forEach(i => {
+ const d = Cast(i.data, InkField);
+ const x = NumCast(i.x);
+ const y = NumCast(i.y);
+ const left = Math.min(...d?.inkData.map(pd => pd.X) ?? [0]);
+ const top = Math.min(...d?.inkData.map(pd => pd.Y) ?? [0]);
+ if (d) {
+ strokes.push(d.inkData.map(pd => ({ X: pd.X + x - left, Y: pd.Y + y - top })));
+ }
+ });
+ CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => {
+ // const wordResults = results.filter((r: any) => r.category === "inkWord");
+ // console.log(wordResults);
+ // console.log(results);
+ // for (const word of wordResults) {
+ // const indices: number[] = word.strokeIds;
+ // indices.forEach(i => {
+ // if (wordToColor.has(word.recognizedText.toLowerCase())) {
+ // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase());
+ // }
+ // else {
+ // for (const alt of word.alternates) {
+ // if (wordToColor.has(alt.recognizedString.toLowerCase())) {
+ // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase());
+ // break;
+ // }
+ // }
+ // }
+ // })
+ // }
+ // const wordResults = results.filter((r: any) => r.category === "inkWord");
+ // for (const word of wordResults) {
+ // const indices: number[] = word.strokeIds;
+ // indices.forEach(i => {
+ // const otherInks: Doc[] = [];
+ // indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2]));
+ // inks[i].relatedInks = new List<Doc>(otherInks);
+ // const uniqueColors: string[] = [];
+ // Array.from(wordToColor.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c));
+ // inks[i].alternativeColors = new List<string>(uniqueColors);
+ // if (wordToColor.has(word.recognizedText.toLowerCase())) {
+ // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase());
+ // }
+ // else if (word.alternates) {
+ // for (const alt of word.alternates) {
+ // if (wordToColor.has(alt.recognizedString.toLowerCase())) {
+ // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase());
+ // break;
+ // }
+ // }
+ // }
+ // });
+ // }
+ const lines = results.filter((r: any) => r.category === "line");
+ console.log(lines);
+ const text = lines.map((l: any) => l.recognizedText).join("\r\n");
+ this.props.addDocument(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text }));
+ });
+ }
+ }
+
+ @action
summary = (e: KeyboardEvent | React.PointerEvent | undefined) => {
const bounds = this.Bounds;
const selected = this.marqueeSelect(false);
@@ -358,13 +435,14 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
d.page = -1;
return d;
});
- const summary = Docs.Create.TextDocument("", { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "-summary-" });
+ const summary = Docs.Create.TextDocument("", { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" });
const portal = Doc.MakeAlias(summary);
- Doc.GetProto(summary)["data-annotations"] = new List<Doc>(selected);
- Doc.GetProto(summary).layout_portal = CollectionView.LayoutString("data-annotations");
+ Doc.GetProto(summary)[Doc.LayoutFieldKey(summary) + "-annotations"] = new List<Doc>(selected);
+ Doc.GetProto(summary).layout_portal = CollectionView.LayoutString(Doc.LayoutFieldKey(summary) + "-annotations");
summary._backgroundColor = "#e2ad32";
portal.layoutKey = "layout_portal";
- DocUtils.MakeLink({ doc: summary, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, "portal link", "portal link");
+ portal.title = "document collection";
+ DocUtils.MakeLink({ doc: summary, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, "summarizing");
this.props.addLiveTextDocument(summary);
MarqueeOptionsMenu.Instance.fadeOut(true);
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 82175c0b5..bd20781dc 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -223,17 +223,13 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
*/
@computed
private get contents(): JSX.Element[] | null {
- // bcz: feels like a hack ... trying to show something useful when there's no list document in the data field of a templated object
- const expanded = Cast(this.props.Document.expandedTemplate, Doc, null);
- let { childLayoutPairs } = this.dataDoc[this.props.fieldKey] instanceof List || !expanded ? this : { childLayoutPairs: [] } as { childLayoutPairs: { layout: Doc, data: Doc }[] };
- const replaced = !childLayoutPairs.length && !Cast(expanded?.layout, Doc, null) && expanded;
- childLayoutPairs = childLayoutPairs.length || !replaced ? childLayoutPairs : [{ layout: replaced, data: replaced }];
+ let { childLayoutPairs } = this;
const { Document, PanelHeight } = this.props;
const collector: JSX.Element[] = [];
for (let i = 0; i < childLayoutPairs.length; i++) {
const { layout } = childLayoutPairs[i];
const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin));
- const width = () => expanded ? this.props.PanelWidth() : this.lookupPixels(layout);
+ const width = () => this.lookupPixels(layout);
const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0);
collector.push(
<div className={"document-wrapper"}
@@ -248,6 +244,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
<ResizeBar
width={resizerWidth}
key={"resizer" + i}
+ select={this.props.select}
columnUnitLength={this.getColumnUnitLength}
toLeft={layout}
toRight={childLayoutPairs[i + 1]?.layout}
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 2cbeb3526..e1e604686 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -11,6 +11,7 @@ interface ResizerProps {
columnUnitLength(): number | undefined;
toLeft?: Doc;
toRight?: Doc;
+ select: (isCtrlPressed: boolean) => void;
}
const resizerOpacity = 1;
@@ -23,6 +24,7 @@ export default class ResizeBar extends React.Component<ResizerProps> {
@action
private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => {
+ this.props.select(false);
e.stopPropagation();
e.preventDefault();
window.removeEventListener("pointermove", this.onPointerMove);
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 3b19a6dba..83cdf3574 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -9,7 +9,6 @@
width:20px;
height:100%;
display:inline-block;
- background: gray;
}
.audiobox-control, .audiobox-control-interactive {
top:0;
@@ -30,6 +29,8 @@
}
.audiobox-record-interactive {
pointer-events: all;
+ width:100%;
+ height:100%;
}
.audiobox-controls {
width:100%;
@@ -37,7 +38,6 @@
position: relative;
display: flex;
padding-left: 2px;
- border: gray solid 3px;
.audiobox-player {
margin-top:auto;
margin-bottom:auto;
@@ -74,6 +74,7 @@
margin-left:-2.55px;
background:gray;
border-radius: 100%;
+ opacity:0.9;
background-color: transparent;
box-shadow: black 2px 2px 1px;
.docuLinkBox-cont {
@@ -100,7 +101,7 @@
}
}
.audiobox-linker:hover, .audiobox-linker-mini:hover {
- transform:scale(1.5);
+ opacity:1;
}
.audiobox-marker-container, .audiobox-marker-minicontainer {
position:absolute;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 9c57b6d7f..80d59af0b 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -17,6 +17,8 @@ import { ContextMenu } from "../ContextMenu";
import { Id } from "../../../new_fields/FieldSymbols";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentView } from "./DocumentView";
+import { Docs } from "../../documents/Documents";
+import { ComputedField } from "../../../new_fields/ScriptField";
// testing testing
@@ -48,37 +50,45 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
_recordStart = 0;
_stream: MediaStream | undefined;
+ public static START = 0;
@observable private static _scrubTime = 0;
- @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
- @observable private _playing = false;
- public static SetScrubTime = action((timeInMillisFrom1970: number) => AudioBox._scrubTime = timeInMillisFrom1970);
+ @computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); }
+ set audioState(value) { this.dataDoc.audioState = value; }
+ public static SetScrubTime = (timeInMillisFrom1970: number) => { runInAction(() => AudioBox._scrubTime = 0); runInAction(() => AudioBox._scrubTime = timeInMillisFrom1970); };
public static ActiveRecordings: Doc[] = [];
+ @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
+ async slideTemplate() { return (await Cast((await Cast(Doc.UserDoc().slidesBtn, Doc) as Doc).dragFactory, Doc) as Doc); }
+
+ componentWillUnmount() {
+ this._reactionDisposer?.();
+ this._linkPlayDisposer?.();
+ this._scrubbingDisposer?.();
+ }
componentDidMount() {
- runInAction(() => this._audioState = this.path ? "recorded" : "unrecorded");
+ runInAction(() => this.audioState = this.path ? "paused" : undefined);
this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
scrollLinkId => {
- scrollLinkId && DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
- const la1 = l.anchor1 as Doc;
- const linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode);
- setTimeout(() => { this.playFrom(linkTime); Doc.linkFollowHighlight(l); }, 250);
- });
- scrollLinkId && Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
+ if (scrollLinkId) {
+ DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
+ const linkTime = Doc.AreProtosEqual(l.anchor1 as Doc, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode);
+ setTimeout(() => { this.playFromTime(linkTime); Doc.linkFollowHighlight(l); }, 250);
+ });
+ Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
+ }
}, { fireImmediately: true });
this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
selected => {
const sel = selected.length ? selected[0].props.Document : undefined;
- this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
+ this.Document.playOnSelect && this.recordingStart && sel && sel.creationDate && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFromTime(DateCast(sel.creationDate).date.getTime());
+ this.Document.playOnSelect && this.recordingStart && !sel && this.pause();
});
- this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => {
- const start = DateCast(this.dataDoc[this.props.fieldKey + "-recordingStart"]);
- start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
- });
+ this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, (time) => this.Document.playOnSelect && this.playFromTime(AudioBox._scrubTime));
}
timecodeChanged = () => {
const htmlEle = this._ele;
- if (this._audioState === "recorded" && htmlEle) {
+ if (this.audioState !== "recording" && htmlEle) {
htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration);
DocListCast(this.dataDoc.links).map(l => {
let la1 = l.anchor1 as Doc;
@@ -97,32 +107,33 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
pause = action(() => {
this._ele!.pause();
- this._playing = false;
+ this.audioState = "paused";
});
+ playFromTime = (absoluteTime: number) => {
+ this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000);
+ }
playFrom = (seekTimeInSeconds: number) => {
if (this._ele && AudioBox.Enabled) {
if (seekTimeInSeconds < 0) {
- this.pause();
+ if (seekTimeInSeconds > -1) {
+ setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
+ } else {
+ this.pause();
+ }
} else if (seekTimeInSeconds <= this._ele.duration) {
this._ele.currentTime = seekTimeInSeconds;
this._ele.play();
- runInAction(() => this._playing = true);
+ runInAction(() => this.audioState = "playing");
} else {
this.pause();
}
}
}
- componentWillUnmount() {
- this._reactionDisposer && this._reactionDisposer();
- this._linkPlayDisposer && this._linkPlayDisposer();
- this._scrubbingDisposer && this._scrubbingDisposer();
- }
-
updateRecordTime = () => {
- if (this._audioState === "recording") {
+ if (this.audioState === "recording") {
setTimeout(this.updateRecordTime, 30);
this.Document.currentTimecode = (new Date().getTime() - this._recordStart) / 1000;
}
@@ -136,6 +147,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
self._stream = stream;
self._recorder = new MediaRecorder(stream);
self.dataDoc[self.props.fieldKey + "-recordingStart"] = new DateField(new Date());
+ AudioBox.START = new DateField(new Date()).date.getTime();
AudioBox.ActiveRecordings.push(self.props.Document);
self._recorder.ondataavailable = async function (e: any) {
const formData = new FormData();
@@ -149,8 +161,9 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
// upload to server with known URL
self.props.Document[self.props.fieldKey] = new AudioField(url);
};
- runInAction(() => self._audioState = "recording");
self._recordStart = new Date().getTime();
+ console.log("RECORD START = " + self._recordStart);
+ runInAction(() => self.audioState = "recording");
setTimeout(self.updateRecordTime, 0);
self._recorder.start();
setTimeout(() => {
@@ -170,7 +183,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
stopRecording = action(() => {
this._recorder.stop();
this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000;
- this._audioState = "recorded";
+ this.audioState = "paused";
this._stream?.getAudioTracks()[0].stop();
const ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
@@ -188,14 +201,25 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
e.stopPropagation();
}
onStop = (e: any) => {
- this.stopRecording();
- this._ele!.currentTime = 0;
+ this.Document.playOnSelect = !this.Document.playOnSelect;
+ e.stopPropagation();
+ }
+ onFile = (e: any) => {
+ const newDoc = Docs.Create.TextDocument("", {
+ title: "", _chromeStatus: "disabled",
+ x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10,
+ _width: NumCast(this.props.Document._width), _height: 3 * NumCast(this.props.Document._height)
+ });
+ Doc.GetProto(newDoc).recordingSource = this.dataDoc;
+ Doc.GetProto(newDoc).recordingStart = 0;
+ Doc.GetProto(newDoc).audioState = ComputedField.MakeFunction("this.recordingSource.audioState");
+ this.props.addDocument?.(newDoc);
e.stopPropagation();
}
setRef = (e: HTMLAudioElement | null) => {
- e && e.addEventListener("timeupdate", this.timecodeChanged);
- e && e.addEventListener("ended", this.pause);
+ e?.addEventListener("timeupdate", this.timecodeChanged);
+ e?.addEventListener("ended", this.pause);
this._ele = e;
}
@@ -218,21 +242,26 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
const interactive = this.active() ? "-interactive" : "";
return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
onClick={!this.path ? this.recordClick : undefined}>
- <div className="audiobox-handle"></div>
{!this.path ?
- <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
- {this._audioState === "recording" ? "STOP" : "RECORD"}
- </button> :
+ <div style={{ display: "flex", width: "100%", alignItems: "center" }}>
+ <div className="audiobox-playhead" style={{ alignItems: "center", display: "inherit", background: "dimgray" }} onClick={this.onFile}>
+ <FontAwesomeIcon style={{ width: "30px", background: this.Document.playOnSelect ? "yellow" : "dimGray" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+ </div>
+ <button className={`audiobox-record${interactive}`} style={{ position: "relative", backgroundColor: this.audioState === "recording" ? "red" : "black" }}>
+ {this.audioState === "recording" ? "STOP" : "RECORD"}
+ </button>
+ </div> :
<div className="audiobox-controls">
<div className="audiobox-player" onClick={this.onPlay}>
- <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
- <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+ <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this.audioState === "paused" ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+ <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%", background: this.Document.playOnSelect ? "yellow" : "dimGray" }} icon="hand-point-left" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
<div className="audiobox-timeline" onClick={e => e.stopPropagation()}
onPointerDown={e => {
if (e.button === 0 && !e.ctrlKey) {
const rect = (e.target as any).getBoundingClientRect();
+ const wasPaused = this.audioState === "paused";
this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
- this.pause();
+ wasPaused && this.pause();
e.stopPropagation();
}
}} >
@@ -249,12 +278,12 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
<div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
<div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
<DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
+ ContainingCollectionDoc={this.props.Document}
parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
backgroundColor={returnTransparent} />
</div>
<div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
- onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
- onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
+ onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const wasPaused = this.audioState === "paused"; this.playFrom(linkTime); wasPaused && this.pause(); e.stopPropagation(); } }} />
</div>;
})}
<div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index bfda13eb3..dcb6d4a31 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -26,14 +26,14 @@ import { PDFBox } from "./PDFBox";
import { PresBox } from "./PresBox";
import { QueryBox } from "./QueryBox";
import { ColorBox } from "./ColorBox";
+import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo";
import { DocuLinkBox } from "./DocuLinkBox";
import { PresElementBox } from "../presentationview/PresElementBox";
import { VideoBox } from "./VideoBox";
import { WebBox } from "./WebBox";
import { InkingStroke } from "../InkingStroke";
import React = require("react");
-import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo";
-
+import { RecommendationsBox } from "../RecommendationsBox";
import { TraceMobx } from "../../../new_fields/util";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -73,21 +73,11 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
get dataDoc() {
- if (this.props.DataDoc === undefined && typeof Doc.LayoutField(this.props.Document) !== "string") {
- // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string),
- // then we render the layout document as a template and use this document as the data context for the template layout.
- const proto = Doc.GetProto(this.props.Document);
- return proto instanceof Promise ? undefined : proto;
- }
- return this.props.DataDoc instanceof Promise ? undefined : this.props.DataDoc;
+ const proto = this.props.DataDoc || Doc.GetProto(this.props.Document);
+ return proto instanceof Promise ? undefined : proto;
}
get layoutDoc() {
- if (this.props.LayoutDoc || (this.props.DataDoc === undefined && typeof Doc.LayoutField(this.props.Document) !== "string")) {
- // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string),
- // then we render the layout document as a template and use this document as the data context for the template layout.
- return Doc.expandTemplateLayout(this.props.LayoutDoc?.() || Doc.Layout(this.props.Document), this.props.Document);
- }
- return Doc.Layout(this.props.Document);
+ return Doc.expandTemplateLayout(this.props.LayoutDoc?.() || Doc.Layout(this.props.Document), this.props.Document);
}
CreateBindings(): JsxBindings {
@@ -108,7 +98,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, ButtonBox, SliderBox, FieldView,
CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, PresElementBox, QueryBox,
- ColorBox, DashWebRTCVideo, DocuLinkBox, InkingStroke, DocumentBox, LinkBox
+ ColorBox, DashWebRTCVideo, DocuLinkBox, InkingStroke, DocumentBox, LinkBox,
+ RecommendationsBox,
}}
bindings={this.CreateBindings()}
jsx={this.layout}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fb97c18c3..7d2940df5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,6 +1,6 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, observable, runInAction } from "mobx";
+import { action, computed, runInAction, trace, observable } from "mobx";
import { observer } from "mobx-react";
import * as rp from "request-promise";
import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
@@ -45,6 +45,12 @@ import { FormattedTextBox } from './FormattedTextBox';
import React = require("react");
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { CollectionStackingView } from '../collections/CollectionStackingView';
+import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField';
+import { ClientRecommender } from '../../ClientRecommender';
+import { SearchUtil } from '../../util/SearchUtil';
+import { RadialMenu } from './RadialMenu';
+import { KeyphraseQueryView } from '../KeyphraseQueryView';
library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight,
fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale,
@@ -59,6 +65,8 @@ export interface DocumentViewProps {
LibraryPath: Doc[];
fitToBox?: boolean;
onClick?: ScriptField;
+ onPointerDown?: ScriptField;
+ onPointerUp?: ScriptField;
dragDivName?: string;
addDocument?: (doc: Doc) => boolean;
removeDocument?: (doc: Doc) => boolean;
@@ -92,10 +100,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
private _doubleTap = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _dropDisposer?: DragManager.DragDropDisposer;
+ private _showKPQuery: boolean = false;
+ private _queries: string = "";
private _gestureEventDisposer?: GestureUtils.GestureEventDisposer;
private _titleRef = React.createRef<EditableView>();
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ private holdDisposer?: InteractionUtils.MultiTouchEventDisposer;
public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
public get ContentDiv() { return this._mainCont.current; }
@@ -104,72 +115,85 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get nativeWidth() { return this.layoutDoc._nativeWidth || 0; }
@computed get nativeHeight() { return this.layoutDoc._nativeHeight || 0; }
@computed get onClickHandler() { return this.props.onClick || this.layoutDoc.onClick || this.Document.onClick; }
+ @computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; }
+ @computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; }
- private _firstX: number = 0;
- private _firstY: number = 0;
-
-
- // handle1PointerHoldStart = (e: React.TouchEvent): any => {
- // this.onRadialMenu(e);
- // const pt = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0];
- // this._firstX = pt.pageX;
- // this._firstY = pt.pageY;
- // e.stopPropagation();
- // e.preventDefault();
-
- // document.removeEventListener("touchmove", this.onTouch);
- // document.removeEventListener("touchmove", this.handle1PointerHoldMove);
- // document.addEventListener("touchmove", this.handle1PointerHoldMove);
- // document.removeEventListener("touchend", this.handle1PointerHoldEnd);
- // document.addEventListener("touchend", this.handle1PointerHoldEnd);
- // }
-
- // handle1PointerHoldMove = (e: TouchEvent): void => {
- // const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
- // if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) {
- // this.handleRelease();
- // }
- // document.removeEventListener("touchmove", this.handle1PointerHoldMove);
- // document.addEventListener("touchmove", this.handle1PointerHoldMove);
- // document.removeEventListener("touchend", this.handle1PointerHoldEnd);
- // document.addEventListener("touchend", this.handle1PointerHoldEnd);
- // }
-
- // handleRelease() {
- // RadialMenu.Instance.closeMenu();
- // document.removeEventListener("touchmove", this.handle1PointerHoldMove);
- // document.removeEventListener("touchend", this.handle1PointerHoldEnd);
- // }
-
- // handle1PointerHoldEnd = (e: TouchEvent): void => {
- // RadialMenu.Instance.closeMenu();
- // document.removeEventListener("touchmove", this.handle1PointerHoldMove);
- // document.removeEventListener("touchend", this.handle1PointerHoldEnd);
- // }
-
- // @action
- // onRadialMenu = (e: React.TouchEvent): void => {
- // const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
-
- // RadialMenu.Instance.openMenu();
-
- // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight"), icon: "layer-group", selected: -1 });
- // RadialMenu.Instance.addItem({ description: "Delete this document", event: () => this.props.ContainingCollectionView?.removeDocument(this.props.Document), icon: "trash", selected: -1 });
- // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "folder", selected: -1 });
- // RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 });
-
- // RadialMenu.Instance.displayMenu(pt.pageX - 15, pt.pageY - 15);
- // if (!SelectionManager.IsSelected(this, true)) {
- // SelectionManager.SelectDoc(this, false);
- // }
- // e.stopPropagation();
- // }
+ private _firstX: number = -1;
+ private _firstY: number = -1;
+
+
+
+ handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
+ this.removeMoveListeners();
+ this.removeEndListeners();
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ console.log(SelectionManager.SelectedDocuments());
+ console.log("START");
+ if (RadialMenu.Instance._display === false) {
+ this.addHoldMoveListeners();
+ this.addHoldEndListeners();
+ this.onRadialMenu(e, me);
+ const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
+ this._firstX = pt.pageX;
+ this._firstY = pt.pageY;
+ }
+
+ }
+
+ handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
+
+ const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
+
+ if (this._firstX === -1 || this._firstY === -1) {
+ return;
+ }
+ if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) {
+ this.handle1PointerHoldEnd(e, me);
+ }
+ }
+
+ handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
+ this.removeHoldMoveListeners();
+ this.removeHoldEndListeners();
+ RadialMenu.Instance.closeMenu();
+ this._firstX = -1;
+ this._firstY = -1;
+ SelectionManager.DeselectAll();
+ me.touchEvent.stopPropagation();
+ me.touchEvent.preventDefault();
+ e.stopPropagation();
+ if (RadialMenu.Instance.used) {
+ this.onContextMenu(me.touches[0]);
+ }
+ }
+
+ @action
+ onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
+ // console.log(InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true));
+ // const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
+ const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
+ RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15);
+
+ RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Delete this document", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "layer-group", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "folder", selected: -1 });
+
+ // if (SelectionManager.IsSelected(this, true)) {
+ // SelectionManager.SelectDoc(this, false);
+ // }
+ SelectionManager.DeselectAll();
+
+
+ }
@action
componentDidMount() {
this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this)));
this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this)));
this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)));
+ // this._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this)));
!this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.push(this);
}
@@ -179,16 +203,24 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._dropDisposer && this._dropDisposer();
this._gestureEventDisposer && this._gestureEventDisposer();
this.multiTouchDisposer && this.multiTouchDisposer();
+ this.holdDisposer && this.holdDisposer();
this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this)));
this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this)));
this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)));
+ this._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this)));
}
@action
componentWillUnmount() {
this._dropDisposer && this._dropDisposer();
+ this._gestureEventDisposer && this._gestureEventDisposer();
+ this.multiTouchDisposer && this.multiTouchDisposer();
+ this.holdDisposer && this.holdDisposer();
Doc.UnBrushDoc(this.props.Document);
- !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
+ if (!this.props.dontRegisterView) {
+ const index = DocumentManager.Instance.DocumentViews.indexOf(this);
+ index !== -1 && DocumentManager.Instance.DocumentViews.splice(index, 1);
+ }
}
startDragging(x: number, y: number, dropAction: dropActionType) {
@@ -284,9 +316,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
+ SelectionManager.DeselectAll();
if (this.Document.onPointerDown) return;
- const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
- console.log("down");
+ const touch = me.touchEvent.changedTouches.item(0);
if (touch) {
this._downX = touch.clientX;
this._downY = touch.clientY;
@@ -307,8 +339,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.removeMoveListeners();
}
else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) {
- const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
- if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) {
+
+ const touch = me.touchEvent.changedTouches.item(0);
+ if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) {
if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) {
this.cleanUpInteractions();
this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined);
@@ -404,6 +437,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
e.stopPropagation();
+ // TODO: check here for panning/inking
}
return;
}
@@ -447,6 +481,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
onPointerUp = (e: PointerEvent): void => {
+ this.cleanUpInteractions();
+
+ if (this.onPointerUpHandler && this.onPointerUpHandler.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+ this.onPointerUpHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ return;
+ }
+
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
@@ -476,28 +518,20 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (doc[customName] === undefined) {
const _width = NumCast(doc._width);
const _height = NumCast(doc._height);
- const options = { title: "data", _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
+ const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
- const field = doc.data;
let fieldTemplate: Opt<Doc>;
- if (field instanceof RichTextField || typeof (field) === "string") {
+ if (doc.data instanceof RichTextField || typeof (doc.data) === "string") {
fieldTemplate = Docs.Create.TextDocument("", options);
- } else if (field instanceof PdfField) {
+ } else if (doc.data instanceof PdfField) {
fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
- } else if (field instanceof VideoField) {
+ } else if (doc.data instanceof VideoField) {
fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
- } else if (field instanceof AudioField) {
+ } else if (doc.data instanceof AudioField) {
fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
- } else if (field instanceof ImageField) {
+ } else if (doc.data instanceof ImageField) {
fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
}
-
- if (fieldTemplate) {
- fieldTemplate.backgroundColor = doc.backgroundColor;
- fieldTemplate.heading = 1;
- fieldTemplate._autoHeight = true;
- }
-
const docTemplate = docLayoutTemplate || creator(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate));
@@ -527,8 +561,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.stopPropagation();
de.complete.annoDragData.linkedToDoc = true;
- DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc },
- `Link from ${StrCast(de.complete.annoDragData.annotationDocument.title)}`);
+ DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, "link");
+ }
+ if (de.complete.docDragData) {
+ if (de.complete.docDragData.applyAsTemplate) {
+ Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layout_custom", undefined);
+ e.stopPropagation();
+ }
+ else if (de.complete.docDragData.draggedDocuments[0].type === "text") {
+ const text = Cast(de.complete.docDragData.draggedDocuments[0].data, RichTextField)?.Text;
+ if (text && text[0] === "{" && text[text.length - 1] === "}" && text.includes(":")) {
+ let loc = text.indexOf(":");
+ let key = text.slice(1, loc);
+ let value = text.slice(loc + 1, text.length - 1);
+ console.log(key);
+ console.log(value);
+ console.log(this.props.Document);
+ this.props.Document[key] = value;
+ console.log(de.complete.docDragData.draggedDocuments[0].x);
+ console.log(de.complete.docDragData.draggedDocuments[0].x);
+ e.preventDefault();
+ e.stopPropagation();
+ de.complete.aborted = true;
+ }
+ }
}
if (de.complete.linkDragData) {
e.stopPropagation();
@@ -536,7 +592,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
// const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView);
de.complete.linkDragData.linkSourceDocument !== this.props.Document &&
(de.complete.linkDragData.linkDocument = DocUtils.MakeLink({ doc: de.complete.linkDragData.linkSourceDocument },
- { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `link from ${de.complete.linkDragData.linkSourceDocument.title} to ${this.props.Document.title}`)); // TODODO this is where in text links get passed
+ { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `link`)); // TODODO this is where in text links get passed
}
}
@@ -562,7 +618,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const portalLink = DocListCast(this.Document.links).find(d => d.anchor1 === this.props.Document);
if (!portalLink) {
const portal = Docs.Create.FreeformDocument([], { _width: (this.layoutDoc._width || 0) + 10, _height: this.layoutDoc._height || 0, title: StrCast(this.props.Document.title) + ".portal" });
- DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, "portal link", "portal link");
+ DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, "portal to");
}
this.Document.isButton = true;
}
@@ -618,7 +674,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return;
}
e.persist();
- e.stopPropagation();
+ e?.stopPropagation();
if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 ||
e.isDefaultPrevented()) {
e.preventDefault();
@@ -700,6 +756,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
// a.download = `DocExport-${this.props.Document[Id]}.zip`;
// a.click();
});
+ let recommender_subitems: ContextMenuProps[] = [];
+
+ recommender_subitems.push({
+ description: "Internal recommendations",
+ event: () => this.recommender(e),
+ icon: "brain"
+ });
+
+ let ext_recommender_subitems: ContextMenuProps[] = [];
+
+ ext_recommender_subitems.push({
+ description: "arXiv",
+ event: () => this.externalRecommendation("arxiv"),
+ icon: "brain"
+ });
+ ext_recommender_subitems.push({
+ description: "Bing",
+ event: () => this.externalRecommendation("bing"),
+ icon: "brain"
+ });
+
+ recommender_subitems.push({
+ description: "External recommendations",
+ subitems: ext_recommender_subitems,
+ icon: "brain"
+ });
+
+ cm.addItem({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" });
+
moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" });
moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" });
@@ -754,6 +839,104 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
}
+ recommender = async (e: React.MouseEvent) => {
+ if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" });
+ const documents: Doc[] = [];
+ const allDocs = await SearchUtil.GetAllDocs();
+ // allDocs.forEach(doc => console.log(doc.title));
+ // clears internal representation of documents as vectors
+ ClientRecommender.Instance.reset_docs();
+ //ClientRecommender.Instance.arxivrequest("electrons");
+ await Promise.all(allDocs.map((doc: Doc) => {
+ let isMainDoc: boolean = false;
+ const dataDoc = Doc.GetProto(doc);
+ if (doc.type === DocumentType.TEXT) {
+ if (dataDoc === Doc.GetProto(this.props.Document)) {
+ isMainDoc = true;
+ }
+ if (!documents.includes(dataDoc)) {
+ documents.push(dataDoc);
+ const extdoc = doc.data_ext as Doc;
+ return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, "", isMainDoc);
+ }
+ }
+ if (doc.type === DocumentType.IMG) {
+ if (dataDoc === Doc.GetProto(this.props.Document)) {
+ isMainDoc = true;
+ }
+ if (!documents.includes(dataDoc)) {
+ documents.push(dataDoc);
+ const extdoc = doc.data_ext as Doc;
+ return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, "", isMainDoc, true);
+ }
+ }
+ }));
+ const doclist = ClientRecommender.Instance.computeSimilarities("cosine");
+ let recDocs: { preview: Doc, score: number }[] = [];
+ // tslint:disable-next-line: prefer-for-of
+ for (let i = 0; i < doclist.length; i++) {
+ recDocs.push({ preview: doclist[i].actualDoc, score: doclist[i].score });
+ }
+
+ const data = recDocs.map(unit => {
+ unit.preview.score = unit.score;
+ return unit.preview;
+ });
+
+ console.log(recDocs.map(doc => doc.score));
+
+ const title = `Showing ${data.length} recommendations for "${StrCast(this.props.Document.title)}"`;
+ const recommendations = Docs.Create.RecommendationsDocument(data, { title });
+ recommendations.documentIconHeight = 150;
+ recommendations.sourceDoc = this.props.Document;
+ recommendations.sourceDocContext = this.props.ContainingCollectionView!.props.Document;
+ CollectionDockingView.AddRightSplit(recommendations, undefined);
+
+ // RecommendationsBox.Instance.displayRecommendations(e.pageX + 100, e.pageY);
+ }
+
+ @action
+ externalRecommendation = async (api: string) => {
+ if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" });
+ ClientRecommender.Instance.reset_docs();
+ const doc = Doc.GetDataDoc(this.props.Document);
+ const extdoc = doc.data_ext as Doc;
+ const recs_and_kps = await ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, false, api);
+ let recs: any;
+ let kps: any;
+ if (recs_and_kps) {
+ recs = recs_and_kps.recs;
+ kps = recs_and_kps.keyterms;
+ }
+ else {
+ console.log("recommender system failed :(");
+ return;
+ }
+ console.log("ibm keyterms: ", kps.toString());
+ const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("href")];
+ const bodies: Doc[] = [];
+ const titles = recs.title_vals;
+ const urls = recs.url_vals;
+ for (let i = 0; i < 5; i++) {
+ const body = Docs.Create.FreeformDocument([], { title: titles[i] });
+ body.href = urls[i];
+ bodies.push(body);
+ }
+ CollectionDockingView.AddRightSplit(Docs.Create.SchemaDocument(headers, bodies, { title: `Showing External Recommendations for "${StrCast(doc.title)}"` }), undefined);
+ this._showKPQuery = true;
+ this._queries = kps.toString();
+ }
+
+ onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); };
+ onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); };
+
+ // does Document set a layout prop
+ // does Document set a layout prop
+ setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)];
+ // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise.
+ getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]);
+ getLayoutPropNum = (prop: string) => NumCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]);
+
isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction);
select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
@@ -920,7 +1103,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"];
let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear;
highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
- return <div id={this.props.Document[Id]} className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown}
+ return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown}
onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
style={{
@@ -944,11 +1127,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
</> :
this.innards}
</div>;
+ { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined }
}
}
-Scripting.addGlobal(function toggleDetail(doc: any, layoutKey: string) {
+Scripting.addGlobal(function toggleDetail(doc: any, layoutKey: string, otherKey: string="layout") {
const dv = DocumentManager.Instance.getDocumentView(doc);
- if (dv?.props.Document.layoutKey === layoutKey) dv?.switchViews(false, "");
+ if (dv?.props.Document.layoutKey === layoutKey) dv?.switchViews(otherKey !== "layout", otherKey.replace("layout_", ""));
else dv?.switchViews(true, layoutKey.replace("layout_", ""));
}); \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index ad804209b..f18183600 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -20,7 +20,7 @@ import { InkTool } from '../../../new_fields/InkField';
import { RichTextField } from "../../../new_fields/RichTextField";
import { RichTextUtils } from '../../../new_fields/RichTextUtils';
import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
import { TraceMobx } from '../../../new_fields/util';
import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, Utils } from '../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
@@ -82,6 +82,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
private _lastY = 0;
private _undoTyping?: UndoManager.Batch;
private _searchReactionDisposer?: Lambda;
+ private _recordReactionDisposer: Opt<IReactionDisposer>;
private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>;
private _reactionDisposer: Opt<IReactionDisposer>;
private _heightReactionDisposer: Opt<IReactionDisposer>;
@@ -92,6 +93,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
private _scrollDisposer: Opt<IReactionDisposer>;
private dropDisposer?: DragManager.DragDropDisposer;
+ @computed get _recording() { return this.dataDoc.audioState === "recording"; }
+ set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; }
+
@observable private _entered = false;
public static FocusedBox: FormattedTextBox | undefined;
@@ -150,7 +154,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500 }, value);
DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
- else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id);
+ else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "link to named target", id);
});
});
});
@@ -186,7 +190,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
(tx.storedMarks && !this._editorView.state.storedMarks) && (this._editorView.state.storedMarks = tx.storedMarks);
const tsel = this._editorView.state.selection.$from;
- tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000)));
+ tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000)));
const curText = state.doc.textBetween(0, state.doc.content.size, "\n\n");
const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField);
if (!this._applyingChange) {
@@ -207,7 +211,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
updateTitle = () => {
- if ((this.props.Document.isTemplateForField === "data" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing
+ if ((this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing
StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) {
const str = this._editorView.state.doc.textContent;
const titlestr = str.substr(0, Math.min(40, str.length));
@@ -387,7 +391,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
!this.props.Document.expandedTemplate && funcs.push({ description: "Make Template", event: () => { this.props.Document.isTemplateDoc = true; Doc.AddDocToList(Cast(Doc.UserDoc().noteTypes, Doc, null), "data", this.props.Document); }, icon: "eye" });
funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" });
funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" });
- funcs.push({ description: "Record Bullet", event: () => this.recordBullet(), icon: "expand-arrows-alt" });
+ funcs.push({ description: "Toggle Audio", event: () => this.props.Document._showAudio = !this.props.Document._showAudio, icon: "expand-arrows-alt" });
funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" });
["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
funcs.push({
@@ -405,12 +409,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" });
}
- @observable _recording = false;
-
recordDictation = () => {
- //this._editorView!.focus();
- if (this._recording) return;
- runInAction(() => this._recording = true);
DictationManager.Controls.listen({
interimHandler: this.setCurrentBulletContent,
continuous: { indefinite: false },
@@ -418,13 +417,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
if (results && [DictationManager.Controls.Infringed].includes(results)) {
DictationManager.Controls.stop();
}
- this._editorView!.focus();
+ //this._editorView!.focus();
});
}
- stopDictation = (abort: boolean) => {
- runInAction(() => this._recording = false);
- DictationManager.Controls.stop(!abort);
- }
+ stopDictation = (abort: boolean) => { DictationManager.Controls.stop(!abort); }
@action
toggleMenubar = () => {
@@ -449,12 +445,28 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
setCurrentBulletContent = (value: string) => {
if (this._editorView) {
let state = this._editorView.state;
+ let now = Date.now();
+ if (NumCast(this.props.Document.recordingStart, -1) === 0) {
+ this.props.Document.recordingStart = now = AudioBox.START;
+ }
+ console.log("NOW = " + (now - AudioBox.START) / 1000);
+ let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) });
+ if (!this._break && state.selection.to !== state.selection.from) {
+ for (let i = state.selection.from; i <= state.selection.to; i++) {
+ const pos = state.doc.resolve(i);
+ const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark);
+ if (um) {
+ mark = um;
+ break;
+ }
+ }
+ }
+ this._break = false;
+ console.log("start = " + (mark.attrs.modified * 1000 - AudioBox.START) / 1000);
+ value = "" + (mark.attrs.modified * 1000 - AudioBox.START) / 1000 + value;
const from = state.selection.from;
- const to = state.selection.to;
- this._editorView.dispatch(state.tr.insertText(value, from, to));
- state = this._editorView.state;
- const updated = TextSelection.create(state.doc, from, from + value.length);
- this._editorView.dispatch(state.tr.setSelection(updated));
+ const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark);
+ this._editorView.dispatch(inserted.setSelection(TextSelection.create(inserted.doc, from, from + value.length + 1)));
}
}
@@ -558,6 +570,17 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(),
{ fireImmediately: true });
+ this._recordReactionDisposer = reaction(() => this._recording,
+ () => {
+ if (this._recording) {
+ setTimeout(action(() => {
+ this.stopDictation(true);
+ setTimeout(() => this.recordDictation(), 500);
+ }), 500);
+ } else setTimeout(() => this.stopDictation(true), 0);
+ }
+ );
+
this._scrollToRegionReactionDisposer = reaction(
() => StrCast(this.layoutDoc.scrollToLinkID),
async (scrollToLinkID) => {
@@ -723,7 +746,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
targetAnnotations?.push(pdfRegion);
});
- const link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link");
+ const link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "PDF pasted");
if (link) {
cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
const linkId = link[Id];
@@ -807,7 +830,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
(selectOnLoad /* || !rtfField?.Text*/) && this._editorView!.focus();
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
- this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })];
+ this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })];
}
getFont(font: string) {
switch (font) {
@@ -831,6 +854,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
this._pullReactionDisposer?.();
this._heightReactionDisposer?.();
this._searchReactionDisposer?.();
+ this._recordReactionDisposer?.();
this._buttonBarReactionDisposer?.();
this._editorView?.destroy();
}
@@ -838,7 +862,19 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
static _downEvent: any;
_downX = 0;
_downY = 0;
+ _break = false;
onPointerDown = (e: React.PointerEvent): void => {
+ if (this._recording && !e.ctrlKey && e.button === 0) {
+ this.stopDictation(true);
+ this._break = true;
+ let state = this._editorView!.state;
+ const to = state.selection.to;
+ const updated = TextSelection.create(state.doc, to, to);
+ this._editorView!.dispatch(this._editorView!.state.tr.setSelection(updated).insertText("\n", to));
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._recording) setTimeout(() => this.recordDictation(), 500);
+ }
this._downX = e.clientX;
this._downY = e.clientY;
this.doLinkOnDeselect();
@@ -955,7 +991,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
this.props.select(e.ctrlKey);
this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false);
}
- if (this._recording) setTimeout(() => { this.stopDictation(true); setTimeout(() => this.recordDictation(), 500); }, 500);
}
// this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them.
@@ -1016,7 +1051,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
richTextMenuPlugin() {
return new Plugin({
view(newView) {
- RichTextMenu.Instance.changeView(newView);
+ RichTextMenu.Instance && RichTextMenu.Instance.changeView(newView);
return RichTextMenu.Instance;
}
});
@@ -1062,17 +1097,13 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
if (e.key === "Tab" || e.key === "Enter") {
e.preventDefault();
}
- const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) });
+ const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
this._lastTimedMark = mark;
this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
if (!this._undoTyping) {
this._undoTyping = UndoManager.StartBatch("undoTyping");
}
- if (this._recording) {
- this.stopDictation(true);
- setTimeout(() => this.recordDictation(), 250);
- }
}
onscrolled = (ev: React.UIEvent) => {
@@ -1169,8 +1200,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
</div>}
{!this.props.Document._showAudio ? (null) :
<div className="formattedTextBox-dictation"
- onClick={e => {
- this._recording ? this.stopDictation(true) : this.recordDictation();
+ onPointerDown={e => {
+ runInAction(() => this._recording = !this._recording);
setTimeout(() => this._editorView!.focus(), 500);
e.stopPropagation();
}} >
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index c46191270..e5848614c 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,6 +1,6 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEye } from '@fortawesome/free-regular-svg-icons';
-import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons';
+import { faAsterisk, faFileAudio, faImage, faPaintBrush, faBrain } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, runInAction, trace } from 'mobx';
import { observer } from "mobx-react";
@@ -22,6 +22,8 @@ import FaceRectangles from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
+import { SearchUtil } from '../../util/SearchUtil';
+import { ClientRecommender } from '../../ClientRecommender';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { documentSchema } from '../../../new_fields/documentSchemas';
import { Id, Copy } from '../../../new_fields/FieldSymbols';
@@ -35,7 +37,7 @@ const path = require('path');
const { Howl } = require('howler');
-library.add(faImage, faEye as any, faPaintBrush);
+library.add(faImage, faEye as any, faPaintBrush, faBrain);
library.add(faFileAudio, faAsterisk);
@@ -178,6 +180,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" });
modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" });
+ //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" });
!existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" });
ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" });
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 593f40f10..f47620c24 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -56,7 +56,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
const backup = "oldPath";
const { Document } = this.props;
- const { url: { href } } = Cast(Document[this.props.fieldKey], PdfField)!;
+ const { url: { href } } = Cast(this.dataDoc[this.props.fieldKey], PdfField)!;
const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g;
const matches = pathCorrectionTest.exec(href);
console.log("\nHere's the { url } being fed into the outer regex:");
@@ -78,9 +78,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
}
}
- componentWillUnmount() {
- this._selectReactionDisposer && this._selectReactionDisposer();
- }
+ componentWillUnmount() { this._selectReactionDisposer?.(); }
componentDidMount() {
this._selectReactionDisposer = reaction(() => this.props.isSelected(),
() => {
@@ -96,11 +94,11 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
!this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
}
- public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); }
- public prevAnnotation() { this._pdfViewer && this._pdfViewer.prevAnnotation(); }
- public nextAnnotation() { this._pdfViewer && this._pdfViewer.nextAnnotation(); }
- public backPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); }
- public forwardPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); }
+ public search = (string: string, fwd: boolean) => { this._pdfViewer?.search(string, fwd); }
+ public prevAnnotation = () => { this._pdfViewer?.prevAnnotation(); }
+ public nextAnnotation = () => { this._pdfViewer?.nextAnnotation(); }
+ public backPage = () => { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); }
+ public forwardPage = () => { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); }
public gotoPage = (p: number) => { this._pdfViewer!.gotoPage(p); };
@undoBatch
@@ -233,7 +231,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
isChildActive = (outsideReaction?: boolean) => this._isChildActive;
@computed get renderPdfView() {
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
- return <div className={"pdfBox"} onContextMenu={this.specificContextMenu}>
+ return <div className={"pdfBox"} onContextMenu={this.specificContextMenu} style={{ height: this.props.Document._scrollTop ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}>
<PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded}
setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView}
renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth}
@@ -243,7 +241,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
isChildActive={this.isChildActive}
- fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 ? true : false} />
+ fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 || this.props.Document._scrollTop ? true : false} />
{this.settingsPanel()}
</div>;
}
@@ -252,7 +250,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
render() {
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null);
if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
- if (pdfUrl && (this._everActive || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) {
+ if (pdfUrl && (this._everActive || this.props.Document._scrollTop || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) {
if (pdfUrl instanceof PdfField && this._pdf) {
return this.renderPdfView;
}
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 4180ee255..d43df0bfb 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -1,6 +1,6 @@
import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faHandPointLeft, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
@@ -24,6 +24,7 @@ library.add(faArrowLeft);
library.add(faArrowRight);
library.add(faPlay);
library.add(faStop);
+library.add(faHandPointLeft);
library.add(faPlus);
library.add(faTimes);
library.add(faMinus);
@@ -340,7 +341,7 @@ export class PresBox extends React.Component<FieldViewProps> {
const funcs: ContextMenuProps[] = [];
funcs.push({ description: "Show as Slideshow", event: action(() => this.props.Document._viewType = CollectionViewType.Carousel), icon: "asterisk" });
funcs.push({ description: "Show as Timeline", event: action(() => this.props.Document._viewType = CollectionViewType.Time), icon: "asterisk" });
- funcs.push({ description: "Show as List", event: action(() => this.props.Document._viewType = CollectionViewType.Invalid), icon: "asterisk" });
+ funcs.push({ description: "Show as List", event: action(() => { this.props.Document._viewType = CollectionViewType.Stacking; this.props.Document._pivotField = undefined; }), icon: "asterisk" });
ContextMenu.Instance.addItem({ description: "Presentation Funcs...", subitems: funcs, icon: "asterisk" });
}
@@ -377,6 +378,7 @@ export class PresBox extends React.Component<FieldViewProps> {
viewChanged = action((e: React.ChangeEvent) => {
//@ts-ignore
this.props.Document._viewType = Number(e.target.selectedOptions[0].value);
+ this.props.Document._viewType === CollectionViewType.Stacking && (this.props.Document._pivotField = undefined);
this.updateMinimize(e, Number(this.props.Document._viewType));
});
diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx
index 9314a3899..0ffed78de 100644
--- a/src/client/views/nodes/RadialMenu.tsx
+++ b/src/client/views/nodes/RadialMenu.tsx
@@ -5,6 +5,8 @@ import { RadialMenuItem, RadialMenuProps } from "./RadialMenuItem";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Measure from "react-measure";
import "./RadialMenu.scss";
+import MobileInkOverlay from "../../../mobile/MobileInkOverlay";
+import MobileInterface from "../../../mobile/MobileInterface";
@observer
export class RadialMenu extends React.Component {
@@ -23,13 +25,23 @@ export class RadialMenu extends React.Component {
@observable private _mouseDown: boolean = false;
private _reactionDisposer?: IReactionDisposer;
+ public used: boolean = false;
+
+
+ catchTouch = (te: React.TouchEvent) => {
+ console.log("caught");
+ te.stopPropagation();
+ te.preventDefault();
+ }
@action
onPointerDown = (e: PointerEvent) => {
this._mouseDown = true;
this._mouseX = e.clientX;
this._mouseY = e.clientY;
+ this.used = false;
document.addEventListener("pointermove", this.onPointerMove);
+
}
@observable
@@ -42,7 +54,6 @@ export class RadialMenu extends React.Component {
const deltX = this._mouseX - curX;
const deltY = this._mouseY - curY;
const scale = Math.hypot(deltY, deltX);
-
if (scale < 150 && scale > 50) {
const rad = Math.atan2(deltY, deltX) + Math.PI;
let closest = 0;
@@ -62,6 +73,7 @@ export class RadialMenu extends React.Component {
}
@action
onPointerUp = (e: PointerEvent) => {
+ this.used = true;
this._mouseDown = false;
const curX = e.clientX;
const curY = e.clientY;
@@ -83,6 +95,7 @@ export class RadialMenu extends React.Component {
@action
componentDidMount = () => {
+ console.log(this._pageX);
document.addEventListener("pointerdown", this.onPointerDown);
document.addEventListener("pointerup", this.onPointerUp);
this.previewcircle();
@@ -98,7 +111,7 @@ export class RadialMenu extends React.Component {
@observable private _pageX: number = 0;
@observable private _pageY: number = 0;
- @observable private _display: boolean = false;
+ @observable _display: boolean = false;
@observable private _yRelativeToTop: boolean = true;
@@ -124,35 +137,34 @@ export class RadialMenu extends React.Component {
displayMenu = (x: number, y: number) => {
//maxX and maxY will change if the UI/font size changes, but will work for any amount
//of items added to the menu
-
- this._pageX = x;
- this._pageY = y;
+ this._mouseX = x;
+ this._mouseY = y;
this._shouldDisplay = true;
}
-
- get pageX() {
- const x = this._pageX;
- if (x < 0) {
- return 0;
- }
- const width = this._width;
- if (x + width > window.innerWidth - RadialMenu.buffer) {
- return window.innerWidth - RadialMenu.buffer - width;
- }
- return x;
- }
-
- get pageY() {
- const y = this._pageY;
- if (y < 0) {
- return 0;
- }
- const height = this._height;
- if (y + height > window.innerHeight - RadialMenu.buffer) {
- return window.innerHeight - RadialMenu.buffer - height;
- }
- return y;
- }
+ // @computed
+ // get pageX() {
+ // const x = this._pageX;
+ // if (x < 0) {
+ // return 0;
+ // }
+ // const width = this._width;
+ // if (x + width > window.innerWidth - RadialMenu.buffer) {
+ // return window.innerWidth - RadialMenu.buffer - width;
+ // }
+ // return x;
+ // }
+ // @computed
+ // get pageY() {
+ // const y = this._pageY;
+ // if (y < 0) {
+ // return 0;
+ // }
+ // const height = this._height;
+ // if (y + height > window.innerHeight - RadialMenu.buffer) {
+ // return window.innerHeight - RadialMenu.buffer - height;
+ // }
+ // return y;
+ // }
@computed get menuItems() {
return this._items.map((item, index) => <RadialMenuItem {...item} key={item.description} closeMenu={this.closeMenu} max={this._items.length} min={index} selected={this._closest} />);
@@ -166,7 +178,10 @@ export class RadialMenu extends React.Component {
}
@action
- openMenu = () => {
+ openMenu = (x: number, y: number) => {
+
+ this._pageX = x;
+ this._pageY = y;
this._shouldDisplay;
this._display = true;
}
@@ -204,15 +219,15 @@ export class RadialMenu extends React.Component {
render() {
- if (!this._display) {
+ if (!this._display || MobileInterface.Instance) {
return null;
}
- const style = this._yRelativeToTop ? { left: this._mouseX - 150, top: this._mouseY - 150 } :
- { left: this._mouseX - 150, top: this._mouseY - 150 };
+ const style = this._yRelativeToTop ? { left: this._pageX - 130, top: this._pageY - 130 } :
+ { left: this._pageX - 130, top: this._pageY - 130 };
return (
- <div className="radialMenu-cont" style={style}>
+ <div className="radialMenu-cont" onTouchStart={this.catchTouch} style={style}>
<canvas id="newCanvas" style={{ position: "absolute" }} height="300" width="300"> Your browser does not support the HTML5 canvas tag.</canvas>
{this.menuItems}
</div>
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 69c6f2617..439f2d85f 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -134,7 +134,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
});
imageSummary.isButton = true;
this.props.addDocument && this.props.addDocument(imageSummary);
- DocUtils.MakeLink({ doc: imageSummary }, { doc: this.props.Document }, "snapshot from " + this.Document.title, "video frame snapshot");
+ DocUtils.MakeLink({ doc: imageSummary }, { doc: this.props.Document }, "video snapshot");
}
});
}
@@ -261,7 +261,6 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
}
private get uIButtons() {
- const scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
const curTime = (this.Document.currentTimecode || 0);
return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} >
<span>{"" + Math.round(curTime)}</span>
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index fbe9bf063..226103a53 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -90,4 +90,19 @@
width: 100%;
margin-right: 10px;
height: 100%;
+}
+
+.touch-iframe-overlay {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ pointer-events: all;
+
+ .indicator {
+ position: absolute;
+
+ &.active {
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 4413ba020..2f8b6167f 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -21,6 +21,10 @@ import "./WebBox.scss";
import React = require("react");
import { DocAnnotatableComponent } from "../DocComponent";
import { documentSchema } from "../../../new_fields/documentSchemas";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { DragManager } from "../../util/DragManager";
+import { ImageUtils } from "../../util/Import & Export/ImageUtils";
+import { select } from "async";
library.add(faStickyNote);
@@ -34,6 +38,13 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
@observable private collapsed: boolean = true;
@observable private url: string = "hello";
+ private _longPressSecondsHack?: NodeJS.Timeout;
+ private _iframeRef = React.createRef<HTMLIFrameElement>();
+ private _iframeIndicatorRef = React.createRef<HTMLDivElement>();
+ private _iframeDragRef = React.createRef<HTMLDivElement>();
+ @observable private _pressX: number = 0;
+ @observable private _pressY: number = 0;
+
componentDidMount() {
const field = Cast(this.props.Document[this.props.fieldKey], WebField);
@@ -49,6 +60,14 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
}
this.setURL();
+
+ document.addEventListener("pointerup", this.onLongPressUp);
+ document.addEventListener("pointermove", this.onLongPressMove);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("pointerup", this.onLongPressUp);
+ document.removeEventListener("pointermove", this.onLongPressMove);
}
@action
@@ -164,6 +183,107 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
}
}
+ onLongPressDown = (e: React.PointerEvent) => {
+ this._pressX = e.clientX;
+ this._pressY = e.clientY;
+
+ // find the pressed element in the iframe (currently only works if its an img)
+ let pressedElement: HTMLElement | undefined;
+ let pressedBound: ClientRect | undefined;
+ let selectedText: string = "";
+ let pressedImg: boolean = false;
+ if (this._iframeRef.current) {
+ const B = this._iframeRef.current.getBoundingClientRect();
+ const iframeDoc = this._iframeRef.current.contentDocument;
+ if (B && iframeDoc) {
+ // TODO: this only works when scale = 1 as it is currently only inteded for mobile upload
+ const element = iframeDoc.elementFromPoint(this._pressX - B.left, this._pressY - B.top);
+ if (element && element.nodeName === "IMG") {
+ pressedBound = element.getBoundingClientRect();
+ pressedElement = element.cloneNode(true) as HTMLElement;
+ pressedImg = true;
+ } else {
+ // check if there is selected text
+ const text = iframeDoc.getSelection();
+ if (text && text.toString().length > 0) {
+ selectedText = text.toString();
+
+ // get html of the selected text
+ const range = text.getRangeAt(0);
+ const contents = range.cloneContents();
+ const div = document.createElement("div");
+ div.appendChild(contents);
+ pressedElement = div;
+
+ pressedBound = range.getBoundingClientRect();
+ }
+ }
+ }
+ }
+
+ // mark the pressed element
+ if (pressedElement && pressedBound) {
+ if (this._iframeIndicatorRef.current) {
+ this._iframeIndicatorRef.current.style.top = pressedBound.top + "px";
+ this._iframeIndicatorRef.current.style.left = pressedBound.left + "px";
+ this._iframeIndicatorRef.current.style.width = pressedBound.width + "px";
+ this._iframeIndicatorRef.current.style.height = pressedBound.height + "px";
+ this._iframeIndicatorRef.current.classList.add("active");
+ }
+ }
+
+ // start dragging the pressed element if long pressed
+ this._longPressSecondsHack = setTimeout(() => {
+ if (pressedImg && pressedElement && pressedBound) {
+ e.stopPropagation();
+ e.preventDefault();
+ if (pressedElement.nodeName === "IMG") {
+ const src = pressedElement.getAttribute("src"); // TODO: may not always work
+ if (src) {
+ const doc = Docs.Create.ImageDocument(src);
+ ImageUtils.ExtractExif(doc);
+
+ // add clone to div so that dragging ghost is placed properly
+ if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement);
+
+ const dragData = new DragManager.DocumentDragData([doc]);
+ DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX, this._pressY, { hideSource: true });
+ }
+ }
+ } else if (selectedText && pressedBound && pressedElement) {
+ e.stopPropagation();
+ e.preventDefault();
+ // create doc with the selected text's html
+ const doc = Docs.Create.HtmlDocument(pressedElement.innerHTML);
+
+ // create dragging ghost with the selected text
+ if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement);
+
+ // start the drag
+ const dragData = new DragManager.DocumentDragData([doc]);
+ DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX - pressedBound.top, this._pressY - pressedBound.top, { hideSource: true });
+ }
+ }, 1500);
+ }
+
+ onLongPressMove = (e: PointerEvent) => {
+ // this._pressX = e.clientX;
+ // this._pressY = e.clientY;
+ }
+
+ onLongPressUp = (e: PointerEvent) => {
+ if (this._longPressSecondsHack) {
+ clearTimeout(this._longPressSecondsHack);
+ }
+ if (this._iframeIndicatorRef.current) {
+ this._iframeIndicatorRef.current.classList.remove("active");
+ }
+ if (this._iframeDragRef.current) {
+ while (this._iframeDragRef.current.firstChild) this._iframeDragRef.current.removeChild(this._iframeDragRef.current.firstChild);
+ }
+ }
+
+
@computed
get content() {
const field = this.dataDoc[this.props.fieldKey];
@@ -171,9 +291,9 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
if (field instanceof HtmlField) {
view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
} else if (field instanceof WebField) {
- view = <iframe src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
+ view = <iframe ref={this._iframeRef} src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
} else {
- view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
+ view = <iframe ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
}
const content =
<div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
@@ -181,15 +301,23 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
{view}
</div>;
- const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
+ const decInteracting = DocumentDecorations.Instance && DocumentDecorations.Instance.Interacting;
+
+ const frozen = !this.props.isSelected() || decInteracting;
- const classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+ const classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "");
return (
<>
<div className={classname} >
{content}
</div>
- {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
+ {!frozen ? (null) :
+ <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer}>
+ <div className="touch-iframe-overlay" onPointerDown={this.onLongPressDown} >
+ <div className="indicator" ref={this._iframeIndicatorRef}></div>
+ <div className="dragger" ref={this._iframeDragRef}></div>
+ </div>
+ </div>}
</>);
}
render() {
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 4f81c6f70..5cd2c4fe4 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -1,6 +1,5 @@
-.pdfViewer, .pdfViewer-zoomed {
- pointer-events: all;
+.pdfViewer, .pdfViewer-interactive {
width: 100%;
height: 100%;
position: absolute;
@@ -91,7 +90,8 @@
z-index: 10;
}
}
-.pdfViewer-zoomed {
- overflow-x: scroll;
+
+.pdfViewer-interactive {
+ pointer-events: all;
}
\ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 198aeb856..af06a2646 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -4,7 +4,7 @@ import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
import { Dictionary } from "typescript-collections";
import { Doc, DocListCast, FieldResult, WidthSym, Opt, HeightSym } from "../../../new_fields/Doc";
-import { Id } from "../../../new_fields/FieldSymbols";
+import { Id, Copy } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { makeInterface, createSchema } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
@@ -31,6 +31,8 @@ import { InkingControl } from "../InkingControl";
import { InkTool } from "../../../new_fields/InkField";
import { TraceMobx } from "../../../new_fields/util";
import { PdfField } from "../../../new_fields/URLField";
+import { PDFBox } from "../nodes/PDFBox";
+import { FormattedTextBox } from "../nodes/FormattedTextBox";
const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
const pdfjsLib = require("pdfjs-dist");
@@ -101,6 +103,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
private _reactionDisposer?: IReactionDisposer;
private _selectionReactionDisposer?: IReactionDisposer;
private _annotationReactionDisposer?: IReactionDisposer;
+ private _scrollTopReactionDisposer?: IReactionDisposer;
private _filterReactionDisposer?: IReactionDisposer;
private _searchReactionDisposer?: IReactionDisposer;
private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
@@ -126,7 +129,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
!this.props.Document.lockedTransform && (this.props.Document.lockedTransform = true);
// change the address to be the file address of the PNG version of each page
// file address of the pdf
- const { url: { href } } = Cast(this.props.Document[this.props.fieldKey], PdfField)!;
+ const { url: { href } } = Cast(this.dataDoc[this.props.fieldKey], PdfField)!;
const addr = Utils.prepend(`/thumbnail${this.props.url.substring("files/pdfs/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.png`);
this._coverPath = href.startsWith(window.location.origin) ? JSON.parse(await rp.get(addr)) : { width: 100, height: 100, path: "" };
runInAction(() => this._showWaiting = this._showCover = true);
@@ -162,10 +165,11 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
componentWillUnmount = () => {
this._reactionDisposer && this._reactionDisposer();
- this._annotationReactionDisposer && this._annotationReactionDisposer();
- this._filterReactionDisposer && this._filterReactionDisposer();
- this._selectionReactionDisposer && this._selectionReactionDisposer();
- this._searchReactionDisposer && this._searchReactionDisposer();
+ this._scrollTopReactionDisposer?.();
+ this._annotationReactionDisposer?.();
+ this._filterReactionDisposer?.();
+ this._selectionReactionDisposer?.();
+ this._searchReactionDisposer?.();
document.removeEventListener("copy", this.copy);
}
@@ -206,6 +210,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
this.props.setPdfViewer(this);
await this.initialLoad();
+ this._scrollTopReactionDisposer = reaction(() => Cast(this.props.Document._scrollTop, "number", null),
+ (stop) => (stop !== undefined) && this._mainCont.current && smoothScroll(500, this._mainCont.current, stop), { fireImmediately: true });
this._annotationReactionDisposer = reaction(
() => DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]),
annotations => annotations?.length && (this._annotations = annotations),
@@ -227,6 +233,12 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
this.createPdfViewer();
}
+ pagesinit = action(() => {
+ this._pdfViewer.currentScaleValue = this._zoomed = 1;
+ this.gotoPage(this.Document.curPage || 1);
+ document.removeEventListener("pagesinit", this.pagesinit);
+ })
+
createPdfViewer() {
if (!this._mainCont.current) { // bcz: I don't think this is ever triggered or needed
if (this._retries < 5) {
@@ -237,10 +249,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
}
document.removeEventListener("copy", this.copy);
document.addEventListener("copy", this.copy);
- document.addEventListener("pagesinit", action(() => {
- this._pdfViewer.currentScaleValue = this._zoomed = 1;
- this.gotoPage(this.Document.curPage || 1);
- }));
+ document.addEventListener("pagesinit", this.pagesinit);
document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
const pdfLinkService = new PDFJSViewer.PDFLinkService();
const pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService });
@@ -267,7 +276,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
let minY = Number.MAX_VALUE;
if ((this._savedAnnotations.values()[0][0] as any).marqueeing) {
const anno = this._savedAnnotations.values()[0][0];
- const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + this.Document.title });
+ const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, _LODdisable: true, title: "Annotation on " + this.Document.title });
if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
if (anno.style.height) annoDoc._height = parseInt(anno.style.height);
@@ -320,7 +329,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
@action
gotoPage = (p: number) => {
- this._pdfViewer && this._pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
+ this._pdfViewer?.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
}
@action
@@ -554,32 +563,33 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
* start a drag event and create or put the necessary info into the drag event.
*/
@action
- startDrag = (e: PointerEvent, ele: HTMLElement): void => {
+ startDrag = async (e: PointerEvent, ele: HTMLElement) => {
e.preventDefault();
e.stopPropagation();
+
+ const clipDoc = Docs.Create.PdfDocument(Cast(this.dataDoc[this.props.fieldKey], PdfField, null)?.url.href || "http://www.msn.com", { title: "snippetView", _fitWidth: true, _width: this.marqueeWidth(), _height: this.marqueeHeight(), _scrollTop: this.marqueeY() });
const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
+ Doc.GetProto(targetDoc).layout = FormattedTextBox.LayoutString("contents");
+ Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]);
+ Doc.GetProto(targetDoc).layout_slideView = (await Cast(Doc.UserDoc().slidesBtn, Doc))?.dragFactory;
+ targetDoc.layoutKey = "layout_slideView";
+ // const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
+ // Doc.GetProto(targetDoc).snipped = this.dataDoc[this.props.fieldKey][Copy]();
+ // const snipLayout = Docs.Create.PdfDocument("http://www.msn.com", { title: "snippetView", isTemplateDoc: true, isTemplateForField: "snipped", _fitWidth: true, _width: this.marqueeWidth(), _height: this.marqueeHeight(), _scrollTop: this.marqueeY() });
+ // Doc.GetProto(snipLayout).layout = PDFBox.LayoutString("snipped");
const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection
if (annotationDoc) {
DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, {
- dragComplete: e => !e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc &&
- DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument, ctx: e.annoDragData.targetContext }, `Annotation from ${this.Document.title}`, "link from PDF")
+ dragComplete: e => {
+ if (!e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc) {
+ const link = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument, ctx: e.annoDragData.targetContext }, "Annotation");
+ if (link) link.maximizeLocation = "onRight";
+ }
+ }
});
}
}
- createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
- const view = Doc.MakeAlias(this.props.Document);
- const data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));
- data.title = StrCast(data.title) + "_snippet";
- view.proto = data;
- view._nativeHeight = marquee.height;
- view._height = (this.Document[WidthSym]() / (this.Document._nativeWidth || 1)) * marquee.height;
- view._nativeWidth = this.Document._nativeWidth;
- view.startY = marquee.top;
- view._width = this.Document[WidthSym]();
- DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
- }
-
scrollXf = () => {
return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._scrollTop) : this.props.ScreenToLocalTransform();
}
@@ -643,6 +653,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
select={emptyFunction}
active={this.annotationsActive}
ContentScaling={this.contentZoom}
+ bringToFront={emptyFunction}
whenActiveChanged={this.whenActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
@@ -673,9 +684,10 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
contentZoom = () => this._zoomed;
render() {
TraceMobx();
- return <div className={"pdfViewer" + (this._zoomed !== 1 ? "-zoomed" : "")} ref={this._mainCont}
+ return <div className={"pdfViewer" + (this.active() ? "-interactive" : "")} ref={this._mainCont}
onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}
style={{
+ overflowX: this._zoomed !== 1 ? "scroll" : undefined,
width: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeWidth) : `${100 / this.contentScaling}%`,
height: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeHeight) : `${100 / this.contentScaling}%`,
transform: `scale(${this.props.ContentScaling()})`