From 146f8622d5bac2edc6b09f57c173bd057dfbcfad Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 8 Jul 2022 00:17:26 -0400 Subject: restructured currentUserUtils to avoid having import cycles. --- .../CollectionFreeFormLayoutEngines.tsx | 248 +++++----- .../CollectionFreeFormLinkView.tsx | 201 ++++---- .../CollectionFreeFormRemoteCursors.tsx | 105 +++-- .../collectionFreeForm/CollectionFreeFormView.tsx | 28 +- .../collections/collectionFreeForm/MarqueeView.tsx | 517 +++++++++++---------- 5 files changed, 572 insertions(+), 527 deletions(-) (limited to 'src/client/views/collections/collectionFreeForm') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 9de2cfcf9..a0ebe4cdc 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -1,13 +1,12 @@ -import { Doc, Field, FieldResult, HeightSym, WidthSym } from "../../../../fields/Doc"; -import { Id, ToString } from "../../../../fields/FieldSymbols"; -import { ObjectField } from "../../../../fields/ObjectField"; -import { RefField } from "../../../../fields/RefField"; -import { listSpec } from "../../../../fields/Schema"; -import { Cast, NumCast, StrCast } from "../../../../fields/Types"; -import { aggregateBounds } from "../../../../Utils"; -import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; -import React = require("react"); -import { ColorScheme } from "../../../util/SettingsManager"; +import { Doc, Field, FieldResult, HeightSym, WidthSym } from '../../../../fields/Doc'; +import { Id, ToString } from '../../../../fields/FieldSymbols'; +import { ObjectField } from '../../../../fields/ObjectField'; +import { RefField } from '../../../../fields/RefField'; +import { listSpec } from '../../../../fields/Schema'; +import { Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { aggregateBounds } from '../../../../Utils'; +import { ColorScheme } from '../../../util/SettingsManager'; +import React = require('react'); export interface ViewDefBounds { type: string; @@ -25,7 +24,7 @@ export interface ViewDefBounds { color?: string; opacity?: number; replica?: string; - pair?: { layout: Doc, data?: Doc }; + pair?: { layout: Doc; data?: Doc }; } export interface PoolData { @@ -40,7 +39,7 @@ export interface PoolData { transition?: string; highlight?: boolean; replica: string; - pair: { layout: Doc, data?: Doc }; + pair: { layout: Doc; data?: Doc }; } export interface ViewDefResult { @@ -48,7 +47,7 @@ export interface ViewDefResult { bounds?: ViewDefBounds; } function toLabel(target: FieldResult) { - if (typeof target === "number" || Number(target)) { + if (typeof target === 'number' || Number(target)) { const truncated = Number(Number(target).toFixed(0)); const precise = Number(Number(target).toFixed(2)); return truncated === precise ? Number(target).toFixed(0) : Number(target).toFixed(2); @@ -60,16 +59,16 @@ function toLabel(target: FieldResult) { } /** * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. - * + * * @param {String} text The text to be rendered. * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). - * + * * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 */ function getTextWidth(text: string, font: string): number { // re-use canvas object for better performance - const canvas = (getTextWidth as any).canvas || ((getTextWidth as any).canvas = document.createElement("canvas")); - const context = canvas.getContext("2d"); + const canvas = (getTextWidth as any).canvas || ((getTextWidth as any).canvas = document.createElement('canvas')); + const context = canvas.getContext('2d'); context.font = font; const metrics = context.measureText(text); return metrics.width; @@ -81,14 +80,7 @@ interface PivotColumn { filters: string[]; } -export function computerPassLayout( - poolData: Map, - pivotDoc: Doc, - childPairs: { layout: Doc, data?: Doc }[], - panelDim: number[], - viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], - engineProps: any -) { +export function computerPassLayout(poolData: Map, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { const docMap = new Map(); childPairs.forEach(({ layout, data }, i) => { docMap.set(layout[Id], { @@ -97,60 +89,49 @@ export function computerPassLayout( width: layout[WidthSym](), height: layout[HeightSym](), pair: { layout, data }, - replica: "" + replica: '', }); }); return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []); } -export function computerStarburstLayout( - poolData: Map, - pivotDoc: Doc, - childPairs: { layout: Doc, data?: Doc }[], - panelDim: number[], - viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], - engineProps: any -) { +export function computerStarburstLayout(poolData: Map, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { const mustFit = pivotDoc[WidthSym]() !== panelDim[0]; // if a panel size is set that's not the same as the pivot doc's size, then assume this is in a panel for a content fitting view (like a grid) in which case everything must be scaled to stay within the panel const docMap = new Map(); - const docSize = mustFit ? panelDim[0] * .33 : 75; // assume an icon sized at 75 + const docSize = mustFit ? panelDim[0] * 0.33 : 75; // assume an icon sized at 75 const burstRadius = mustFit ? panelDim : [NumCast(pivotDoc._starburstRadius, panelDim[0]) - docSize, NumCast(pivotDoc._starburstRadius, panelDim[1]) - docSize]; const scaleDim = [burstRadius[0] * 2 + docSize, burstRadius[1] * 2 + docSize]; childPairs.forEach(({ layout, data }, i) => { - const docSize = layout.layoutKey === "layout_icon" ? (mustFit ? panelDim[0] * .33 : 75) : 400; // assume a icon sized at 75 - const deg = i / childPairs.length * Math.PI * 2; + const docSize = layout.layoutKey === 'layout_icon' ? (mustFit ? panelDim[0] * 0.33 : 75) : 400; // assume a icon sized at 75 + const deg = (i / childPairs.length) * Math.PI * 2; docMap.set(layout[Id], { x: Math.cos(deg) * burstRadius[0] - docSize / 2, - y: Math.sin(deg) * burstRadius[1] - docSize * layout[HeightSym]() / layout[WidthSym]() / 2, - width: docSize,//layout[WidthSym](), - height: docSize * layout[HeightSym]() / layout[WidthSym](), + y: Math.sin(deg) * burstRadius[1] - (docSize * layout[HeightSym]()) / layout[WidthSym]() / 2, + width: docSize, //layout[WidthSym](), + height: (docSize * layout[HeightSym]()) / layout[WidthSym](), zIndex: NumCast(layout.zIndex), pair: { layout, data }, - replica: "" + replica: '', }); }); - const divider = { type: "div", color: "transparent", x: -burstRadius[0], y: 0, width: 15, height: 15, payload: undefined }; + const divider = { type: 'div', color: 'transparent', x: -burstRadius[0], y: 0, width: 15, height: 15, payload: undefined }; return normalizeResults(scaleDim, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]); } - -export function computePivotLayout( - poolData: Map, - pivotDoc: Doc, - childPairs: { layout: Doc, data?: Doc }[], - panelDim: number[], - viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], - engineProps: any -) { +export function computePivotLayout(poolData: Map, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { const docMap = new Map(); - const fieldKey = "data"; + const fieldKey = 'data'; const pivotColumnGroups = new Map, PivotColumn>(); let nonNumbers = 0; - const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField) || "author"; + const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField) || 'author'; childPairs.map(pair => { - const lval = pivotFieldKey === "#" || pivotFieldKey === "tags" ? Array.from(Object.keys(Doc.GetProto(pair.layout))).filter(k => k.startsWith("#")).map(k => k.substring(1)) : - Cast(pair.layout[pivotFieldKey], listSpec("string"), null); + const lval = + pivotFieldKey === '#' || pivotFieldKey === 'tags' + ? Array.from(Object.keys(Doc.GetProto(pair.layout))) + .filter(k => k.startsWith('#')) + .map(k => k.substring(1)) + : Cast(pair.layout[pivotFieldKey], listSpec('string'), null); const num = toNumber(pair.layout[pivotFieldKey]); if (num === undefined || Number.isNaN(num)) { @@ -166,7 +147,7 @@ export function computePivotLayout( } else if (val) { !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, { docs: [], filters: [val], replicas: [] }); pivotColumnGroups.get(val)!.docs.push(pair.layout); - pivotColumnGroups.get(val)!.replicas.push(""); + pivotColumnGroups.get(val)!.replicas.push(''); } else { docMap.set(pair.layout[Id], { x: 0, @@ -175,11 +156,11 @@ export function computePivotLayout( width: 0, height: 0, pair, - replica: "" + replica: '', }); } }); - const pivotNumbers = nonNumbers / childPairs.length < .1; + const pivotNumbers = nonNumbers / childPairs.length < 0.1; if (pivotColumnGroups.size > 10) { const arrayofKeys = Array.from(pivotColumnGroups.keys()); const sortedKeys = pivotNumbers ? arrayofKeys.sort((n1: FieldResult, n2: FieldResult) => toNumber(n1)! - toNumber(n2)!) : arrayofKeys.sort(); @@ -196,9 +177,11 @@ export function computePivotLayout( } } } - const fontSize = NumCast(pivotDoc[fieldKey + "-timelineFontSize"], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3)); + const fontSize = NumCast(pivotDoc[fieldKey + '-timelineFontSize'], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3)); const desc = `${fontSize}px ${getComputedStyle(document.body).fontFamily}`; - const textlen = Array.from(pivotColumnGroups.keys()).map(c => getTextWidth(toLabel(c), desc)).reduce((p, c) => Math.max(p, c), 0 as number); + const textlen = Array.from(pivotColumnGroups.keys()) + .map(c => getTextWidth(toLabel(c), desc)) + .reduce((p, c) => Math.max(p, c), 0 as number); const max_text = Math.min(Math.ceil(textlen / 120) * 28, panelDim[1] / 2); const maxInColumn = Array.from(pivotColumnGroups.values()).reduce((p, s) => Math.max(p, s.docs.length), 1); @@ -222,7 +205,7 @@ export function computePivotLayout( const groupNames: ViewDefBounds[] = []; const expander = 1.05; - const gap = .15; + const gap = 0.15; const maxColHeight = pivotAxisWidth * expander * Math.ceil(maxInColumn / numCols); let x = 0; const sortedPivotKeys = pivotNumbers ? Array.from(pivotColumnGroups.keys()).sort((n1: FieldResult, n2: FieldResult) => toNumber(n1)! - toNumber(n2)!) : Array.from(pivotColumnGroups.keys()).sort(); @@ -232,14 +215,14 @@ export function computePivotLayout( let xCount = 0; const text = toLabel(key); groupNames.push({ - type: "text", + type: 'text', text, x, y: pivotAxisWidth, width: pivotAxisWidth * expander * numCols, height: max_text, fontSize, - payload: val + payload: val, }); val.docs.forEach((doc, i) => { const layoutDoc = Doc.Layout(doc); @@ -249,13 +232,13 @@ export function computePivotLayout( hgt = pivotAxisWidth; wid = (Doc.NativeAspect(layoutDoc) || 1) * pivotAxisWidth; } - docMap.set(doc[Id] + (val.replicas || ""), { - x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.docs.length < numCols ? (numCols - val.docs.length) * pivotAxisWidth / 2 : 0), + docMap.set(doc[Id] + (val.replicas || ''), { + x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.docs.length < numCols ? ((numCols - val.docs.length) * pivotAxisWidth) / 2 : 0), y: -y + (pivotAxisWidth - hgt) / 2, width: wid, height: hgt, pair: { layout: doc }, - replica: val.replicas[i] + replica: val.replicas[i], }); xCount++; if (xCount >= numCols) { @@ -266,13 +249,14 @@ export function computePivotLayout( x += pivotAxisWidth * (numCols * expander + gap); }); - const dividers = sortedPivotKeys.map((key, i) => - ({ - type: "div", color: "lightGray", - x: i * pivotAxisWidth * (numCols * expander + gap) - pivotAxisWidth * (expander - 1) / 2, - y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander, + const dividers = sortedPivotKeys.map((key, i) => ({ + type: 'div', + color: 'lightGray', + x: i * pivotAxisWidth * (numCols * expander + gap) - (pivotAxisWidth * (expander - 1)) / 2, + y: -maxColHeight + pivotAxisWidth, + width: pivotAxisWidth * numCols * expander, height: maxColHeight, - payload: pivotColumnGroups.get(key)!.filters + payload: pivotColumnGroups.get(key)!.filters, })); groupNames.push(...dividers); return normalizeResults(panelDim, max_text, docMap, poolData, viewDefsToJSX, groupNames, 0, []); @@ -282,24 +266,17 @@ function toNumber(val: FieldResult) { return val === undefined ? undefined : NumCast(val, Number(StrCast(val))); } -export function computeTimelineLayout( - poolData: Map, - pivotDoc: Doc, - childPairs: { layout: Doc, data?: Doc }[], - panelDim: number[], - viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], - engineProps?: any -) { - const fieldKey = "data"; +export function computeTimelineLayout(poolData: Map, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps?: any) { + const fieldKey = 'data'; const pivotDateGroups = new Map(); const docMap = new Map(); const groupNames: ViewDefBounds[] = []; const timelineFieldKey = Field.toString(pivotDoc._pivotField as Field); - const curTime = toNumber(pivotDoc[fieldKey + "-timelineCur"]); - const curTimeSpan = Cast(pivotDoc[fieldKey + "-timelineSpan"], "number", null); - const minTimeReq = curTimeSpan === undefined ? Cast(pivotDoc[fieldKey + "-timelineMinReq"], "number", null) : curTime && (curTime - curTimeSpan); - const maxTimeReq = curTimeSpan === undefined ? Cast(pivotDoc[fieldKey + "-timelineMaxReq"], "number", null) : curTime && (curTime + curTimeSpan); - const fontSize = NumCast(pivotDoc[fieldKey + "-timelineFontSize"], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3)); + const curTime = toNumber(pivotDoc[fieldKey + '-timelineCur']); + const curTimeSpan = Cast(pivotDoc[fieldKey + '-timelineSpan'], 'number', null); + const minTimeReq = curTimeSpan === undefined ? Cast(pivotDoc[fieldKey + '-timelineMinReq'], 'number', null) : curTime && curTime - curTimeSpan; + const maxTimeReq = curTimeSpan === undefined ? Cast(pivotDoc[fieldKey + '-timelineMaxReq'], 'number', null) : curTime && curTime + curTimeSpan; + const fontSize = NumCast(pivotDoc[fieldKey + '-timelineFontSize'], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3)); const fontHeight = panelDim[1] > 58 ? 30 : panelDim[1] / 2; const findStack = (time: number, stack: number[]) => { const index = stack.findIndex(val => val === undefined || val < x); @@ -325,8 +302,8 @@ export function computeTimelineLayout( } } setTimeout(() => { - pivotDoc[fieldKey + "-timelineMin"] = minTime = minTimeReq ? Math.min(minTimeReq, minTime) : minTime; - pivotDoc[fieldKey + "-timelineMax"] = maxTime = maxTimeReq ? Math.max(maxTimeReq, maxTime) : maxTime; + pivotDoc[fieldKey + '-timelineMin'] = minTime = minTimeReq ? Math.min(minTimeReq, minTime) : minTime; + pivotDoc[fieldKey + '-timelineMax'] = maxTime = maxTimeReq ? Math.max(maxTimeReq, maxTime) : maxTime; }, 0); if (maxTime === minTime) { @@ -340,10 +317,10 @@ export function computeTimelineLayout( let prevKey = Math.floor(minTime); if (sortedKeys.length && scaling * (sortedKeys[0] - prevKey) > 25) { - groupNames.push({ type: "text", text: toLabel(prevKey), x: x, y: 0, height: fontHeight, fontSize, payload: undefined }); + groupNames.push({ type: 'text', text: toLabel(prevKey), x: x, y: 0, height: fontHeight, fontSize, payload: undefined }); } if (!sortedKeys.length && curTime !== undefined) { - groupNames.push({ type: "text", text: toLabel(curTime), x: (curTime - minTime) * scaling, zIndex: 1000, color: "orange", y: 0, height: fontHeight, fontSize, payload: undefined }); + groupNames.push({ type: 'text', text: toLabel(curTime), x: (curTime - minTime) * scaling, zIndex: 1000, color: 'orange', y: 0, height: fontHeight, fontSize, payload: undefined }); } const pivotAxisWidth = NumCast(pivotDoc.pivotTimeWidth, panelDim[1] / 2.5); @@ -351,26 +328,26 @@ export function computeTimelineLayout( let zind = 0; sortedKeys.forEach(key => { if (curTime !== undefined && curTime > prevKey && curTime <= key) { - groupNames.push({ type: "text", text: toLabel(curTime), x: (curTime - minTime) * scaling, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: key }); + groupNames.push({ type: 'text', text: toLabel(curTime), x: (curTime - minTime) * scaling, y: 0, zIndex: 1000, color: 'orange', height: fontHeight, fontSize, payload: key }); } const keyDocs = pivotDateGroups.get(key)!; x += scaling * (key - prevKey); const stack = findStack(x, stacking); prevKey = key; if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) { - groupNames.push({ type: "text", text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined }); + groupNames.push({ type: 'text', text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined }); } layoutDocsAtTime(keyDocs, key); }); if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) { x = (curTime - minTime) * scaling; - groupNames.push({ type: "text", text: toLabel(curTime), x: x, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: undefined }); + groupNames.push({ type: 'text', text: toLabel(curTime), x: x, y: 0, zIndex: 1000, color: 'orange', height: fontHeight, fontSize, payload: undefined }); } if (Math.ceil(maxTime - minTime) * scaling > x + 25) { - groupNames.push({ type: "text", text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); + groupNames.push({ type: 'text', text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); } - const divider = { type: "div", color: CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "dimgray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; + const divider = { type: 'div', color: Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'dimgray' : 'black', x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; return normalizeResults(panelDim, fontHeight, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider]); function layoutDocsAtTime(keyDocs: Doc[], key: number) { @@ -384,13 +361,14 @@ export function computeTimelineLayout( wid = (Doc.NativeAspect(layoutDoc) || 1) * pivotAxisWidth; } docMap.set(doc[Id], { - x: x, y: -Math.sqrt(stack) * pivotAxisWidth / 2 - pivotAxisWidth + (pivotAxisWidth - hgt) / 2, - zIndex: (curTime === key ? 1000 : zind++), + x: x, + y: (-Math.sqrt(stack) * pivotAxisWidth) / 2 - pivotAxisWidth + (pivotAxisWidth - hgt) / 2, + zIndex: curTime === key ? 1000 : zind++, highlight: curTime === key, - width: wid / (Math.max(stack, 1)), - height: hgt / (Math.max(stack, 1)), + width: wid / Math.max(stack, 1), + height: hgt / Math.max(stack, 1), pair: { layout: doc }, - replica: "" + replica: '', }); stacking[stack] = x + pivotAxisWidth; }); @@ -407,41 +385,49 @@ function normalizeResults( minWidth: number, extras: ViewDefBounds[] ): ViewDefResult[] { - const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds); + const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height } as ViewDefBounds)); const docEles = Array.from(docMap.entries()).map(ele => ele[1]); - const aggBounds = aggregateBounds(extras.concat(grpEles.concat(docEles.map(de => ({ ...de, type: "doc", payload: "" })))).filter(e => e.zIndex !== -99), 0, 0); + const aggBounds = aggregateBounds( + extras.concat(grpEles.concat(docEles.map(de => ({ ...de, type: 'doc', payload: '' })))).filter(e => e.zIndex !== -99), + 0, + 0 + ); aggBounds.r = aggBounds.x + Math.max(minWidth, aggBounds.r - aggBounds.x); const wscale = panelDim[0] / (aggBounds.r - aggBounds.x); - let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? (panelDim[1]) / (aggBounds.b - aggBounds.y) : wscale; + let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? panelDim[1] / (aggBounds.b - aggBounds.y) : wscale; if (Number.isNaN(scale)) scale = 1; - Array.from(docMap.entries()).filter(ele => ele[1].pair).map(ele => { - const newPosRaw = ele[1]; - if (newPosRaw) { - const newPos = { - x: newPosRaw.x * scale, - y: newPosRaw.y * scale, - z: newPosRaw.z, - replica: newPosRaw.replica, - highlight: newPosRaw.highlight, - zIndex: newPosRaw.zIndex, - width: (newPosRaw.width || 0) * scale, - height: newPosRaw.height! * scale, - pair: ele[1].pair - }; - poolData.set(newPos.pair.layout[Id] + (newPos.replica || ""), { transition: "all 1s", ...newPos }); - } - }); + Array.from(docMap.entries()) + .filter(ele => ele[1].pair) + .map(ele => { + const newPosRaw = ele[1]; + if (newPosRaw) { + const newPos = { + x: newPosRaw.x * scale, + y: newPosRaw.y * scale, + z: newPosRaw.z, + replica: newPosRaw.replica, + highlight: newPosRaw.highlight, + zIndex: newPosRaw.zIndex, + width: (newPosRaw.width || 0) * scale, + height: newPosRaw.height! * scale, + pair: ele[1].pair, + }; + poolData.set(newPos.pair.layout[Id] + (newPos.replica || ''), { transition: 'all 1s', ...newPos }); + } + }); - return viewDefsToJSX(extras.concat(groupNames).map(gname => ({ - type: gname.type, - text: gname.text, - x: gname.x * scale, - y: gname.y * scale, - color: gname.color, - width: gname.width === undefined ? undefined : gname.width * scale, - height: gname.height === -1 ? 1 : gname.type === "text" ? Math.max(fontHeight * scale, (gname.height || 0) * scale) : (gname.height || 0) * scale, - fontSize: gname.fontSize, - payload: gname.payload - }))); + return viewDefsToJSX( + extras.concat(groupNames).map(gname => ({ + type: gname.type, + text: gname.text, + x: gname.x * scale, + y: gname.y * scale, + color: gname.color, + width: gname.width === undefined ? undefined : gname.width * scale, + height: gname.height === -1 ? 1 : gname.type === 'text' ? Math.max(fontHeight * scale, (gname.height || 0) * scale) : (gname.height || 0) * scale, + fontSize: gname.fontSize, + payload: gname.payload, + })) + ); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 5f890c810..d979ef961 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,19 +1,18 @@ -import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, Field } from "../../../../fields/Doc"; -import { Id } from "../../../../fields/FieldSymbols"; -import { List } from "../../../../fields/List"; -import { Cast, NumCast } from "../../../../fields/Types"; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, Field } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +import { List } from '../../../../fields/List'; +import { Cast, NumCast } from '../../../../fields/Types'; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { LinkManager } from "../../../util/LinkManager"; -import { SelectionManager } from "../../../util/SelectionManager"; -import { SnappingManager } from "../../../util/SnappingManager"; -import { DocumentView } from "../../nodes/DocumentView"; -import "./CollectionFreeFormLinkView.scss"; -import React = require("react"); -import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; -import { Colors } from "../../global/globalEnums"; - +import { LinkManager } from '../../../util/LinkManager'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { SettingsManager } from '../../../util/SettingsManager'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { Colors } from '../../global/globalEnums'; +import { DocumentView } from '../../nodes/DocumentView'; +import './CollectionFreeFormLinkView.scss'; +import React = require('react'); export interface CollectionFreeFormLinkViewProps { A: DocumentView; @@ -27,31 +26,41 @@ export class CollectionFreeFormLinkView extends React.Component (Date.now() < this._start++ + 1000) && (this._timeout = setTimeout(this.timeout, 25))); + componentWillUnmount() { + this._anchorDisposer?.(); + } + @action timeout = action(() => Date.now() < this._start++ + 1000 && (this._timeout = setTimeout(this.timeout, 25))); componentDidMount() { - this._anchorDisposer = reaction(() => [ - this.props.A.props.ScreenToLocalTransform(), - Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?.scrollTop, - Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?._highlights, - this.props.B.props.ScreenToLocalTransform(), - Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?.scrollTop, - Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?._highlights, - ], + this._anchorDisposer = reaction( + () => [ + this.props.A.props.ScreenToLocalTransform(), + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?.scrollTop, + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?._highlights, + this.props.B.props.ScreenToLocalTransform(), + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?.scrollTop, + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?._highlights, + ], action(() => { this._start = Date.now(); this._timeout && clearTimeout(this._timeout); this._timeout = setTimeout(this.timeout, 25); setTimeout(this.placeAnchors); - }) - , { fireImmediately: true }); + }), + { fireImmediately: true } + ); } placeAnchors = () => { const { A, B, LinkDocs } = this.props; const linkDoc = LinkDocs[0]; if (SnappingManager.GetIsDragging() || !A.ContentDiv || !B.ContentDiv) return; - setTimeout(action(() => this._opacity = 0.75), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() - setTimeout(action(() => (!LinkDocs.length || !linkDoc.linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line. + setTimeout( + action(() => (this._opacity = 0.75)), + 0 + ); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() + setTimeout( + action(() => (!LinkDocs.length || !linkDoc.linkDisplay) && (this._opacity = 0.05)), + 750 + ); // this will unhighlight the link line. const a = A.ContentDiv.getBoundingClientRect(); const b = B.ContentDiv.getBoundingClientRect(); const { left: aleft, top: atop, width: awidth, height: aheight } = A.ContentDiv.parentElement!.getBoundingClientRect(); @@ -60,7 +69,7 @@ export class CollectionFreeFormLinkView extends React.Component= 0 && mpx <= 1) linkDoc.anchor1_x = mpx * 100; if (mpy >= 0 && mpy <= 1) linkDoc.anchor1_y = mpy * 100; - if (getComputedStyle(targetAhyperlink).fontSize === "0px") linkDoc.opacity = 0; + if (getComputedStyle(targetAhyperlink).fontSize === '0px') linkDoc.opacity = 0; else linkDoc.opacity = 1; } if (!targetBhyperlink) { if (linkDoc.linkAutoMove) { - linkDoc.anchor2_x = (bpt.point.x - bleft) / bwidth * 100; - linkDoc.anchor2_y = (bpt.point.y - btop) / bheight * 100; + linkDoc.anchor2_x = ((bpt.point.x - bleft) / bwidth) * 100; + linkDoc.anchor2_y = ((bpt.point.y - btop) / bheight) * 100; } } else { const m = targetBhyperlink.getBoundingClientRect(); @@ -93,80 +102,86 @@ export class CollectionFreeFormLinkView extends React.Component= 0 && mpx <= 1) linkDoc.anchor2_x = mpx * 100; if (mpy >= 0 && mpy <= 1) linkDoc.anchor2_y = mpy * 100; - if (getComputedStyle(targetBhyperlink).fontSize === "0px") linkDoc.opacity = 0; + if (getComputedStyle(targetBhyperlink).fontSize === '0px') linkDoc.opacity = 0; else linkDoc.opacity = 1; } - } - + }; pointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e, down, delta) => { - this.props.LinkDocs[0].linkOffsetX = NumCast(this.props.LinkDocs[0].linkOffsetX) + delta[0]; - this.props.LinkDocs[0].linkOffsetY = NumCast(this.props.LinkDocs[0].linkOffsetY) + delta[1]; - return false; - }, emptyFunction, () => { - // OverlayView.Instance.addElement( - // { })} - // />, { x: 300, y: 300 }); - }); - - - } + setupMoveUpEvents( + this, + e, + (e, down, delta) => { + this.props.LinkDocs[0].linkOffsetX = NumCast(this.props.LinkDocs[0].linkOffsetX) + delta[0]; + this.props.LinkDocs[0].linkOffsetY = NumCast(this.props.LinkDocs[0].linkOffsetY) + delta[1]; + return false; + }, + emptyFunction, + () => { + // OverlayView.Instance.addElement( + // { })} + // />, { x: 300, y: 300 }); + } + ); + }; visibleY = (el: any) => { let rect = el.getBoundingClientRect(); - const top = rect.top, height = rect.height; + const top = rect.top, + height = rect.height; var el = el.parentNode; while (el && el !== document.body) { rect = el.getBoundingClientRect?.(); if (rect?.width) { - if (top <= rect.bottom === false && getComputedStyle(el).overflow === "hidden") return rect.bottom; + if (top <= rect.bottom === false && getComputedStyle(el).overflow === 'hidden') return rect.bottom; // Check if the element is out of view due to a container scrolling - if ((top + height) <= rect.top && getComputedStyle(el).overflow === "hidden") return rect.top; + if (top + height <= rect.top && getComputedStyle(el).overflow === 'hidden') return rect.top; } el = el.parentNode; } // Check its within the document viewport return top; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; - } + }; visibleX = (el: any) => { let rect = el.getBoundingClientRect(); - const left = rect.left, width = rect.width; + const left = rect.left, + width = rect.width; var el = el.parentNode; while (el && el !== document.body) { rect = el?.getBoundingClientRect(); if (rect?.width) { - if (left <= rect.right === false && getComputedStyle(el).overflow === "hidden") return rect.right; + if (left <= rect.right === false && getComputedStyle(el).overflow === 'hidden') return rect.right; // Check if the element is out of view due to a container scrolling - if ((left + width) <= rect.left && getComputedStyle(el).overflow === "hidden") return rect.left; + if (left + width <= rect.left && getComputedStyle(el).overflow === 'hidden') return rect.left; } el = el.parentNode; } // Check its within the document viewport return left; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; - } + }; @action toggleProperties = () => { - if (CurrentUserUtils.propertiesWidth > 0) { - CurrentUserUtils.propertiesWidth = 0; + if (SettingsManager.propertiesWidth > 0) { + SettingsManager.propertiesWidth = 0; } else { - CurrentUserUtils.propertiesWidth = 250; + SettingsManager.propertiesWidth = 250; } - } + }; onClickLine = () => { SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true); this.toggleProperties(); - } + }; @computed.struct get renderData() { - this._start; SnappingManager.GetIsDragging(); + this._start; + SnappingManager.GetIsDragging(); const { A, B, LinkDocs } = this.props; if (!A.ContentDiv || !B.ContentDiv || !LinkDocs.length) return undefined; - const acont = A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); - const bcont = B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); + const acont = A.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); + const bcont = B.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); const adiv = acont.length ? acont[0] : A.ContentDiv; const bdiv = bcont.length ? bcont[0] : B.ContentDiv; for (let apdiv = adiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return; @@ -185,11 +200,11 @@ export class CollectionFreeFormLinkView extends React.Component= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex]; //access stroke color using index of the relationship in the color list (default black) - const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? "black" : linkColorList[currRelationshipIndex]; + const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? 'black' : linkColorList[currRelationshipIndex]; // const hexStroke = this.rgbToHex(stroke) //calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has) //thickness varies linearly from 3px to 12px for increasing link count - const strokeWidth = linkSize === -1 ? "3px" : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + "px"; + const strokeWidth = linkSize === -1 ? '3px' : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + 'px'; if (this.props.LinkDocs[0].displayArrow === undefined) { this.props.LinkDocs[0].displayArrow = false; } - return this.props.LinkDocs[0].opacity === 0 || !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> - - - - - - - {textX === undefined ? (null) : - {Field.toString(this.props.LinkDocs[0].description as any as Field)} - } - ); + return this.props.LinkDocs[0].opacity === 0 || !a.width || !b.width || (!this.props.LinkDocs[0].linkDisplay && !aActive && !bActive) ? null : ( + <> + + + + + + + {textX === undefined ? null : ( + + {Field.toString(this.props.LinkDocs[0].description as any as Field)} + + )} + + ); } -} \ No newline at end of file +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx index 9f6938e67..9e8d92d7d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -1,79 +1,82 @@ -import { computed } from "mobx"; -import { observer } from "mobx-react"; +import { computed } from 'mobx'; +import { observer } from 'mobx-react'; import * as mobxUtils from 'mobx-utils'; -import CursorField from "../../../../fields/CursorField"; -import { FieldResult } from "../../../../fields/Doc"; -import { List } from "../../../../fields/List"; -import { listSpec } from "../../../../fields/Schema"; -import { Cast } from "../../../../fields/Types"; -import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; -import { CollectionViewProps } from "../CollectionView"; -import "./CollectionFreeFormView.scss"; -import React = require("react"); -import v5 = require("uuid/v5"); +import CursorField from '../../../../fields/CursorField'; +import { Doc, FieldResult } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +import { List } from '../../../../fields/List'; +import { listSpec } from '../../../../fields/Schema'; +import { Cast } from '../../../../fields/Types'; +import { CollectionViewProps } from '../CollectionView'; +import './CollectionFreeFormView.scss'; +import React = require('react'); +import v5 = require('uuid/v5'); @observer export class CollectionFreeFormRemoteCursors extends React.Component { - @computed protected get cursors(): CursorField[] { const doc = this.props.Document; let cursors: FieldResult>; - const { id } = CurrentUserUtils; + const id = Doc.UserDoc()[Id]; if (!id || !(cursors = Cast(doc.cursors, listSpec(CursorField)))) { return []; } const now = mobxUtils.now(); - return (cursors || []).filter(({ data: { metadata } }) => metadata.id !== id && (now - metadata.timestamp) < 1000); + return (cursors || []).filter(({ data: { metadata } }) => metadata.id !== id && now - metadata.timestamp < 1000); } @computed get renderedCursors() { - return this.cursors.map(({ data: { metadata, position: { x, y } } }) => { - return ( -
- { - if (el) { - const ctx = el.getContext('2d'); - if (ctx) { - ctx.fillStyle = "#" + v5(metadata.id, v5.URL).substring(0, 6).toUpperCase() + "22"; - ctx.fillRect(0, 0, 20, 20); + return this.cursors.map( + ({ + data: { + metadata, + position: { x, y }, + }, + }) => { + return ( +
+ { + if (el) { + const ctx = el.getContext('2d'); + if (ctx) { + ctx.fillStyle = '#' + v5(metadata.id, v5.URL).substring(0, 6).toUpperCase() + '22'; + ctx.fillRect(0, 0, 20, 20); - ctx.fillStyle = "black"; - ctx.lineWidth = 0.5; + ctx.fillStyle = 'black'; + ctx.lineWidth = 0.5; - ctx.beginPath(); + ctx.beginPath(); - ctx.moveTo(10, 0); - ctx.lineTo(10, 8); + ctx.moveTo(10, 0); + ctx.lineTo(10, 8); - ctx.moveTo(10, 20); - ctx.lineTo(10, 12); + ctx.moveTo(10, 20); + ctx.lineTo(10, 12); - ctx.moveTo(0, 10); - ctx.lineTo(8, 10); + ctx.moveTo(0, 10); + ctx.lineTo(8, 10); - ctx.moveTo(20, 10); - ctx.lineTo(12, 10); + ctx.moveTo(20, 10); + ctx.lineTo(12, 10); - ctx.stroke(); + ctx.stroke(); + } } - } - }} - width={20} - height={20} - /> -

- {metadata.identifier[0].toUpperCase()} -

-
- ); - }); + }} + width={20} + height={20} + /> +

{metadata.identifier[0].toUpperCase()}

+
+ ); + } + ); } render() { return this.renderedCursors; } -} \ 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 8444c9119..07ea26346 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -18,13 +18,13 @@ import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocUtils } from '../../../documents/Documents'; -import { DocumentType } from '../../../documents/DocumentTypes'; -import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; +import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager, dropActionType } from '../../../util/DragManager'; import { HistoryUtil } from '../../../util/History'; import { InteractionUtils } from '../../../util/InteractionUtils'; import { RecordingApi } from '../../../util/RecordingApi'; +import { ReplayMovements } from '../../../util/ReplayMovements'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; import { ColorScheme } from '../../../util/SettingsManager'; @@ -48,7 +48,6 @@ import { StyleProp } from '../../StyleProvider'; import { CollectionDockingView } from '../CollectionDockingView'; import { CollectionSubView } from '../CollectionSubView'; import { TreeViewType } from '../CollectionTreeView'; -import { CollectionViewType } from '../CollectionView'; import { TabDocView } from '../TabDocView'; import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines'; import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors'; @@ -56,8 +55,6 @@ import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; import React = require('react'); import e = require('connect-flash'); -import { ReplayMovements } from '../../../util/ReplayMovements'; - export type collectionFreeformViewProps = { annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox) @@ -517,7 +514,7 @@ export class CollectionFreeFormView extends CollectionSubView { if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return; if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - CurrentUserUtils.ActiveTool = InkTool.None; + Doc.ActiveTool = InkTool.None; if (this.props.isContentActive(true)) e.stopPropagation(); } else if (!e.cancelBubble) { if (this.tryDragCluster(e, this._hitCluster)) { @@ -907,7 +904,7 @@ export class CollectionFreeFormView extends CollectionSubView { - if (this.layoutDoc._Transform || (this.layoutDoc._fitWidth && this.layoutDoc.nativeHeight) || DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) - return; + if (this.layoutDoc._Transform || (this.layoutDoc._fitWidth && this.layoutDoc.nativeHeight) || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); @@ -1079,7 +1075,7 @@ export class CollectionFreeFormView extends CollectionSubView Transform; @@ -46,15 +43,14 @@ interface MarqueeViewProps { } export interface MarqueeViewBounds { - left: number; - top: number; - width: number; - height: number; + left: number; + top: number; + width: number; + height: number; } @observer -export class MarqueeView extends React.Component -{ +export class MarqueeView extends React.Component { private _commandExecuted = false; @observable _lastX: number = 0; @observable _lastY: number = 0; @@ -64,18 +60,26 @@ export class MarqueeView extends React.Component) { this.props.Document.ink = value; } + get inkDoc() { + return this.props.Document; + } + get ink() { + return Cast(this.props.Document.ink, InkField); + } + set ink(value: Opt) { + this.props.Document.ink = value; + } componentDidMount() { this.props.setPreviewCursor?.(this.setPreviewCursor); @@ -84,14 +88,14 @@ export class MarqueeView extends React.Component { if (all) { - document.removeEventListener("pointerup", this.onPointerUp, true); - document.removeEventListener("pointermove", this.onPointerMove, true); + document.removeEventListener('pointerup', this.onPointerUp, true); + document.removeEventListener('pointermove', this.onPointerMove, true); } - document.removeEventListener("keydown", this.marqueeCommand, true); + document.removeEventListener('keydown', this.marqueeCommand, true); hideMarquee && this.hideMarquee(); this._lassoPts = []; - } + }; @undoBatch @action @@ -100,76 +104,75 @@ export class MarqueeView extends React.Component this.props.addDocTab( - Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: "bing", useCors: true }), "add:right")); + if (e.key === '?') { + cm.setDefaultItem('?', (str: string) => this.props.addDocTab(Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: 'bing', useCors: true }), 'add:right')); cm.displayMenu(this._downX, this._downY, undefined, true); e.stopPropagation(); - } else - if (e.key === "u" && this.props.ungroup) { - e.stopPropagation(); - this.props.ungroup(); - } - else if (e.key === ":") { - DocUtils.addDocumentCreatorMenuItems(this.props.addLiveTextDocument, this.props.addDocument || returnFalse, x, y); + } else if (e.key === 'u' && this.props.ungroup) { + e.stopPropagation(); + this.props.ungroup(); + } else if (e.key === ':') { + DocUtils.addDocumentCreatorMenuItems(this.props.addLiveTextDocument, this.props.addDocument || returnFalse, x, y); - cm.displayMenu(this._downX, this._downY, undefined, true); - e.stopPropagation(); - } else if (e.key === "a" && (e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.selectDocuments(this.props.activeDocuments()); - e.stopPropagation(); - } else if (e.key === "q" && e.ctrlKey) { - e.preventDefault(); - (async () => { - const text: string = await navigator.clipboard.readText(); - const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); - for (let i = 0; i < ns.length - 1; i++) { - while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") || - ns[i].endsWith(";\r") || ns[i].endsWith(";") || - ns[i].endsWith(".\r") || ns[i].endsWith(".") || - ns[i].endsWith(":\r") || ns[i].endsWith(":")) && i < ns.length - 1) { - const sub = ns[i].endsWith("\r") ? 1 : 0; - const br = ns[i + 1].trim() === ""; - ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft()); - if (br) break; - } + cm.displayMenu(this._downX, this._downY, undefined, true); + e.stopPropagation(); + } else if (e.key === 'a' && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.props.selectDocuments(this.props.activeDocuments()); + e.stopPropagation(); + } else if (e.key === 'q' && e.ctrlKey) { + e.preventDefault(); + (async () => { + const text: string = await navigator.clipboard.readText(); + const ns = text.split('\n').filter(t => t.trim() !== '\r' && t.trim() !== ''); + for (let i = 0; i < ns.length - 1; i++) { + while ( + !(ns[i].trim() === '' || ns[i].endsWith('-\r') || ns[i].endsWith('-') || ns[i].endsWith(';\r') || ns[i].endsWith(';') || ns[i].endsWith('.\r') || ns[i].endsWith('.') || ns[i].endsWith(':\r') || ns[i].endsWith(':')) && + i < ns.length - 1 + ) { + const sub = ns[i].endsWith('\r') ? 1 : 0; + const br = ns[i + 1].trim() === ''; + ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft()); + if (br) break; } - let ypos = y; - ns.map(line => { - const indent = line.search(/\S|$/); - const newBox = Docs.Create.TextDocument(line, { _width: 200, _height: 35, x: x + indent / 3 * 10, y: ypos, title: line }); - this.props.addDocument?.(newBox); - ypos += 40 * this.Transform.Scale; - }); - })(); - e.stopPropagation(); - } else if (e.key === "b" && e.ctrlKey) { - document.body.focus(); // so that we can access the clipboard without an error - setTimeout(() => - pasteImageBitmap((data: any, error: any) => { - error && console.log(error); - data && VideoBox.convertDataUri(data, this.props.Document[Id] + "-thumb-frozen").then(returnedfilename => { - this.props.Document["thumb-frozen"] = new ImageField(returnedfilename); + } + let ypos = y; + ns.map(line => { + const indent = line.search(/\S|$/); + const newBox = Docs.Create.TextDocument(line, { _width: 200, _height: 35, x: x + (indent / 3) * 10, y: ypos, title: line }); + this.props.addDocument?.(newBox); + ypos += 40 * this.Transform.Scale; + }); + })(); + e.stopPropagation(); + } else if (e.key === 'b' && e.ctrlKey) { + document.body.focus(); // so that we can access the clipboard without an error + setTimeout(() => + pasteImageBitmap((data: any, error: any) => { + error && console.log(error); + data && + VideoBox.convertDataUri(data, this.props.Document[Id] + '-thumb-frozen').then(returnedfilename => { + this.props.Document['thumb-frozen'] = new ImageField(returnedfilename); }); - })); - } else if (e.key === "s" && e.ctrlKey) { - e.preventDefault(); - const slide = DocUtils.copyDragFactory(DocCast(Doc.UserDoc().emptySlide))!; - slide.x = x; - slide.y = y; - FormattedTextBox.SelectOnLoad = slide[Id]; - TreeView._editTitleOnLoad = { id: slide[Id], parent: undefined }; - this.props.addDocument?.(slide); - e.stopPropagation(); - } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) { - FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this.props.childLayoutString ? e.key : ""; - FormattedTextBox.LiveTextUndo = UndoManager.StartBatch("live text batch"); - this.props.addLiveTextDocument(CurrentUserUtils.GetNewTextDoc("-typed text-", x, y, 200, 100, this.props.xPadding === 0)); - e.stopPropagation(); - } - } + }) + ); + } else if (e.key === 's' && e.ctrlKey) { + e.preventDefault(); + const slide = DocUtils.copyDragFactory(DocCast(Doc.UserDoc().emptySlide))!; + slide.x = x; + slide.y = y; + FormattedTextBox.SelectOnLoad = slide[Id]; + TreeView._editTitleOnLoad = { id: slide[Id], parent: undefined }; + this.props.addDocument?.(slide); + e.stopPropagation(); + } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) { + FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this.props.childLayoutString ? e.key : ''; + FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('live text batch'); + this.props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100, this.props.xPadding === 0)); + e.stopPropagation(); + } + }; //heuristically converts pasted text into a table. // assumes each entry is separated by a tab // skips all rows until it gets to a row with more than one entry @@ -178,26 +181,26 @@ export class MarqueeView extends React.Component 0 && ns[0].split("\t").length < 2) { + while (ns.length > 0 && ns[0].split('\t').length < 2) { ns.splice(0, 1); } if (ns.length > 0) { - const columns = ns[0].split("\t"); + const columns = ns[0].split('\t'); const docList: Doc[] = []; - let groupAttr: string | number = ""; + let groupAttr: string | number = ''; const rowProto = new Doc(); rowProto.title = rowProto.Id; rowProto._width = 200; rowProto.isPrototype = true; for (let i = 1; i < ns.length - 1; i++) { - const values = ns[i].split("\t"); + const values = ns[i].split('\t'); if (values.length === 1 && columns.length > 1) { groupAttr = values[0]; continue; } const docDataProto = Doc.MakeDelegate(rowProto); docDataProto.isPrototype = true; - columns.forEach((col, i) => docDataProto[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined)); + columns.forEach((col, i) => (docDataProto[columns[i]] = values.length > i ? (values[i].indexOf(Number(values[i]).toString()) !== -1 ? Number(values[i]) : values[i]) : undefined)); if (groupAttr) { docDataProto._group = groupAttr; } @@ -206,7 +209,13 @@ export class MarqueeView extends React.Component c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", _width: 300, _height: 100 }); + const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField('_group', '#f1efeb')] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, '#f1efeb'))], docList, { + x: x, + y: y, + title: 'droppedTable', + _width: 300, + _height: 100, + }); this.props.addDocument?.(newCol); } @@ -227,10 +236,9 @@ export class MarqueeView extends React.Component { @@ -238,8 +246,7 @@ export class MarqueeView extends React.Component Utils.DRAG_THRESHOLD || - Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) { + if (Math.abs(this._lastX - this._downX) > Utils.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) { if (!this._commandExecuted) { this.showMarquee(); } @@ -253,7 +260,7 @@ export class MarqueeView extends React.Component { @@ -270,14 +277,14 @@ export class MarqueeView extends React.Component { this.hideMarquee(); MarqueeOptionsMenu.Instance.fadeOut(true); - document.removeEventListener("pointerdown", hideMarquee); - document.removeEventListener("wheel", hideMarquee); + document.removeEventListener('pointerdown', hideMarquee); + document.removeEventListener('wheel', hideMarquee); }; if (PresBox.startMarquee) { this.pinWithView(); PresBox.startMarquee = false; } - if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100) && !PresBox.startMarquee) { + if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100 && !PresBox.startMarquee) { MarqueeOptionsMenu.Instance.createCollection = this.collection; MarqueeOptionsMenu.Instance.delete = this.delete; MarqueeOptionsMenu.Instance.summarize = this.summary; @@ -285,19 +292,22 @@ export class MarqueeView extends React.Component { @@ -312,9 +322,9 @@ export class MarqueeView extends React.Component { - if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && - Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { - if (CurrentUserUtils.ActiveTool === InkTool.None) { + if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + if (Doc.ActiveTool === InkTool.None) { if (!(e.nativeEvent as any).marqueeHit) { (e.nativeEvent as any).marqueeHit = true; if (!this.props.trySelectCluster(e.shiftKey)) { @@ -339,17 +348,22 @@ export class MarqueeView extends React.Component { this._visible = true; } + showMarquee = () => { + this._visible = true; + }; @action - hideMarquee = () => { this._visible = false; } + hideMarquee = () => { + this._visible = false; + }; @undoBatch @action @@ -361,16 +375,18 @@ export class MarqueeView extends React.Component, options: DocumentOptions, id?: string) => Doc>, makeGroup: Opt) => { - const newCollection = creator ? creator(selected, { title: "nested stack", }) : ((doc: Doc) => { - Doc.GetProto(doc).data = new List(selected); - Doc.GetProto(doc).title = makeGroup ? "grouping" : "nested freeform"; - !this.props.isAnnotationOverlay && Doc.AddDocToList(CurrentUserUtils.MyFileOrphans, undefined, Doc.GetProto(doc)); - doc._panX = doc._panY = 0; - return doc; - })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); + const newCollection = creator + ? creator(selected, { title: 'nested stack' }) + : ((doc: Doc) => { + Doc.GetProto(doc).data = new List(selected); + Doc.GetProto(doc).title = makeGroup ? 'grouping' : 'nested freeform'; + !this.props.isAnnotationOverlay && Doc.AddDocToList(Doc.MyFileOrphans, undefined, Doc.GetProto(doc)); + doc._panX = doc._panY = 0; + return doc; + })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newCollection.system = undefined; newCollection._width = this.Bounds.width; newCollection._height = this.Bounds.height; @@ -378,7 +394,7 @@ export class MarqueeView extends React.Component d.context = newCollection); + selected.forEach(d => (d.context = newCollection)); this.hideMarquee(); return newCollection; }); @@ -393,74 +409,76 @@ export class MarqueeView extends React.Component { const scale = Math.min(this.props.PanelWidth() / this.Bounds.width, this.props.PanelHeight() / this.Bounds.height); - const doc = this.props.Document; - const viewOptions:PinViewProps = { - bounds: this.Bounds, - scale: scale - }; - TabDocView.PinDoc(doc, {pinWithView: viewOptions}); - MarqueeOptionsMenu.Instance.fadeOut(true); + const doc = this.props.Document; + const viewOptions: PinViewProps = { + bounds: this.Bounds, + scale: scale, + }; + TabDocView.PinDoc(doc, { pinWithView: viewOptions }); + MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); - } + }; @undoBatch @action collection = (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => { const selected = this.marqueeSelect(false); - if (e instanceof KeyboardEvent ? "cg".includes(e.key) : true) { - selected.map(action(d => { - const dx = NumCast(d.x); - const dy = NumCast(d.y); - delete d.x; - delete d.y; - delete d.activeFrame; - delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection - delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection - d.x = dx - this.Bounds.left - this.Bounds.width / 2; - d.y = dy - this.Bounds.top - this.Bounds.height / 2; - return d; - })); + if (e instanceof KeyboardEvent ? 'cg'.includes(e.key) : true) { + selected.map( + action(d => { + const dx = NumCast(d.x); + const dy = NumCast(d.y); + delete d.x; + delete d.y; + delete d.activeFrame; + delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + d.x = dx - this.Bounds.left - this.Bounds.width / 2; + d.y = dy - this.Bounds.top - this.Bounds.height / 2; + return d; + }) + ); this.props.removeDocument?.(selected); } // TODO: nda - this is the code to actually get a new grouped collection - const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t" ? Docs.Create.StackingDocument : undefined, group); + const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group); this.props.addDocument?.(newCollection); this.props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); - } + }; @undoBatch @action syntaxHighlight = (e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false); - if (e instanceof KeyboardEvent ? e.key === "i" : true) { + if (e instanceof KeyboardEvent ? e.key === 'i' : true) { const inks = selected.filter(s => s.type === DocumentType.INK); const setDocs = selected.filter(s => s.type === DocumentType.RTF && s.color); - const sets = setDocs.map((sd) => Cast(sd.data, RichTextField)?.Text as string); + const sets = setDocs.map(sd => Cast(sd.data, RichTextField)?.Text as string); const colors = setDocs.map(sd => FieldValue(sd.color) as string); const wordToColor = new Map(); - sets.forEach((st: string, i: number) => st.split(",").forEach(word => wordToColor.set(word, colors[i]))); + sets.forEach((st: string, i: number) => st.split(',').forEach(word => wordToColor.set(word, colors[i]))); const strokes: InkData[] = []; inks.filter(i => Cast(i.data, InkField)).forEach(i => { const d = Cast(i.data, InkField, null); - const left = Math.min(...d?.inkData.map(pd => pd.X) ?? [0]); - const top = Math.min(...d?.inkData.map(pd => pd.Y) ?? [0]); + const left = Math.min(...(d?.inkData.map(pd => pd.X) ?? [0])); + const top = Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0])); strokes.push(d.inkData.map(pd => ({ X: pd.X + NumCast(i.x) - left, Y: pd.Y + NumCast(i.y) - top }))); }); - CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => { + CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => { // const wordResults = results.filter((r: any) => r.category === "inkWord"); // for (const word of wordResults) { // const indices: number[] = word.strokeIds; @@ -501,12 +519,12 @@ export class MarqueeView extends React.Component r.category === "line"); - const text = lines.map((l: any) => l.recognizedText).join("\r\n"); + const lines = results.filter((r: any) => r.category === 'line'); + 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 })); }); } - } + }; @undoBatch @action @@ -517,15 +535,15 @@ export class MarqueeView extends React.Component { @@ -534,33 +552,33 @@ export class MarqueeView extends React.Component this.props.selectDocuments([newCollection])); - } + }; @undoBatch marqueeCommand = action((e: KeyboardEvent) => { if (this._commandExecuted || (e as any).propagationIsStopped) { return; } - if (e.key === "Backspace" || e.key === "Delete" || e.key === "d") { + if (e.key === 'Backspace' || e.key === 'Delete' || e.key === 'd') { this._commandExecuted = true; e.stopPropagation(); (e as any).propagationIsStopped = true; this.delete(); e.stopPropagation(); } - if ("cbtsSpg".indexOf(e.key) !== -1) { + if ('cbtsSpg'.indexOf(e.key) !== -1) { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); (e as any).propagationIsStopped = true; - if (e.key === "g") this.collection(e, true); - if (e.key === "c" || e.key === "t") this.collection(e); - if (e.key === "s" || e.key === "S") this.summary(e); - if (e.key === "b") this.background(e); - if (e.key === "p") this.pileup(e); + if (e.key === 'g') this.collection(e, true); + if (e.key === 'c' || e.key === 't') this.collection(e); + if (e.key === 's' || e.key === 'S') this.summary(e); + if (e.key === 'b') this.background(e); + if (e.key === 'p') this.pileup(e); this.cleanupInteractions(false); } - if (e.key === "r" || e.key === " ") { + if (e.key === 'r' || e.key === ' ') { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); @@ -568,18 +586,17 @@ export class MarqueeView extends React.Component pair[0]); const ys = this._lassoPts.map(pair => pair[1]); const tl = this.Transform.transformPoint(Math.min(...xs), Math.min(...ys)); @@ -592,10 +609,10 @@ export class MarqueeView extends React.Component tl[0] && truePoint[0] < r1.left) && (truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height); - hasTop = hasTop || (truePoint[1] > tl[1] && truePoint[1] < r1.top) && (truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width); - hasRight = hasRight || (truePoint[0] < br[0] && truePoint[0] > r1.left + r1.width) && (truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height); - hasBottom = hasBottom || (truePoint[1] < br[1] && truePoint[1] > r1.top + r1.height) && (truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width); + hasLeft = hasLeft || (truePoint[0] > tl[0] && truePoint[0] < r1.left && truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height); + hasTop = hasTop || (truePoint[1] > tl[1] && truePoint[1] < r1.top && truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width); + hasRight = hasRight || (truePoint[0] < br[0] && truePoint[0] > r1.left + r1.width && truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height); + hasBottom = hasBottom || (truePoint[1] < br[1] && truePoint[1] > r1.top + r1.height && truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width); if (hasTop && hasLeft && hasBottom && hasRight) { return true; } @@ -615,9 +632,20 @@ export class MarqueeView extends React.Component !doc.z && !doc._lockedPosition).map(selectFunc); - if (!selection.length && selectBackgrounds) this.props.activeDocuments().filter(doc => doc.z === undefined).map(selectFunc); - if (!selection.length) this.props.activeDocuments().filter(doc => doc.z !== undefined).map(selectFunc); + this.props + .activeDocuments() + .filter(doc => !doc.z && !doc._lockedPosition) + .map(selectFunc); + if (!selection.length && selectBackgrounds) + this.props + .activeDocuments() + .filter(doc => doc.z === undefined) + .map(selectFunc); + if (!selection.length) + this.props + .activeDocuments() + .filter(doc => doc.z !== undefined) + .map(selectFunc); return selection; } @@ -625,31 +653,42 @@ export class MarqueeView extends React.Component {this._lassoFreehand ? - - s + pt[0] + "," + pt[1] + " ", "")} fill="none" stroke="black" strokeWidth="1" strokeDasharray="3" /> - - : - } - ; + return ( +
+ {' '} + {this._lassoFreehand ? ( + + s + pt[0] + ',' + pt[1] + ' ', '')} fill="none" stroke="black" strokeWidth="1" strokeDasharray="3" /> + + ) : ( + + )} +
+ ); } render() { - return
e.preventDefault()} - onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}> - {this._visible ? this.marqueeDiv : null} - {this.props.children} -
; + return ( +
e.preventDefault()} + onScroll={e => (e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0)} + onClick={this.onClick} + onPointerDown={this.onPointerDown}> + {this._visible ? this.marqueeDiv : null} + {this.props.children} +
+ ); } -} \ No newline at end of file +} -- cgit v1.2.3-70-g09d2