diff options
Diffstat (limited to 'src')
86 files changed, 2073 insertions, 858 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 5ec1cfdb4..b6a6cc75a 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -14,7 +14,6 @@ export enum DocumentType { LINKDB = "linkdb", BUTTON = "button", SLIDER = "slider", - EXTENSION = "extension", YOUTUBE = "youtube", WEBCAM = "webcam", FONTICON = "fonticonbox", @@ -22,7 +21,7 @@ export enum DocumentType { RECOMMENDATION = "recommendation", LINKFOLLOW = "linkfollow", PRESELEMENT = "preselement", - QUERY = "search", + QUERY = "query", COLOR = "color", DOCULINK = "doculink", PDFANNO = "pdfanno", diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 96425ba30..cca0e7bc6 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -41,6 +41,8 @@ import { ComputedField, ScriptField } from "../../new_fields/ScriptField"; import { ProxyField } from "../../new_fields/Proxy"; import { DocumentType } from "./DocumentTypes"; import { RecommendationsBox } from "../views/RecommendationsBox"; +import { SearchBox } from "../views/search/SearchBox"; + //import { PresBox } from "../views/nodes/PresBox"; //import { PresField } from "../../new_fields/PresField"; import { PresElementBox } from "../views/presentationview/PresElementBox"; @@ -101,6 +103,7 @@ export interface DocumentOptions { isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents forceActive?: boolean; layout?: string | Doc; + hideFilterView?: boolean; // whether to hide the filter popout on collections hideHeadings?: boolean; // whether stacking view column headings should be hidden isTemplateForField?: string; // the field key for which the containing document is a rendering template isTemplateDoc?: boolean; @@ -115,7 +118,6 @@ export interface DocumentOptions { lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed opacity?: number; defaultBackgroundColor?: string; - dontSelect?: boolean; // whether document decorations should be displayed when the document is selected isBackground?: boolean; isButton?: boolean; columnWidth?: number; @@ -161,6 +163,9 @@ export interface DocumentOptions { 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 + searchText?: string; //for searchbox + searchQuery?: string; // for queryBox + filterQuery?: string; linearViewIsExpanded?: boolean; // is linear view expanded } @@ -430,7 +435,7 @@ export namespace Docs { Scripting.addGlobal(Buxton); 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", + "_chromeStatus", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar", "_showTitle", "_showCaption", "_showTitleHover", "_backgroundColor", "_xMargin", "_yMargin", "_xPadding", "_yPadding", "_singleLine", "_scrollTop", "_color", "isButton", "isBackground", "removeDropProperties", "treeViewOpen"]; @@ -499,7 +504,7 @@ export namespace Docs { const extension = path.extname(target); target = `${target.substring(0, target.length - extension.length)}_o${extension}`; } - requestImageSize(target) + requestImageSize(Utils.CorsProxy(target)) .then((size: any) => { const aspect = size.height / size.width; if (!inst._nativeWidth) { @@ -559,8 +564,6 @@ export namespace Docs { const linkDocProto = Doc.GetProto(doc); linkDocProto.anchor1 = source.doc; linkDocProto.anchor2 = target.doc; - linkDocProto.anchor1_timecode = source.doc.currentTimecode || source.doc.displayTimecode; - linkDocProto.anchor2_timecode = target.doc.currentTimecode || source.doc.displayTimecode; if (linkDocProto.layout_key1 === undefined) { Cast(linkDocProto.proto, Doc, null).layout_key1 = DocuLinkBox.LayoutString("anchor1"); @@ -577,11 +580,25 @@ export namespace Docs { } export function InkDocument(color: string, tool: number, strokeWidth: number, points: { X: number, Y: number }[], options: DocumentOptions = {}) { - const doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options); - doc.color = color; - doc.strokeWidth = strokeWidth; - doc.tool = tool; - return doc; + const I = new Doc(); + I.type = DocumentType.INK; + I.layout = InkingStroke.LayoutString("data"); + I.color = color; + I.strokeWidth = strokeWidth; + I.tool = tool; + I.title = "ink"; + I.x = options.x; + I.y = options.y; + I._width = options._width; + I._height = options._height; + I.data = new InkField(points); + return I; + // return I; + // const doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options); + // doc.color = color; + // doc.strokeWidth = strokeWidth; + // doc.tool = tool; + // return doc; } export function PdfDocument(url: string, options: DocumentOptions = {}) { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index e0ffaf7e0..3d5306841 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,16 +1,16 @@ import { action, computed, observable } from 'mobx'; import { Doc, DocListCastAsync, DocListCast, Opt } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; -import { List } from '../../new_fields/List'; import { Cast, NumCast, StrCast } from '../../new_fields/Types'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionView } from '../views/collections/CollectionView'; -import { DocumentView } from '../views/nodes/DocumentView'; +import { DocumentView, DocFocusFunc } from '../views/nodes/DocumentView'; import { LinkManager } from './LinkManager'; import { Scripting } from './Scripting'; import { SelectionManager } from './SelectionManager'; import { DocumentType } from '../documents/DocumentTypes'; +export type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; export class DocumentManager { @@ -84,24 +84,20 @@ export class DocumentManager { return this.getDocumentViewById(toFind[Id], preferredCollection); } - public getFirstDocumentView(toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined { - const views = this.getDocumentViews(toFind); - return views?.find(view => view.props.Document !== originatingDoc); + public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { + return this.getDocumentViews(toFind)?.find(view => view.props.Document !== originatingDoc); } public getDocumentViews(toFind: Doc): DocumentView[] { const toReturn: DocumentView[] = []; + const docViews = DocumentManager.Instance.DocumentViews; - // heurstic to return the "best" documents first: + // heuristic to return the "best" documents first: // choose an exact match over an alias match // choose documents that have a PanelWidth() over those that don't (the treeview documents have no panelWidth) - DocumentManager.Instance.DocumentViews.map(view => - view.props.Document.presBox === undefined && view.props.PanelWidth() > 1 && view.props.Document === toFind && toReturn.push(view)); - DocumentManager.Instance.DocumentViews.map(view => - view.props.Document.presBox === undefined && view.props.PanelWidth() <= 1 && view.props.Document === toFind && toReturn.push(view)); - DocumentManager.Instance.DocumentViews.map(view => - view.props.Document.presBox === undefined && view.props.PanelWidth() > 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view)); - DocumentManager.Instance.DocumentViews.map(view => - view.props.Document.presBox === undefined && view.props.PanelWidth() <= 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view)); + docViews.map(view => !view.props.Document.presBox && view.props.PanelWidth() > 1 && view.props.Document === toFind && toReturn.push(view)); + docViews.map(view => !view.props.Document.presBox && view.props.PanelWidth() <= 1 && view.props.Document === toFind && toReturn.push(view)); + docViews.map(view => !view.props.Document.presBox && view.props.PanelWidth() > 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view)); + docViews.map(view => !view.props.Document.presBox && view.props.PanelWidth() <= 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view)); return toReturn; } @@ -133,107 +129,107 @@ export class DocumentManager { return pairs; } - public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, linkId?: string, closeContextIfNotFound: boolean = false, originatingDoc: Opt<Doc> = undefined): Promise<void> => { + static addRightSplit = (doc: Doc, finished?: () => void) => { + CollectionDockingView.AddRightSplit(doc); + finished?.(); + } + public jumpToDocument = async ( + targetDoc: Doc, + willZoom: boolean, + createViewFunc = DocumentManager.addRightSplit, + docContext?: Doc, + linkId?: string, + closeContextIfNotFound: boolean = false, + originatingDoc: Opt<Doc> = undefined, + finished?: () => void + ): Promise<void> => { + const getFirstDocView = DocumentManager.Instance.getFirstDocumentView; + const focusAndFinish = () => { finished?.(); return false; }; const highlight = () => { - const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc); - finalDocView && (finalDocView.Document.scrollToLinkID = linkId); - finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document); + const finalDocView = getFirstDocView(targetDoc); + if (finalDocView) { + finalDocView.Document.scrollToLinkID = linkId; + Doc.linkFollowHighlight(finalDocView.props.Document); + } }; - const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc, originatingDoc); + const docView = getFirstDocView(targetDoc, originatingDoc); let annotatedDoc = await Cast(targetDoc.annotationOn, Doc); if (annotatedDoc) { - const first = DocumentManager.Instance.getFirstDocumentView(annotatedDoc); + const first = getFirstDocView(annotatedDoc); if (first) annotatedDoc = first.props.Document; } if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight? - docView.props.focus(docView.props.Document, willZoom); + docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish); highlight(); } else { const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined; - const contextDoc = contextDocs && contextDocs.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined; - const targetDocContext = (annotatedDoc ? annotatedDoc : contextDoc); + const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined; + const targetDocContext = annotatedDoc || contextDoc; if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default - (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc))); + createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc))); highlight(); - } else { - const targetDocContextView = DocumentManager.Instance.getFirstDocumentView(targetDocContext); + } else { // otherwise try to get a view of the context of the target + const targetDocContextView = getFirstDocView(targetDocContext); targetDocContext.scrollY = 0; // this will force PDFs to activate and load their annotations / allow scrolling - if (targetDocContextView) { // we have a context view and aren't forced to create a new one ... focus on the context + if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first.. targetDocContext.panTransformType = "Ease"; targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom); // now find the target document within the context - if (targetDoc.displayTimecode) { // the target should show up once the video scrubs to the display timecode; + if (targetDoc.displayTimecode) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; targetDocContext.currentTimecode = targetDoc.displayTimecode; - } else { + finished?.(); + } else { // no timecode means we need to find the context view and focus on our target 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 + const retryDocView = getFirstDocView(targetDoc); // test again for the target view snce we presumably created the context above by focusing on it + if (retryDocView) { // we found the target in the context + retryDocView.props.focus(targetDoc, willZoom, undefined, focusAndFinish); // focus on the target in the context + } else { // we didn't find the target, so it must have moved out of the context. Go back to just creating it. + if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.props.Document); + targetDoc.layout && createViewFunc(Doc.BrushDoc(targetDoc), finished); // 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); + createViewFunc(targetDocContext); // so first we create the target, but don't pass finished because we still need to create the target setTimeout(() => { - const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc); - const finalDocContextView = DocumentManager.Instance.getFirstDocumentView(targetDocContext); + const finalDocView = getFirstDocView(targetDoc); + const finalDocContextView = getFirstDocView(targetDocContext); setTimeout(() => // if not, wait a bit to see if the context can be loaded (e.g., a PDF). wait interval heurisitic tries to guess how we're animating based on what's just become visible - this.jumpToDocument(targetDoc, willZoom, dockFunc, undefined, linkId, true), finalDocView ? 0 : finalDocContextView ? 250 : 2000); // so call jump to doc again and if the doc isn't found, it will be created. + this.jumpToDocument(targetDoc, willZoom, createViewFunc, undefined, linkId, true, undefined, finished), // pass true this time for closeContextIfNotFound + finalDocView ? 0 : finalDocContextView ? 250 : 2000); // so call jump to doc again and if the doc isn't found, it will be created. }, 0); } } } } - public async FollowLink(link: Doc | undefined, doc: Doc, focus: (doc: Doc, maxLocation: string) => void, zoom: boolean = false, reverse: boolean = false, currentContext?: Doc) { + public async FollowLink(link: Opt<Doc>, doc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { const linkDocs = link ? [link] : DocListCast(doc.links); SelectionManager.DeselectAll(); - const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc)); - const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc)); - const firstDocWithoutView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0); - const secondDocWithoutView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0); - const first = firstDocWithoutView ? [firstDocWithoutView] : firstDocs; - const second = secondDocWithoutView ? [secondDocWithoutView] : secondDocs; - const linkDoc = first.length ? first[0] : second.length ? second[0] : undefined; - const linkFollowDocs = first.length ? [await first[0].anchor2 as Doc, await first[0].anchor1 as Doc] : second.length ? [await second[0].anchor1 as Doc, await second[0].anchor2 as Doc] : undefined; - const linkFollowDocContexts = first.length ? [await first[0].context as Doc, await first[0].context as Doc] : second.length ? [await second[0].context as Doc, await second[0].context as Doc] : [undefined, undefined]; - const linkFollowTimecodes = first.length ? [NumCast(first[0].anchor2_timecode), NumCast(first[0].anchor1_timecode)] : second.length ? [NumCast(second[0].anchor1_timecode), NumCast(second[0].anchor2_timecode)] : [undefined, undefined]; - if (linkFollowDocs && linkDoc) { - 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]; - const annotatedDoc = await Cast(target.annotationOn, Doc); - if (annotatedDoc) { - annotatedDoc.currentTimecode !== undefined && (target.currentTimecode = linkFollowTimecodes[reverse ? 1 : 0]); + const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc)); // link docs where 'doc' is anchor1 + const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc)); // link docs where 'doc' is anchor2 + const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0); + const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0); + const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; + const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : (traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs); + const linkDoc = linkDocList.length && linkDocList[0]; + if (linkDoc) { + const target = (doc === linkDoc.anchor1 ? linkDoc.anchor2 : doc === linkDoc.anchor2 ? linkDoc.anchor1 : + (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc; + if (target) { + const containerDoc = (await Cast(target.annotationOn, Doc)) || target; + containerDoc.currentTimecode !== undefined && (containerDoc.currentTimecode = NumCast(target?.timecode)); + const targetContext = await target?.context as Doc; + const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined; + DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "onRight"), finished), targetNavContext, linkDoc[Id], undefined, doc, finished); } else { - target.currentTimecode !== undefined && (target.currentTimecode = linkFollowTimecodes[reverse ? 1 : 0]); + finished?.(); } - DocumentManager.Instance.jumpToDocument(linkFollowDocs[reverse ? 1 : 0], zoom, (doc: Doc) => focus(doc, maxLocation), targetContext, linkDoc[Id], undefined, doc); - } else if (link) { - DocumentManager.Instance.jumpToDocument(link, zoom, (doc: Doc) => focus(doc, "onRight"), undefined, undefined); - } - } - - @action - zoomIntoScale = (docDelegate: Doc, scale: number) => { - const docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate)); - docView?.props.zoomToScale(scale); - } - - getScaleOfDocView = (docDelegate: Doc) => { - const doc = Doc.GetProto(docDelegate); - - const docView = DocumentManager.Instance.getDocumentView(doc); - if (docView) { - return docView.props.getScale(); } else { - return 1; + finished?.(); } } } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 1f2312032..53c0a2963 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -17,6 +17,7 @@ import { convertDropDataToButtons } from "./DropConverter"; import { AudioBox } from "../views/nodes/AudioBox"; import { DateField } from "../../new_fields/DateField"; import { DocumentView } from "../views/nodes/DocumentView"; +import { UndoManager } from "./UndoManager"; export type dropActionType = "place" | "alias" | "copy" | undefined; export function SetupDrag( @@ -199,6 +200,7 @@ export namespace DragManager { dropDoc instanceof Doc && AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: dropDoc }, { doc: d }, "audio link", "audio timeline")); return dropDoc; }; + const batch = UndoManager.StartBatch("dragging"); const finishDrag = (e: DragCompleteEvent) => { e.docDragData && (e.docDragData.droppedDocuments = dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : @@ -208,6 +210,7 @@ export namespace DragManager { e.docDragData?.droppedDocuments.forEach((drop: Doc, i: number) => (dragData?.removeDropProperties || []).concat(Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), [])).map(prop => drop[prop] = undefined) ); + batch.end(); }; dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded StartDrag(eles, dragData, downX, downY, options, finishDrag); @@ -401,15 +404,17 @@ export namespace DragManager { function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (e: DragCompleteEvent) => void) { const removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => { - const ret = { ele: dragEle, w: dragEle.style.width, h: dragEle.style.height }; + const ret = { ele: dragEle, w: dragEle.style.width, h: dragEle.style.height, o: dragEle.style.overflow }; dragEle.style.width = "0"; dragEle.style.height = "0"; + dragEle.style.overflow = "hidden"; return ret; }); const target = document.elementFromPoint(e.x, e.y); removed.map(r => { r.ele.style.width = r.w; r.ele.style.height = r.h; + r.ele.style.overflow = r.o; }); if (target) { const complete = new DragCompleteEvent(false, dragData); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 69dc303cd..d96257ca1 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -58,7 +58,10 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { (layoutDoc.layout instanceof Doc) && !data.userDropAction; } layoutDoc.isTemplateDoc = true; - dbox = Docs.Create.FontIconDocument({ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), icon: layoutDoc.isTemplateDoc ? "font" : "bolt" }); + dbox = Docs.Create.FontIconDocument({ + _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, isButton: true, + backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), icon: layoutDoc.isTemplateDoc ? "font" : "bolt" + }); dbox.dragFactory = layoutDoc; dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 2eec02a42..f2d569cf3 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -10,20 +10,6 @@ export namespace InteractionUtils { const REACT_POINTER_PEN_BUTTON = 0; const ERASER_BUTTON = 5; - 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 class MultiTouchEvent<T extends React.TouchEvent | TouchEvent> { constructor( readonly fingers: number, diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 094cd58f3..de2707d36 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -16,7 +16,7 @@ import { listSpec } from "../../new_fields/Schema"; import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField"; import { ComputedField } from "../../new_fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../Utils"; +import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../Utils"; import { DocServer } from "../DocServer"; import { Docs } from "../documents/Documents"; import { CollectionViewType } from "../views/collections/CollectionView"; @@ -818,11 +818,14 @@ export class DashDocView { LibraryPath={this._textBox.props.LibraryPath} fitToBox={BoolCast(dashDoc._fitToBox)} addDocument={returnFalse} + rootSelected={this._textBox.props.isSelected} removeDocument={removeDoc} ScreenToLocalTransform={this.getDocTransform} addDocTab={this._textBox.props.addDocTab} pinToPres={returnFalse} renderDepth={self._textBox.props.renderDepth + 1} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={finalLayout[WidthSym]} PanelHeight={finalLayout[HeightSym]} focus={this.outerFocus} @@ -830,8 +833,6 @@ export class DashDocView { parentActive={returnFalse} whenActiveChanged={returnFalse} bringToFront={emptyFunction} - zoomToScale={emptyFunction} - getScale={returnOne} dontRegisterView={false} ContainingCollectionView={this._textBox.props.ContainingCollectionView} ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc} diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index ce21b7fa7..cf04c44ca 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -195,14 +195,14 @@ export type Transformer = { getVars?: () => { capturedVariables: { [name: string]: Field } } }; export interface ScriptOptions { - requiredType?: string; - addReturn?: boolean; - params?: { [name: string]: string }; - capturedVariables?: { [name: string]: Field }; - typecheck?: boolean; - editable?: boolean; - traverser?: TraverserParam; - transformer?: Transformer; + requiredType?: string; // does function required a typed return value + addReturn?: boolean; // does the compiler automatically add a return statement + params?: { [name: string]: string }; // list of function parameters and their types + capturedVariables?: { [name: string]: Field }; // list of captured variables + typecheck?: boolean; // should the compiler perform typechecking + editable?: boolean; // can the script edit Docs + traverser?: TraverserParam; + transformer?: Transformer; // does the editor display a text label by each document that can be used as a captured document reference globals?: { [name: string]: any }; } diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index b597f1e07..2026bf940 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -64,7 +64,7 @@ export namespace SearchUtil { const textDocs = newIds.map((id: string) => textDocMap[id]).map(doc => doc as Doc); for (let i = 0; i < textDocs.length; i++) { const testDoc = textDocs[i]; - if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && testDoc.type !== DocumentType.EXTENSION && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) { + if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) { theDocs.push(Doc.GetProto(testDoc)); theLines.push(newLines[i].map(line => line.replace(query, query.toUpperCase()))); } @@ -74,7 +74,7 @@ export namespace SearchUtil { const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc); for (let i = 0; i < ids.length; i++) { const testDoc = docs[i]; - if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && testDoc.type !== DocumentType.EXTENSION && (options.allowAliases || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) { + if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowAliases || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) { theDocs.push(testDoc); theLines.push([]); } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 0e281e77e..6c386d684 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -54,12 +54,14 @@ export namespace SelectionManager { export function SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { manager.SelectDoc(docView, ctrlPressed); } - + // computed functions, such as used in IsSelected generate errors if they're called outside of a + // reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature + // to avoid unnecessary mobx invalidations when running inside a reaction. export function IsSelected(doc: DocumentView, outsideReaction?: boolean): boolean { return outsideReaction ? - manager.SelectedDocuments.get(doc) ? true : false : - computedFn(function isSelected(doc: DocumentView) { + manager.SelectedDocuments.get(doc) ? true : false : // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get() + computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed return manager.SelectedDocuments.get(doc) ? true : false; })(doc); } diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index f4e830a48..21eec66be 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -6,6 +6,7 @@ import { listSpec } from '../../new_fields/Schema'; import { InkingControl } from './InkingControl'; import { InkTool } from '../../new_fields/InkField'; import { PositionDocument } from '../../new_fields/documentSchemas'; +import { InteractionUtils } from '../util/InteractionUtils'; /// DocComponent returns a generic React base class used by views that don't have any data extensions (e.g.,CollectionFreeFormDocumentView, DocumentView, ButtonBox) @@ -18,6 +19,7 @@ export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: D //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed get Document(): T { return schemaCtor(this.props.Document); } @computed get layoutDoc() { return PositionDocument(Doc.Layout(this.props.Document, this.props.LayoutDoc?.())); } + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; } return Component; } @@ -29,6 +31,7 @@ interface DocExtendableProps { fieldKey: string; isSelected: (outsideReaction?: boolean) => boolean; renderDepth: number; + rootSelected: (outsideReaction?: boolean) => boolean; } export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCtor: (doc: Doc) => T) { class Component extends Touchable<P> { @@ -36,7 +39,8 @@ export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCt @computed get Document(): T { return schemaCtor(this.props.Document); } @computed get layoutDoc() { return Doc.Layout(this.props.Document); } @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Cast(this.props.Document.resolvedDataDoc, Doc, null) || Doc.GetProto(this.props.Document)) as Doc; } - active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools + active = (outsideReaction?: boolean) => !this.props.Document.isBackground && ((this.props.Document.forceActive && this.props.rootSelected(outsideReaction)) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; } return Component; } @@ -50,6 +54,7 @@ export interface DocAnnotatableProps { active: () => boolean; whenActiveChanged: (isActive: boolean) => void; isSelected: (outsideReaction?: boolean) => boolean; + rootSelected: (outsideReaction?: boolean) => boolean; renderDepth: number; } export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) { @@ -60,6 +65,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema @computed get layoutDoc() { return Doc.Layout(this.props.Document); } @computed get dataDoc() { return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; } + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; _annotationKey: string = "annotations"; public set annotationKey(val: string) { this._annotationKey = val; } @@ -86,7 +92,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) && - (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) + ((this.props.Document.forceActive && this.props.rootSelected(outsideReaction)) || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) annotationsActive = (outsideReaction?: boolean) => (InkingControl.Instance.selectedTool !== InkTool.None || (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 79600b7c1..e313b117f 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -69,7 +69,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> get Bounds(): { x: number, y: number, b: number, r: number } { return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { if (documentView.props.renderDepth === 0 || - //documentView.props.Document.dontSelect || Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) { return bounds; } diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index ea60907f6..69aa8dbaa 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -15,7 +15,7 @@ import { Scripting } from "../util/Scripting"; import { FieldValue, Cast, NumCast, BoolCast } from "../../new_fields/Types"; import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; import HorizontalPalette from "./Palette"; -import { Utils, emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, numberRange } from "../../Utils"; +import { Utils, emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, numberRange, returnZero } from "../../Utils"; import { DocumentView } from "./nodes/DocumentView"; import { Transform } from "../util/Transform"; import { DocumentContentsView } from "./nodes/DocumentContentsView"; @@ -193,7 +193,7 @@ export default class GestureOverlay extends Touchable { }, (500)); } else { - clearTimeout(this._holdTimer); + this._holdTimer && clearTimeout(this._holdTimer); } document.removeEventListener("touchmove", this.onReactTouchMove); document.removeEventListener("touchend", this.onReactTouchEnd); @@ -270,7 +270,7 @@ export default class GestureOverlay extends Touchable { onReactTouchMove = (e: TouchEvent) => { const nts: any = this.getNewTouches(e); - clearTimeout(this._holdTimer); + this._holdTimer && clearTimeout(this._holdTimer); this._holdTimer = undefined; document.dispatchEvent( @@ -290,7 +290,7 @@ export default class GestureOverlay extends Touchable { onReactTouchEnd = (e: TouchEvent) => { const nts: any = this.getNewTouches(e); - clearTimeout(this._holdTimer); + this._holdTimer && clearTimeout(this._holdTimer); this._holdTimer = undefined; document.dispatchEvent( @@ -323,7 +323,7 @@ export default class GestureOverlay extends Touchable { } handleHandDown = async (e: React.TouchEvent) => { - clearTimeout(this._holdTimer!); + this._holdTimer && 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); @@ -623,7 +623,7 @@ export default class GestureOverlay extends Touchable { actionPerformed = true; break; case GestureUtils.Gestures.EndBracket: - this.dispatchGesture(GestureUtils.Gestures.EndBracket); + this.dispatchGesture("endbracket"); actionPerformed = true; break; case GestureUtils.Gestures.Line: @@ -648,7 +648,7 @@ export default class GestureOverlay extends Touchable { document.removeEventListener("pointerup", this.onPointerUp); } - dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, data?: any) => { + dispatchGesture = (gesture: "box" | "line" | "startbracket" | "endbracket" | "stroke" | "scribble" | "text", 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", @@ -656,7 +656,7 @@ export default class GestureOverlay extends Touchable { bubbles: true, detail: { points: stroke ?? this._points, - gesture: gesture, + gesture: gesture as any, bounds: this.getBounds(stroke ?? this._points), text: data } @@ -695,7 +695,8 @@ export default class GestureOverlay extends Touchable { </svg>] ]; } - + screenToLocalTransform = () => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + this.height, 1); + return300 = () => 300; @action public openFloatingDoc = (doc: Doc) => { this._clipboardDoc = @@ -705,13 +706,16 @@ export default class GestureOverlay extends Touchable { LibraryPath={emptyPath} addDocument={undefined} addDocTab={returnFalse} + rootSelected={returnTrue} pinToPres={emptyFunction} onClick={undefined} removeDocument={undefined} - ScreenToLocalTransform={() => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + this.height, 1)} + ScreenToLocalTransform={this.screenToLocalTransform} ContentScaling={returnOne} - PanelWidth={() => 300} - PanelHeight={() => 300} + PanelWidth={this.return300} + PanelHeight={this.return300} + NativeHeight={returnZero} + NativeWidth={returnZero} renderDepth={0} backgroundColor={returnEmptyString} focus={emptyFunction} @@ -720,8 +724,6 @@ export default class GestureOverlay extends Touchable { bringToFront={emptyFunction} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne} />; } @@ -736,7 +738,6 @@ export default class GestureOverlay extends Touchable { } render() { - trace(); return ( <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}> {this.showMobileInkOverlay ? <MobileInkOverlay /> : <></>} diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 8d9be5980..bef92f0fd 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,54 +1,46 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { - faFileAlt, faStickyNote, faArrowDown, faBullseye, faFilter, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, - faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp, faClipboard, faVideo, -} from '@fortawesome/free-solid-svg-icons'; +import { faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faThumbtack, faTree, faTv, faUndoAlt, faVideo } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; import Measure from 'react-measure'; -import { Doc, DocListCast, Field, FieldResult, Opt } from '../../new_fields/Doc'; +import { Doc, DocListCast, Field, Opt } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; import { List } from '../../new_fields/List'; import { listSpec } from '../../new_fields/Schema'; -import { Cast, FieldValue, StrCast, BoolCast } from '../../new_fields/Types'; +import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types'; +import { TraceMobx } from '../../new_fields/util'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils, emptyPath } from '../../Utils'; +import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, returnTrue, Utils } from '../../Utils'; import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { Docs, DocumentOptions } from '../documents/Documents'; +import { DocumentType } from '../documents/DocumentTypes'; import { HistoryUtil } from '../util/History'; +import RichTextMenu from '../util/RichTextMenu'; +import { Scripting } from '../util/Scripting'; +import SettingsManager from '../util/SettingsManager'; import SharingManager from '../util/SharingManager'; import { Transform } from '../util/Transform'; -import { CollectionLinearView } from './collections/CollectionLinearView'; -import { CollectionViewType, CollectionView } from './collections/CollectionView'; import { CollectionDockingView } from './collections/CollectionDockingView'; +import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu'; +import { CollectionLinearView } from './collections/CollectionLinearView'; +import { CollectionView, CollectionViewType } from './collections/CollectionView'; import { ContextMenu } from './ContextMenu'; import { DictationOverlay } from './DictationOverlay'; import { DocumentDecorations } from './DocumentDecorations'; +import GestureOverlay from './GestureOverlay'; import KeyManager from './GlobalKeyHandler'; import "./MainView.scss"; import { MainViewNotifs } from './MainViewNotifs'; +import { AudioBox } from './nodes/AudioBox'; import { DocumentView } from './nodes/DocumentView'; +import { RadialMenu } from './nodes/RadialMenu'; +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'; -import { AudioBox } from './nodes/AudioBox'; -import SettingsManager from '../util/SettingsManager'; -import { TraceMobx } from '../../new_fields/util'; -import { RadialMenu } from './nodes/RadialMenu'; -import RichTextMenu from '../util/RichTextMenu'; -import { DocumentType } from '../documents/DocumentTypes'; @observer export class MainView extends React.Component { @@ -309,11 +301,14 @@ export class MainView extends React.Component { addDocument={undefined} addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} + rootSelected={returnTrue} onClick={undefined} backgroundColor={this.defaultBackgroundColors} removeDocument={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={this.getPWidth} PanelHeight={this.getPHeight} renderDepth={0} @@ -323,8 +318,6 @@ export class MainView extends React.Component { bringToFront={emptyFunction} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne} />; } @computed get dockingContent() { @@ -407,12 +400,15 @@ export class MainView extends React.Component { DataDoc={undefined} LibraryPath={emptyPath} addDocument={undefined} + rootSelected={returnTrue} addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} removeDocument={undefined} onClick={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={this.flyoutWidthFunc} PanelHeight={this.getPHeight} renderDepth={0} @@ -422,10 +418,7 @@ export class MainView extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne}> - </DocumentView> + ContainingCollectionDoc={undefined} /> </div> <div className="mainView-contentArea" style={{ position: "relative", height: `calc(100% - ${this._buttonBarHeight}px)`, width: "100%", overflow: "visible" }}> <DocumentView @@ -435,6 +428,9 @@ export class MainView extends React.Component { addDocument={undefined} addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} + NativeHeight={returnZero} + NativeWidth={returnZero} + rootSelected={returnTrue} removeDocument={returnFalse} onClick={undefined} ScreenToLocalTransform={this.mainContainerXf} @@ -448,10 +444,7 @@ export class MainView extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne}> - </DocumentView> + ContainingCollectionDoc={undefined} /> <button className="mainView-settings" key="settings" onClick={() => SettingsManager.Instance.open()}> Settings </button> @@ -523,6 +516,7 @@ export class MainView extends React.Component { fieldKey={"data"} dropAction={"alias"} annotationsKey={""} + rootSelected={returnTrue} bringToFront={emptyFunction} select={emptyFunction} active={returnFalse} @@ -536,6 +530,8 @@ export class MainView extends React.Component { onClick={undefined} ScreenToLocalTransform={this.buttonBarXf} ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={this.flyoutWidthFunc} PanelHeight={this.getContentsHeight} renderDepth={0} diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 220efd4a8..4000cade5 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -174,6 +174,7 @@ export class OverlayView extends React.Component { Document={d} LibraryPath={emptyPath} ChromeHeight={returnZero} + rootSelected={returnTrue} // isSelected={returnFalse} // select={emptyFunction} // layoutKey={"layout"} @@ -181,6 +182,8 @@ export class OverlayView extends React.Component { addDocument={undefined} removeDocument={undefined} ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={returnOne} PanelHeight={returnOne} ScreenToLocalTransform={Transform.Identity} @@ -192,9 +195,7 @@ export class OverlayView extends React.Component { addDocTab={returnFalse} pinToPres={emptyFunction} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne} /> + ContainingCollectionDoc={undefined}/> </div>; }); } diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx index e04f814d1..63744cb50 100644 --- a/src/client/views/Palette.tsx +++ b/src/client/views/Palette.tsx @@ -1,23 +1,12 @@ +import { IReactionDisposer, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; import * as React from "react"; -import "./Palette.scss"; -import { PointData } from "../../new_fields/InkField"; import { Doc } from "../../new_fields/Doc"; -import { Docs } from "../documents/Documents"; -import { ScriptField, ComputedField } from "../../new_fields/ScriptField"; -import { List } from "../../new_fields/List"; -import { DocumentView } from "./nodes/DocumentView"; -import { emptyPath, returnFalse, emptyFunction, returnOne, returnEmptyString, returnTrue } from "../../Utils"; -import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; +import { NumCast } from "../../new_fields/Types"; +import { emptyFunction, emptyPath, returnEmptyString, returnZero, returnFalse, returnOne, returnTrue } from "../../Utils"; import { Transform } from "../util/Transform"; -import { computed, action, IReactionDisposer, reaction, observable } from "mobx"; -import { FieldValue, Cast, NumCast } from "../../new_fields/Types"; -import { observer } from "mobx-react"; -import { DocumentContentsView } from "./nodes/DocumentContentsView"; -import { CollectionStackingView } from "./collections/CollectionStackingView"; -import { CollectionView } from "./collections/CollectionView"; -import { CollectionSubView, SubCollectionViewProps } from "./collections/CollectionSubView"; -import { makeInterface } from "../../new_fields/Schema"; -import { documentSchema } from "../../new_fields/documentSchemas"; +import { DocumentView } from "./nodes/DocumentView"; +import "./Palette.scss"; export interface PaletteProps { x: number; @@ -40,7 +29,7 @@ export default class Palette extends React.Component<PaletteProps> { } componentWillUnmount = () => { - this._selectedDisposer && this._selectedDisposer(); + this._selectedDisposer?.(); } render() { @@ -54,11 +43,14 @@ export default class Palette extends React.Component<PaletteProps> { LibraryPath={emptyPath} addDocument={undefined} addDocTab={returnFalse} + rootSelected={returnTrue} pinToPres={emptyFunction} removeDocument={undefined} onClick={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={() => window.screen.width} PanelHeight={() => window.screen.height} renderDepth={0} @@ -68,10 +60,7 @@ export default class Palette extends React.Component<PaletteProps> { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne}> - </DocumentView> + ContainingCollectionDoc={undefined} /> <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div> </div> </div> diff --git a/src/client/views/RecommendationsBox.scss b/src/client/views/RecommendationsBox.scss index dd8a105f6..7d89042a4 100644 --- a/src/client/views/RecommendationsBox.scss +++ b/src/client/views/RecommendationsBox.scss @@ -54,10 +54,11 @@ margin-left: 5px; } -img{ - width: 100%; - height: 100%; -} +// bcz: UGH!! Can't have global settings like this!!! +// img{ +// width: 100%; +// height: 100%; +// } .score { // margin-left: 15px; diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx index 5ebba0abb..e66fd3eb4 100644 --- a/src/client/views/RecommendationsBox.tsx +++ b/src/client/views/RecommendationsBox.tsx @@ -6,7 +6,7 @@ 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, emptyPath } from "../../Utils"; +import { returnFalse, emptyFunction, returnEmptyString, returnOne, emptyPath, returnZero } from "../../Utils"; import { Transform } from "../util/Transform"; import { ObjectField } from "../../new_fields/ObjectField"; import { DocumentView } from "./nodes/DocumentView"; @@ -68,10 +68,13 @@ export class RecommendationsBox extends React.Component<FieldViewProps> { addDocument={returnFalse} LibraryPath={emptyPath} removeDocument={returnFalse} + rootSelected={returnFalse} ScreenToLocalTransform={Transform.Identity} addDocTab={returnFalse} pinToPres={returnFalse} renderDepth={1} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={returnXDimension} PanelHeight={returnYDimension} focus={emptyFunction} @@ -79,8 +82,6 @@ export class RecommendationsBox extends React.Component<FieldViewProps> { parentActive={returnFalse} whenActiveChanged={returnFalse} bringToFront={emptyFunction} - zoomToScale={emptyFunction} - getScale={returnOne} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} ContentScaling={scale} diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx index cc5d7640e..1e81bb80b 100644 --- a/src/client/views/ScriptBox.tsx +++ b/src/client/views/ScriptBox.tsx @@ -121,6 +121,6 @@ export class ScriptBox extends React.Component<ScriptBoxProps> { overlayDisposer(); } }} showDocumentIcons />; - overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: title }); + overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title }); } } diff --git a/src/client/views/SearchDocBox.tsx b/src/client/views/SearchDocBox.tsx new file mode 100644 index 000000000..4790a2ad7 --- /dev/null +++ b/src/client/views/SearchDocBox.tsx @@ -0,0 +1,431 @@ +import { library } from "@fortawesome/fontawesome-svg-core"; +import { faBullseye, faLink } from "@fortawesome/free-solid-svg-icons"; +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +//import "./SearchBoxDoc.scss"; +import { Doc, DocListCast } from "../../new_fields/Doc"; +import { Id } from "../../new_fields/FieldSymbols"; +import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types"; +import { returnFalse } from "../../Utils"; +import { Docs } from "../documents/Documents"; +import { SearchUtil } from "../util/SearchUtil"; +import { EditableView } from "./EditableView"; +import { ContentFittingDocumentView } from "./nodes/ContentFittingDocumentView"; +import { FieldView, FieldViewProps } from "./nodes/FieldView"; +import { FilterBox } from "./search/FilterBox"; +import { SearchItem } from "./search/SearchItem"; +import React = require("react"); + +export interface RecProps { + documents: { preview: Doc, similarity: number }[]; + node: Doc; + +} + +library.add(faBullseye, faLink); +export const keyPlaceholder = "Query"; + +@observer +export class SearchDocBox extends React.Component<FieldViewProps> { + + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchDocBox, 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); + this.editingMetadata = this.editingMetadata || false; + //SearchBox.Instance = this; + this.resultsScrolled = this.resultsScrolled.bind(this); + } + + + @computed + private get editingMetadata() { + return BoolCast(this.props.Document.editingMetadata); + } + + private set editingMetadata(value: boolean) { + this.props.Document.editingMetadata = value; + } + + static readonly buffer = 20; + + componentDidMount() { + runInAction(() => { + console.log("didit" + ); + this.query = StrCast(this.props.Document.searchText); + this.content = (Docs.Create.TreeDocument(DocListCast(Doc.GetProto(this.props.Document).data), { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query })); + + }); + if (this.inputRef.current) { + this.inputRef.current.focus(); + runInAction(() => { + this._searchbarOpen = true; + }); + } + } + + @observable + private content: Doc | undefined; + + @action + updateKey = async (newKey: string) => { + this.query = newKey; + if (newKey.length > 1) { + const newdocs = await this.getAllResults(this.query); + const things = newdocs.docs; + console.log(things); + console.log(this.content); + runInAction(() => { + this.content = Docs.Create.TreeDocument(things, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query }); + }); + console.log(this.content); + } + + + //this.keyRef.current && this.keyRef.current.setIsFocused(false); + //this.query.length === 0 && (this.query = keyPlaceholder); + return true; + } + + @computed + public get query() { + return StrCast(this.props.Document.query); + } + + public set query(value: string) { + this.props.Document.query = value; + } + + @observable private _searchString: string = ""; + @observable private _resultsOpen: boolean = false; + @observable private _searchbarOpen: boolean = false; + @observable private _results: [Doc, string[], string[]][] = []; + private _resultsSet = new Map<Doc, number>(); + @observable private _openNoResults: boolean = false; + @observable private _visibleElements: JSX.Element[] = []; + + private resultsRef = React.createRef<HTMLDivElement>(); + public inputRef = React.createRef<HTMLInputElement>(); + + private _isSearch: ("search" | "placeholder" | undefined)[] = []; + private _numTotalResults = -1; + private _endIndex = -1; + + + private _maxSearchIndex: number = 0; + private _curRequest?: Promise<any> = undefined; + + @action + getViews = async (doc: Doc) => { + const results = await SearchUtil.GetViewsOfDocument(doc); + let toReturn: Doc[] = []; + await runInAction(() => { + toReturn = results; + }); + return toReturn; + } + + @action.bound + onChange(e: React.ChangeEvent<HTMLInputElement>) { + this._searchString = e.target.value; + + this._openNoResults = false; + this._results = []; + this._resultsSet.clear(); + this._visibleElements = []; + this._numTotalResults = -1; + this._endIndex = -1; + this._curRequest = undefined; + this._maxSearchIndex = 0; + } + + enter = async (e: React.KeyboardEvent) => { + console.log(e.key); + if (e.key === "Enter") { + const newdocs = await this.getAllResults(this.query); + console.log(newdocs.docs); + this.content = Docs.Create.TreeDocument(newdocs.docs, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs: "Results"` }); + + } + } + + + @action + submitSearch = async () => { + let query = this._searchString; + query = FilterBox.Instance.getFinalQuery(query); + this._results = []; + this._resultsSet.clear(); + this._isSearch = []; + this._visibleElements = []; + FilterBox.Instance.closeFilter(); + + //if there is no query there should be no result + if (query === "") { + return; + } + else { + this._endIndex = 12; + this._maxSearchIndex = 0; + this._numTotalResults = -1; + await this.getResults(query); + } + + runInAction(() => { + this._resultsOpen = true; + this._searchbarOpen = true; + this._openNoResults = true; + this.resultsScrolled(); + }); + } + + getAllResults = async (query: string) => { + return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 }); + } + + private get filterQuery() { + const types = FilterBox.Instance.filterTypes; + const includeDeleted = FilterBox.Instance.getDataStatus(); + return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted_b:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}" OR type_t:"extension"`).join(" ")})` : ""); + } + + + private NumResults = 25; + private lockPromise?: Promise<void>; + getResults = async (query: string) => { + if (this.lockPromise) { + await this.lockPromise; + } + this.lockPromise = new Promise(async res => { + while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) { + this._curRequest = SearchUtil.Search(query, true, { fq: this.filterQuery, start: this._maxSearchIndex, rows: this.NumResults, hl: true, "hl.fl": "*" }).then(action(async (res: SearchUtil.DocSearchResult) => { + + // happens at the beginning + if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { + this._numTotalResults = res.numFound; + } + + const highlighting = res.highlighting || {}; + const highlightList = res.docs.map(doc => highlighting[doc[Id]]); + const lines = new Map<string, string[]>(); + res.docs.map((doc, i) => lines.set(doc[Id], res.lines[i])); + const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc)); + const highlights: typeof res.highlighting = {}; + docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); + const filteredDocs = FilterBox.Instance.filterDocsByType(docs); + runInAction(() => { + // this._results.push(...filteredDocs); + filteredDocs.forEach(doc => { + const index = this._resultsSet.get(doc); + const highlight = highlights[doc[Id]]; + const line = lines.get(doc[Id]) || []; + const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : []; + if (index === undefined) { + this._resultsSet.set(doc, this._results.length); + this._results.push([doc, hlights, line]); + } else { + this._results[index][1].push(...hlights); + this._results[index][2].push(...line); + } + }); + }); + + this._curRequest = undefined; + })); + this._maxSearchIndex += this.NumResults; + + await this._curRequest; + } + this.resultsScrolled(); + res(); + }); + return this.lockPromise; + } + + collectionRef = React.createRef<HTMLSpanElement>(); + startDragCollection = async () => { + const res = await this.getAllResults(FilterBox.Instance.getFinalQuery(this._searchString)); + const filtered = FilterBox.Instance.filterDocsByType(res.docs); + // console.log(this._results) + const docs = filtered.map(doc => { + const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); + if (isProto) { + return Doc.MakeDelegate(doc); + } else { + return Doc.MakeAlias(doc); + } + }); + let x = 0; + let y = 0; + for (const doc of docs.map(d => Doc.Layout(d))) { + doc.x = x; + doc.y = y; + const size = 200; + const aspect = NumCast(doc._nativeHeight) / NumCast(doc._nativeWidth, 1); + if (aspect > 1) { + doc._height = size; + doc._width = size / aspect; + } else if (aspect > 0) { + doc._width = size; + doc._height = size * aspect; + } else { + doc._width = size; + doc._height = size; + } + x += 250; + if (x > 1000) { + x = 0; + y += 300; + } + } + //return Docs.Create.TreeDocument(docs, { _width: 200, _height: 400, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` }); + return Docs.Create.QueryDocument({ _width: 200, _height: 400, searchText: this._searchString, title: `Query Docs: "${this._searchString}"` }); + } + + @action.bound + openSearch(e: React.SyntheticEvent) { + e.stopPropagation(); + this._openNoResults = false; + FilterBox.Instance.closeFilter(); + this._resultsOpen = true; + this._searchbarOpen = true; + FilterBox.Instance._pointerTime = e.timeStamp; + } + + @action.bound + closeSearch = () => { + FilterBox.Instance.closeFilter(); + this.closeResults(); + this._searchbarOpen = false; + } + + @action.bound + closeResults() { + this._resultsOpen = false; + this._results = []; + this._resultsSet.clear(); + this._visibleElements = []; + this._numTotalResults = -1; + this._endIndex = -1; + this._curRequest = undefined; + } + + @action + resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => { + if (!this.resultsRef.current) return; + const scrollY = e ? e.currentTarget.scrollTop : this.resultsRef.current ? this.resultsRef.current.scrollTop : 0; + const itemHght = 53; + const startIndex = Math.floor(Math.max(0, scrollY / itemHght)); + const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this.resultsRef.current.getBoundingClientRect().height / itemHght))); + + this._endIndex = endIndex === -1 ? 12 : endIndex; + + if ((this._numTotalResults === 0 || this._results.length === 0) && this._openNoResults) { + this._visibleElements = [<div className="no-result">No Search Results</div>]; + return; + } + + if (this._numTotalResults <= this._maxSearchIndex) { + this._numTotalResults = this._results.length; + } + + // only hit right at the beginning + // visibleElements is all of the elements (even the ones you can't see) + else if (this._visibleElements.length !== this._numTotalResults) { + // undefined until a searchitem is put in there + this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults); + // indicates if things are placeholders + this._isSearch = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults); + } + + for (let i = 0; i < this._numTotalResults; i++) { + //if the index is out of the window then put a placeholder in + //should ones that have already been found get set to placeholders? + if (i < startIndex || i > endIndex) { + if (this._isSearch[i] !== "placeholder") { + this._isSearch[i] = "placeholder"; + this._visibleElements[i] = <div className="searchBox-placeholder" key={`searchBox-placeholder-${i}`}>Loading...</div>; + } + } + else { + if (this._isSearch[i] !== "search") { + let result: [Doc, string[], string[]] | undefined = undefined; + if (i >= this._results.length) { + this.getResults(this._searchString); + if (i < this._results.length) result = this._results[i]; + if (result) { + const highlights = Array.from([...Array.from(new Set(result[1]).values())]); + this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />; + this._isSearch[i] = "search"; + } + } + else { + result = this._results[i]; + if (result) { + const highlights = Array.from([...Array.from(new Set(result[1]).values())]); + this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />; + this._isSearch[i] = "search"; + } + } + } + } + } + if (this._maxSearchIndex >= this._numTotalResults) { + this._visibleElements.length = this._results.length; + this._isSearch.length = this._results.length; + } + } + + @computed + get resFull() { return this._numTotalResults <= 8; } + + @computed + get resultHeight() { return this._numTotalResults * 70; } + + render() { + const isEditing = this.editingMetadata; + return ( + <div style={{ pointerEvents: "all" }}> + <ContentFittingDocumentView {...this.props} + Document={this.content} + rootSelected={returnFalse} + getTransform={this.props.ScreenToLocalTransform}> + </ContentFittingDocumentView> + <div + style={{ + position: "absolute", + right: 0, + width: 20, + height: 20, + background: "black", + pointerEvents: "all", + opacity: 1, + transition: "0.4s opacity ease", + zIndex: 99, + top: 0, + }} + title={"Add Metadata"} + onClick={action(() => this.editingMetadata = !this.editingMetadata)} + /> + <div className="editableclass" onKeyPress={this.enter} style={{ opacity: isEditing ? 1 : 0, pointerEvents: isEditing ? "auto" : "none", transition: "0.4s opacity ease", position: "absolute", top: 0, left: 0, height: 20, width: "-webkit-fill-available" }}> + <EditableView + contents={this.query} + SetValue={this.updateKey} + GetValue={() => ""} + /> + </div> + </div > + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 8eb5c5050..8fb8e7516 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -10,7 +10,7 @@ import { Doc, DocListCast } from "../../new_fields/Doc"; import { Docs, } from "../documents/Documents"; import { StrCast, Cast } from "../../new_fields/Types"; import { CollectionTreeView } from "./collections/CollectionTreeView"; -import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath } from "../../Utils"; +import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZero } from "../../Utils"; import { Transform } from "../util/Transform"; import { ScriptField, ComputedField } from "../../new_fields/ScriptField"; import { Scripting } from "../util/Scripting"; @@ -124,44 +124,45 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />); templateMenu.push(<OtherToggle key={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />); if (noteTypesDoc) { - addedTypes.concat(noteTypes).map(template => template.treeViewChecked = ComputedField.MakeFunction("templateIsUsed(this, firstDoc)", { firstDoc: "string" }, { firstDoc: StrCast(firstDoc.title) })); + addedTypes.concat(noteTypes).map(template => template.treeViewChecked = ComputedField.MakeFunction(`templateIsUsed(this)`)); this._addedKeys && Array.from(this._addedKeys).filter(key => !noteTypes.some(nt => nt.title === key)).forEach(template => templateMenu.push( <OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />)); - templateMenu.push( - <CollectionTreeView - Document={Doc.UserDoc().templateDocs as Doc} - CollectionView={undefined} - ContainingCollectionDoc={undefined} - ContainingCollectionView={undefined} - onCheckedClick={this.scriptField!} - onChildClick={this.scriptField!} - LibraryPath={emptyPath} - dropAction={undefined} - active={returnTrue} - ContentScaling={returnOne} - bringToFront={emptyFunction} - focus={emptyFunction} - whenActiveChanged={emptyFunction} - ScreenToLocalTransform={Transform.Identity} - isSelected={returnFalse} - pinToPres={emptyFunction} - select={emptyFunction} - renderDepth={1} - addDocTab={returnFalse} - PanelWidth={this.return100} - PanelHeight={this.return100} - treeViewHideHeaderFields={true} - annotationsKey={""} - dontRegisterView={true} - fieldKey={"data"} - moveDocument={(doc: Doc) => false} - removeDocument={(doc: Doc) => false} - addDocument={(doc: Doc) => false} /> - ); } return <ul className="template-list" style={{ display: "block" }}> - <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress}></input> + <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress} /> {templateMenu} + <CollectionTreeView + Document={Doc.UserDoc().templateDocs as Doc} + CollectionView={undefined} + ContainingCollectionDoc={undefined} + ContainingCollectionView={undefined} + rootSelected={returnFalse} + onCheckedClick={this.scriptField!} + onChildClick={this.scriptField!} + LibraryPath={emptyPath} + dropAction={undefined} + active={returnTrue} + ContentScaling={returnOne} + bringToFront={emptyFunction} + focus={emptyFunction} + whenActiveChanged={emptyFunction} + ScreenToLocalTransform={Transform.Identity} + isSelected={returnFalse} + pinToPres={emptyFunction} + select={emptyFunction} + renderDepth={1} + addDocTab={returnFalse} + NativeHeight={returnZero} + NativeWidth={returnZero} + PanelWidth={this.return100} + PanelHeight={this.return100} + treeViewHideHeaderFields={true} + annotationsKey={""} + dontRegisterView={true} + fieldKey={"data"} + moveDocument={(doc: Doc) => false} + removeDocument={(doc: Doc) => false} + addDocument={(doc: Doc) => false} /> </ul>; } } @@ -174,10 +175,11 @@ Scripting.addGlobal(function switchView(doc: Doc, template: Doc) { return templateTitle && DocumentView.makeCustomViewClicked(doc, Docs.Create.FreeformDocument, templateTitle, template); }); -Scripting.addGlobal(function templateIsUsed(templateDoc: Doc, firstDocTitlte: string) { +Scripting.addGlobal(function templateIsUsed(templateDoc: Doc) { const firstDoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0].props.Document : undefined; - if (!firstDoc) return false; - const template = StrCast(templateDoc.dragFactory ? Cast(templateDoc.dragFactory, Doc, null)?.title : templateDoc.title); - return StrCast(firstDoc.layoutKey) === "layout_" + template ? 'check' : 'unchecked'; - // return SelectionManager.SelectedDocuments().some(view => StrCast(view.props.Document.layoutKey) === "layout_" + template) ? 'check' : 'unchecked' + if (firstDoc) { + const template = StrCast(templateDoc.dragFactory ? Cast(templateDoc.dragFactory, Doc, null)?.title : templateDoc.title); + return StrCast(firstDoc.layoutKey) === "layout_" + template ? 'check' : 'unchecked'; + } + return false; });
\ No newline at end of file diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx index bc3d07130..08310786b 100644 --- a/src/client/views/Touchable.tsx +++ b/src/client/views/Touchable.tsx @@ -42,7 +42,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> { if (pt.clientX === tPt.clientX && pt.clientY === tPt.clientY) { // pen is also a touch, but with a radius of 0.5 (at least with the surface pens) // and this seems to be the only way of differentiating pen and touch on touch events - if (pt.radiusX > 1 && pt.radiusY > 1) { + if ((pt as any).radiusX > 1 && (pt as any).radiusY > 1) { this.prevPoints.set(pt.identifier, pt); } } diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index fd1296286..a9a1898f5 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -13,6 +13,7 @@ height: calc(100% - 50px); display: inline-block; width: 100%; + user-select: none; } } .carouselView-back, .carouselView-fwd { diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index a0cb1fe19..9e7248db2 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -80,10 +80,33 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) }); } } + _downX = 0; + _downY = 0; + onPointerDown = (e: React.PointerEvent) => { + this._downX = e.clientX; + this._downY = e.clientY; + console.log("CAROUSEL down"); + document.addEventListener("pointerup", this.onpointerup); + } + private _lastTap: number = 0; + private _doubleTap = false; + onpointerup = (e: PointerEvent) => { + console.log("CAROUSEL up"); + this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); + this._lastTap = Date.now(); + } + + onClick = (e: React.MouseEvent) => { + if (this._doubleTap) { + e.stopPropagation(); + this.props.Document.isLightboxOpen = true; + } + } + render() { - return <div className="collectionCarouselView-outer" ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}> + return <div className="collectionCarouselView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}> {this.content} - {this.buttons} + {this.props.Document._chromeStatus !== "replaced" ? this.buttons : (null)} </div>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 4e1e76f39..28aaf0c57 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -15,7 +15,7 @@ import { FieldId } from "../../../new_fields/RefField"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { TraceMobx } from '../../../new_fields/util'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { emptyFunction, returnOne, returnTrue, Utils } from "../../../Utils"; +import { emptyFunction, returnOne, returnTrue, Utils, returnZero } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -788,11 +788,14 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { Document={document} DataDoc={resolvedDataDoc} bringToFront={emptyFunction} + rootSelected={returnTrue} addDocument={undefined} removeDocument={undefined} ContentScaling={this.contentScaling} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} + NativeHeight={returnZero} + NativeWidth={returnZero} ScreenToLocalTransform={this.ScreenToLocalTransform} renderDepth={0} parentActive={returnTrue} @@ -802,9 +805,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { addDocTab={this.addDocTab} pinToPres={DockedFrameRenderer.PinDoc} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne} />; + ContainingCollectionDoc={undefined} />; } render() { diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss index eae9e0220..123a27deb 100644 --- a/src/client/views/collections/CollectionLinearView.scss +++ b/src/client/views/collections/CollectionLinearView.scss @@ -8,6 +8,8 @@ display:flex; height: 100%; >label { + margin-top: "auto"; + margin-bottom: "auto"; background: $dark-color; color: $light-color; display: inline-block; diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index 344605412..cb0206260 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -3,8 +3,8 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc'; import { makeInterface } from '../../../new_fields/Schema'; -import { BoolCast, NumCast, StrCast, Cast } from '../../../new_fields/Types'; -import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../../Utils'; +import { BoolCast, NumCast, StrCast, Cast, ScriptCast } from '../../../new_fields/Types'; +import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils, returnFalse, returnZero } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { Transform } from '../../util/Transform'; import "./CollectionLinearView.scss"; @@ -13,7 +13,6 @@ import { CollectionSubView } from './CollectionSubView'; import { DocumentView } from '../nodes/DocumentView'; import { documentSchema } from '../../../new_fields/documentSchemas'; import { Id } from '../../../new_fields/FieldSymbols'; -import { ScriptField } from '../../../new_fields/ScriptField'; type LinearDocument = makeInterface<[typeof documentSchema,]>; @@ -28,12 +27,10 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { private _selectedDisposer?: IReactionDisposer; componentWillUnmount() { - this._dropDisposer && this._dropDisposer(); - this._widthDisposer && this._widthDisposer(); - this._selectedDisposer && this._selectedDisposer(); - this.childLayoutPairs.map((pair, ind) => { - Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log); - }); + this._dropDisposer?.(); + this._widthDisposer?.(); + this._selectedDisposer?.(); + this.childLayoutPairs.map((pair, ind) => ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log)); } componentDidMount() { @@ -54,11 +51,11 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { selected = pair; } else { - Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log); + ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log); } }); if (selected && selected.layout) { - Cast(selected.layout.proto?.onPointerDown, ScriptField)?.script.run({ this: selected.layout.proto }, console.log); + ScriptCast(selected.layout.proto?.onPointerDown)?.script.run({ this: selected.layout.proto }, console.log); } }), { fireImmediately: true } @@ -81,14 +78,16 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { render() { const guid = Utils.GenerateGuid(); const flexDir: any = StrCast(this.Document.flexDirection); + const backgroundColor = StrCast(this.props.Document.backgroundColor, "black"); + const color = StrCast(this.props.Document.color, "white"); 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}`} title="Close Menu" 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") }} > + <label htmlFor={`${guid}`} title="Close Menu" style={{ background: backgroundColor === color ? "black" : backgroundColor }} + onPointerDown={e => e.stopPropagation()} > <p>+</p> </label> + <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)} /> <div className="collectionLinearView-content" style={{ height: this.dimension(), flexDirection: flexDir }}> {this.childLayoutPairs.map((pair, ind) => { @@ -110,10 +109,13 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { moveDocument={this.props.moveDocument} addDocTab={this.props.addDocTab} pinToPres={emptyFunction} + rootSelected={this.props.isSelected} removeDocument={this.props.removeDocument} onClick={undefined} ScreenToLocalTransform={this.getTransform(dref)} ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={nested ? pair.layout[WidthSym] : () => this.dimension()}// ugh - need to get rid of this inline function to avoid recomputing PanelHeight={nested ? pair.layout[HeightSym] : () => this.dimension()} renderDepth={this.props.renderDepth + 1} @@ -123,10 +125,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne}> - </DocumentView> + ContainingCollectionDoc={undefined} /> </div>; })} </div> diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index af3e18a4b..b272151c1 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -36,6 +36,9 @@ interface CMVFieldRowProps { createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; setDocHeight: (key: string, thisHeight: number) => void; + observeHeight: (myref: any) => void; + unobserveHeight: (myref: any) => void; + showHandle: boolean; } @observer @@ -53,14 +56,19 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr private _contRef: React.RefObject<HTMLDivElement> = React.createRef(); private _sensitivity: number = 16; private _counter: number = 0; - + private _ele: any; createRowDropRef = (ele: HTMLDivElement | null) => { this._dropDisposer && this._dropDisposer(); if (ele) { + this._ele = ele; + this.props.observeHeight(ele); this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this)); } } + componentWillUnmount() { + this.props.unobserveHeight(this._ele); + } getTrueHeight = () => { if (this._collapsed) { @@ -293,7 +301,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this.props.parent.columnWidth}px`, ""), }}> {this.props.parent.children(this.props.docList)} - {this.props.parent.columnDragger} + {this.props.showHandle && this.props.parent.props.active() ? this.props.parent.columnDragger : (null)} </div> </div>; } diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index f124fe21b..ae71c54f7 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -159,6 +159,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { LibraryPath: [], dropAction: "alias", bringToFront: emptyFunction, + rootSelected: returnFalse, fieldKey: this.props.rowProps.column.id as string, ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, @@ -171,6 +172,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> { whenActiveChanged: emptyFunction, PanelHeight: returnZero, PanelWidth: returnZero, + NativeHeight: returnZero, + NativeWidth: returnZero, addDocTab: this.props.addDocTab, pinToPres: this.props.pinToPres, ContentScaling: returnOne diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 981438513..e835811c9 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -12,7 +12,7 @@ import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { ComputedField } from "../../../new_fields/ScriptField"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; import { Docs, DocumentOptions } from "../../documents/Documents"; import { Gateway } from "../../northstar/manager/Gateway"; import { CompileScript, Transformer, ts } from "../../util/Scripting"; @@ -28,7 +28,8 @@ import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionView } from "./CollectionView"; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { setupMoveUpEvents, emptyFunction } from "../../../Utils"; +import { setupMoveUpEvents, emptyFunction, returnZero, returnOne } from "../../../Utils"; +import { DocumentView } from "../nodes/DocumentView"; library.add(faCog, faPlus, faSortUp, faSortDown); library.add(faTable); @@ -117,26 +118,32 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get previewPanel() { - return <div ref={this.createTarget}> - <ContentFittingDocumentView - Document={this.previewDocument} - DataDocument={undefined} - LibraryPath={this.props.LibraryPath} - childDocs={this.childDocs} - renderDepth={this.props.renderDepth} - PanelWidth={this.previewWidth} - PanelHeight={this.previewHeight} - getTransform={this.getPreviewTransform} - CollectionDoc={this.props.CollectionView && this.props.CollectionView.props.Document} - CollectionView={this.props.CollectionView} - moveDocument={this.props.moveDocument} - addDocument={this.props.addDocument} - removeDocument={this.props.removeDocument} - active={this.props.active} - whenActiveChanged={this.props.whenActiveChanged} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - /> + return <div ref={this.createTarget} style={{ width: `${this.previewWidth()}px` }}> + {!this.previewDocument ? (null) : + <ContentFittingDocumentView + Document={this.previewDocument} + DataDocument={undefined} + NativeHeight={returnZero} + NativeWidth={returnZero} + fitToBox={true} + FreezeDimensions={true} + focus={emptyFunction} + LibraryPath={this.props.LibraryPath} + renderDepth={this.props.renderDepth} + rootSelected={this.rootSelected} + PanelWidth={this.previewWidth} + PanelHeight={this.previewHeight} + getTransform={this.getPreviewTransform} + CollectionDoc={this.props.CollectionView?.props.Document} + CollectionView={this.props.CollectionView} + moveDocument={this.props.moveDocument} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + active={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + />} </div>; } @@ -179,7 +186,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { render() { return <div className="collectionSchemaView-container"> - <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} ref={this.createTarget}> + <div className="collectionSchemaView-tableContainer" style={{ width: `calc(100% - ${this.previewWidth()}px)` }} onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} ref={this.createTarget}> {this.schemaTable} </div> {this.dividerDragger} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index bfa5ea278..47faa9239 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -26,6 +26,9 @@ position: relative; display: block; } + .collectionStackingViewFieldColumn { + height:max-content; + } .collectionSchemaView-previewDoc { height: 100%; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 076dd3629..b066f2be3 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -11,7 +11,7 @@ import { listSpec } from "../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../new_fields/Types"; import { TraceMobx } from "../../../new_fields/util"; -import { Utils, setupMoveUpEvents, emptyFunction } from "../../../Utils"; +import { Utils, setupMoveUpEvents, emptyFunction, returnZero, returnOne } from "../../../Utils"; import { DragManager, dropActionType } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -24,13 +24,13 @@ import "./CollectionStackingView.scss"; import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionViewType } from "./CollectionView"; -import { Docs } from "../../documents/Documents"; +import { SelectionManager } from "../../util/SelectionManager"; +const _global = (window /* browser */ || global /* node */) as any; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { _masonryGridRef: HTMLDivElement | null = null; _draggerRef = React.createRef<HTMLDivElement>(); - _heightDisposer?: IReactionDisposer; _pivotFieldDisposer?: IReactionDisposer; _docXfs: any[] = []; _columnStart: number = 0; @@ -47,12 +47,14 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } @computed get showAddAGroup() { return (this.pivotField && (this.props.Document._chromeStatus !== 'view-mode' && this.props.Document._chromeStatus !== 'disabled')); } @computed get columnWidth() { + TraceMobx(); 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)); } @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } children(docs: Doc[], columns?: number) { + TraceMobx(); this._docXfs.length = 0; return docs.map((d, i) => { const height = () => this.getDocHeight(d); @@ -112,34 +114,21 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { return fields; } + getSimpleDocHeight(d?: Doc) { + if (!d) return 0; + const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); + const nw = NumCast(layoutDoc._nativeWidth); + const nh = NumCast(layoutDoc._nativeHeight); + let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); + if (!layoutDoc._fitWidth && nw && nh) { + const aspect = nw && nh ? nh / nw : 1; + if (!(this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); + return wid * aspect; + } + return layoutDoc._fitWidth ? wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1) : layoutDoc[HeightSym](); + } componentDidMount() { super.componentDidMount(); - this._heightDisposer = reaction(() => { - if (this.props.Document._autoHeight) { - const sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]); - if (this.isStackingView) { - const res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => { - const r1 = Math.max(maxHght, - (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => { - const val = height + this.getDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap); - return val; - }, this.yMargin)); - return r1; - }, 0); - return res; - } else { - const sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0); - return this.props.ContentScaling() * (sum + (this.Sections.size ? (this.props.Document.miniHeaders ? 20 : 85) : -15)); - } - } - return -1; - }, - (hgt: number) => { - const doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; - doc && hgt > 0 && (Doc.Layout(doc)._height = hgt); - }, - { fireImmediately: true } - ); // reset section headers when a new filter is inputted this._pivotFieldDisposer = reaction( @@ -149,7 +138,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } componentWillUnmount() { super.componentWillUnmount(); - this._heightDisposer?.(); this._pivotFieldDisposer?.(); } @@ -174,15 +162,19 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { backgroundColor={this.props.backgroundColor} LayoutDoc={this.props.childLayoutTemplate} LibraryPath={this.props.LibraryPath} + FreezeDimensions={this.props.freezeChildDimensions} renderDepth={this.props.renderDepth + 1} - fitToBox={this.props.fitToBox} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} - onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler} PanelWidth={width} PanelHeight={height} + NativeHeight={returnZero} + NativeWidth={returnZero} + fitToBox={BoolCast(this.props.Document._freezeChildDimensions)} + rootSelected={this.rootSelected} + dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler} getTransform={dxf} focus={this.props.focus} - CollectionDoc={this.props.CollectionView && this.props.CollectionView.props.Document} + CollectionDoc={this.props.CollectionView?.props.Document} CollectionView={this.props.CollectionView} addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} @@ -286,6 +278,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { }); } headings = () => Array.from(this.Sections); + refList: any[] = []; sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.pivotField; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; @@ -296,6 +289,19 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const cols = () => this.isStackingView ? 1 : Math.max(1, Math.min(this.filteredChildren.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); return <CollectionStackingViewFieldColumn + unobserveHeight={(ref) => this.refList.splice(this.refList.indexOf(ref), 1)} + observeHeight={(ref) => { + if (ref) { + this.refList.push(ref); + const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; + this.observer = new _global.ResizeObserver(action((entries: any) => { + if (this.props.Document._autoHeight && ref && this.refList.length && !SelectionManager.GetIsDragging()) { + Doc.Layout(doc)._height = Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", "")))); + } + })); + this.observer.observe(ref); + } + }} key={heading ? heading.heading : ""} cols={cols} headings={this.headings} @@ -314,14 +320,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const y = this._scroll; // required for document decorations to update when the text box container is scrolled const { scale, translateX, translateY } = Utils.GetScreenTransform(dref); const outerXf = Utils.GetScreenTransform(this._masonryGridRef!); - const scaling = 1 / Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]()); const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - const offsetx = (doc[WidthSym]() - doc[WidthSym]() / scaling) / 2; const offsety = (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0); - return this.props.ScreenToLocalTransform().translate(offset[0] - offsetx, offset[1] + offsety).scale(scaling); + return this.props.ScreenToLocalTransform().translate(offset[0], offset[1] + offsety); } - sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { + sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[], first: boolean) => { 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]); @@ -331,6 +335,20 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const rows = () => !this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); return <CollectionMasonryViewFieldRow + showHandle={first} + unobserveHeight={(ref) => this.refList.splice(this.refList.indexOf(ref), 1)} + observeHeight={(ref) => { + if (ref) { + this.refList.push(ref); + const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; + this.observer = new _global.ResizeObserver(action((entries: any) => { + if (this.props.Document._autoHeight && ref && this.refList.length && !SelectionManager.GetIsDragging()) { + Doc.Layout(doc)._height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0) + } + })); + this.observer.observe(ref); + } + }} key={heading ? heading.heading : ""} rows={rows} headings={this.headings} @@ -383,11 +401,16 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const entries = Array.from(this.Sections.entries()); sections = entries.sort(this.sortFunc); } - return sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1])); + return sections.map((section, i) => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1], i === 0)); } - @computed get scaling() { return !this.props.Document._nativeWidth ? 1 : this.props.PanelHeight() / NumCast(this.props.Document._nativeHeight); } + @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth) || this.props.NativeWidth() || 0; } + @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight) || this.props.NativeHeight() || 0; } + + @computed get scaling() { return !this.nativeWidth ? 1 : this.props.PanelHeight() / this.nativeHeight; } + + observer: any; render() { TraceMobx(); const editableViewProps = { diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 0a48c95e4..5d926b7c7 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -39,6 +39,8 @@ interface CSVFieldColumnProps { type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined; createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; + observeHeight: (myref: any) => void; + unobserveHeight: (myref: any) => void; } @observer @@ -50,13 +52,19 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + _ele: HTMLElement | null = null; createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); if (ele) { + this._ele = ele; + this.props.observeHeight(ele); this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this)); } } + componentWillUnmount() { + this.props.unobserveHeight(this._ele); + } @undoBatch columnDrop = action((e: Event, de: DragManager.DropEvent) => { @@ -353,7 +361,12 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; const chromeStatus = this.props.parent.props.Document._chromeStatus; return ( - <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }} + <div className="collectionStackingViewFieldColumn" key={heading} + style={{ + width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, + height: SelectionManager.GetIsDragging() ? "100%" : undefined, + background: this._background + }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> {this.props.parent.Document.hideHeadings ? (null) : headingView} { diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 70927cf22..746b2e174 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -34,13 +34,17 @@ export interface CollectionViewProps extends FieldViewProps { PanelHeight: () => number; VisibleHeight?: () => number; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; + rootSelected: (outsideReaction?: boolean) => boolean; fieldKey: string; + NativeWidth: () => number; + NativeHeight: () => number; } export interface SubCollectionViewProps extends CollectionViewProps { CollectionView: Opt<CollectionView>; children?: never | (() => JSX.Element[]) | React.ReactNode; - overrideDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explict list (see LinkBox) + freezeChildDimensions?: boolean; // used by TimeView to coerce documents to treat their width height as their native width/height + overrideDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox) ignoreFields?: string[]; // used in TreeView to ignore specified fields (see LinkBox) isAnnotationOverlay?: boolean; annotationsKey: string; @@ -95,6 +99,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: this.props.Document.resolvedDataDoc ? this.props.Document : Doc.GetProto(this.props.Document)); // if the layout document has a resolvedDataDoc, then we don't want to get its parent which would be the unexpanded template } + rootSelected = (outsideReaction: boolean) => { + return this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument || this.props.Document.forceActive ? this.props.rootSelected(outsideReaction) : false); + } + // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc. // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through // to its children which may be templates. @@ -208,9 +216,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: this.props.Document.dropConverter.script.run({ dragData: docDragData }); /// bcz: check this if (docDragData) { let added = false; - if (this.props.Document._freezeOnDrop) { - de.complete.docDragData?.droppedDocuments.forEach(drop => Doc.freezeNativeDimensions(drop, drop[WidthSym](), drop[HeightSym]())); - } if (docDragData.dropAction || docDragData.userDropAction) { added = docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } else if (docDragData.moveDocument) { diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 4f77e8b0e..53de2fbbe 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -5,7 +5,7 @@ import { List } from "../../../new_fields/List"; import { ObjectField } from "../../../new_fields/ObjectField"; import { RichTextField } from "../../../new_fields/RichTextField"; import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; -import { NumCast, StrCast } from "../../../new_fields/Types"; +import { NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils"; import { Scripting } from "../../util/Scripting"; import { ContextMenu } from "../ContextMenu"; @@ -29,10 +29,9 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { 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; - const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailView'); alias.layoutKey='layout_detailedView'; 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" }); + const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(thisContainer.childDetailed, alias, 'layout_detailView'); alias.layoutKey='layout_detailedView'; alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; + this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "string", thisContainer: Doc.name, shiftKey: "boolean" }); this.props.Document._fitToBox = true; if (!this.props.Document.onViewDefClick) { this.props.Document.onViewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" }); @@ -73,7 +72,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { @computed get contents() { return <div className="collectionTimeView-innards" key="timeline" style={{ width: "100%" }}> - <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} /> + <CollectionFreeFormView {...this.props} freezeChildDimensions={BoolCast(this.layoutDoc._freezeChildDimensions, true)} layoutEngine={this.layoutEngine} /> </div>; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 6ee48f11b..7edda5a4f 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -103,7 +103,7 @@ class TreeView extends React.Component<TreeViewProps> { set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; } @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); } - @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); } + @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } @computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; } @computed get fieldKey() { const splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\'"); @@ -267,8 +267,9 @@ class TreeView extends React.Component<TreeViewProps> { docTransform = () => { const { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!); const outerXf = this.props.outerXf(); - const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1] + (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0)); + const offset = this.props.ScreenToLocalTransform().transformDirection((outerXf.translateX - translateX), outerXf.translateY - translateY); + const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); + return finalXf; } getTransform = () => { @@ -280,7 +281,7 @@ class TreeView extends React.Component<TreeViewProps> { } docWidth = () => { const layoutDoc = Doc.Layout(this.props.document); - const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth); + const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; } @@ -288,7 +289,7 @@ class TreeView extends React.Component<TreeViewProps> { const layoutDoc = Doc.Layout(this.props.document); const bounds = this.boundsOfCollectionDocument; return Math.min(this.MAX_EMBED_HEIGHT, (() => { - const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth, 1); + const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); if (aspect) return this.docWidth() * aspect; if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); return layoutDoc._fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection._height) : @@ -373,8 +374,10 @@ class TreeView extends React.Component<TreeViewProps> { DataDocument={this.templateDataDoc} LibraryPath={emptyPath} renderDepth={this.props.renderDepth + 1} + rootSelected={returnTrue} backgroundColor={this.props.backgroundColor} fitToBox={this.boundsOfCollectionDocument !== undefined} + FreezeDimensions={true} PanelWidth={this.docWidth} PanelHeight={this.docHeight} getTransform={this.docTransform} @@ -454,6 +457,7 @@ class TreeView extends React.Component<TreeViewProps> { LibraryPath={this.props.libraryPath || []} addDocument={undefined} addDocTab={this.props.addDocTab} + rootSelected={returnTrue} pinToPres={emptyFunction} onClick={this.props.onChildClick || editTitle} dropAction={this.props.dropAction} @@ -463,6 +467,8 @@ class TreeView extends React.Component<TreeViewProps> { ContentScaling={returnOne} PanelWidth={returnZero} PanelHeight={returnZero} + NativeHeight={returnZero} + NativeWidth={returnZero} renderDepth={1} focus={emptyFunction} parentActive={returnTrue} @@ -471,8 +477,6 @@ class TreeView extends React.Component<TreeViewProps> { dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildren)} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne} />} </div > {this.props.treeViewHideHeaderFields() ? (null) : headerElements} diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index df1770ffe..c7ab66c9f 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -10,10 +10,10 @@ import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be im import { DateField } from '../../../new_fields/DateField'; import { DataSym, Doc, DocListCast, Field, Opt } from '../../../new_fields/Doc'; import { List } from '../../../new_fields/List'; -import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; +import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../new_fields/Types'; import { ImageField } from '../../../new_fields/URLField'; import { TraceMobx } from '../../../new_fields/util'; -import { Utils, setupMoveUpEvents, returnFalse } from '../../../Utils'; +import { Utils, setupMoveUpEvents, returnFalse, returnZero } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; @@ -42,6 +42,8 @@ import { Id } from '../../../new_fields/FieldSymbols'; import { listSpec } from '../../../new_fields/Schema'; import { Docs } from '../../documents/Documents'; import { ScriptField, ComputedField } from '../../../new_fields/ScriptField'; +import { InteractionUtils } from '../../util/InteractionUtils'; +import { ObjectField } from '../../../new_fields/ObjectField'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -99,11 +101,14 @@ export class CollectionView extends Touchable<FieldViewProps> { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); } private _isChildActive = false; //TODO should this be observable? - @observable private _isLightboxOpen = false; + get _isLightboxOpen() { return BoolCast(this.props.Document.isLightboxOpen); } + set _isLightboxOpen(value) { this.props.Document.isLightboxOpen = value; } @observable private _curLightboxImg = 0; @observable private static _safeMode = false; public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + get collectionViewType(): CollectionViewType | undefined { const viewField = NumCast(this.props.Document._viewType); if (CollectionView._safeMode) { @@ -117,9 +122,9 @@ export class CollectionView extends Touchable<FieldViewProps> { return viewField; } - active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; + active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.rootSelected(outsideReaction) && BoolCast(this.props.Document.forceActive)) || this._isChildActive || this.props.renderDepth === 0; - whenActiveChanged = (isActive: boolean) => { this.props.whenActiveChanged(this._isChildActive = isActive); }; + whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); @action.bound addDocument(doc: Doc): boolean { @@ -158,6 +163,7 @@ export class CollectionView extends Touchable<FieldViewProps> { // moving it into the target. @action.bound moveDocument(doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean { + doc.context = targetCollection; if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { return true; } @@ -244,6 +250,8 @@ export class CollectionView extends Touchable<FieldViewProps> { if (this.props.Document.childDetailed instanceof Doc) { layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailed as Doc, "onRight"), icon: "project-diagram" }); } + layoutItems.push({ description: "Toggle is inPlace Container", event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); + !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); const open = ContextMenu.Instance.findByDescription("Open..."); @@ -252,7 +260,15 @@ export class CollectionView extends Touchable<FieldViewProps> { const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; - onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) }); + const funcs = [{ key: "onChildClick", name: "On Child Clicked", script: undefined as any as ScriptField }]; + DocListCast(Cast(Doc.UserDoc().childClickFuncs, Doc, null).data).forEach(childClick => + funcs.push({ key: "onChildClick", name: StrCast(childClick.title), script: ScriptCast(childClick.script) })); + funcs.map(func => onClicks.push({ + description: `Edit ${func.name} script`, icon: "edit", event: (obj: any) => { + func.script && (this.props.Document[func.key] = ObjectField.MakeCopy(func.script)); + ScriptBox.EditButtonScript(func.name + "...", this.props.Document, func.key, obj.x, obj.y, { thisContainer: Doc.name }); + } + })); !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); const more = ContextMenu.Instance.findByDescription("More..."); @@ -282,7 +298,7 @@ export class CollectionView extends Touchable<FieldViewProps> { bodyPanelWidth = () => this.props.PanelWidth() - this.facetWidth(); getTransform = () => this.props.ScreenToLocalTransform().translate(-this.facetWidth(), 0); - facetWidth = () => Math.min(this.props.PanelWidth() - 25, this._facetWidth); + facetWidth = () => Math.max(0, Math.min(this.props.PanelWidth() - 25, this._facetWidth)); @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : @@ -307,8 +323,8 @@ export class CollectionView extends Touchable<FieldViewProps> { get childDocs() { const dfield = this.dataField; const rawdocs = (dfield instanceof Doc) ? [dfield] : Cast(dfield, listSpec(Doc), Cast(this.props.Document.rootDocument, Doc, null) ? [Cast(this.props.Document.rootDocument, Doc, null)] : []); - const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc); - const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); + const docs = rawdocs.filter(d => d && !(d instanceof Promise)).map(d => d as Doc); + const viewSpecScript = ScriptCast(this.props.Document.viewSpecScript); return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; } @computed get _allFacets() { @@ -394,6 +410,7 @@ export class CollectionView extends Touchable<FieldViewProps> { const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; return ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); } + @computed get treeIgnoreFields() { return ["_facetCollection", "_docFilters"]; } @computed get filterView() { const facetCollection = this.props.Document; const flyout = ( @@ -419,9 +436,11 @@ export class CollectionView extends Touchable<FieldViewProps> { <CollectionTreeView {...this.props} CollectionView={this} treeViewHideTitle={true} + NativeHeight={returnZero} + NativeWidth={returnZero} treeViewHideHeaderFields={true} onCheckedClick={this.scriptField!} - ignoreFields={["_facetCollection", "_docFilters"]} + ignoreFields={this.treeIgnoreFields} annotationsKey={""} dontRegisterView={true} PanelWidth={this.facetWidth} @@ -463,7 +482,7 @@ export class CollectionView extends Touchable<FieldViewProps> { Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href : ""))} - {!this.props.isSelected() || this.props.PanelHeight() < 100 ? (null) : + {!this.props.isSelected() || this.props.PanelHeight() < 100 || this.props.Document.hideFilterView ? (null) : <div className="collectionTimeView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this.facetWidth()}px, 0px)` }} > <span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} /> </div> diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 2d565d9db..9bade1c82 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -18,6 +18,7 @@ import { CollectionView } from "./CollectionView"; import "./CollectionViewChromes.scss"; import * as Autosuggest from 'react-autosuggest'; import KeyRestrictionRow from "./KeyRestrictionRow"; +import { ObjectField } from "../../../new_fields/ObjectField"; const datepicker = require('js-datepicker'); interface CollectionViewChromeProps { @@ -42,20 +43,20 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro get target() { return this.props.CollectionView.props.Document; } _templateCommand = { params: ["target", "source"], title: "=> item view", - script: "setChildLayout(this.target, this.source?.[0])", - immediate: (source: Doc[]) => Doc.setChildLayout(this.target, source?.[0]), + script: "target.childLayout = getDocTemplate(this.source?.[0])", + immediate: (source: Doc[]) => this.target.childLayout = Doc.getDocTemplate(source?.[0]), initialize: emptyFunction, }; _narrativeCommand = { params: ["target", "source"], title: "=> click item view", - script: "setChildDetailedLayout(this.target, this.source?.[0])", - immediate: (source: Doc[]) => Doc.setChildDetailedLayout(this.target, source?.[0]), + script: "this.target.childDetailed = getDocTemplate(this.source?.[0])", + immediate: (source: Doc[]) => this.target.childDetailed = Doc.getDocTemplate(source?.[0]), initialize: emptyFunction, }; _contentCommand = { params: ["target", "source"], title: "=> content", - script: "getProto(this.target).data = aliasDocs(this.source);", - immediate: (source: Doc[]) => Doc.GetProto(this.target).data = Doc.aliasDocs(source), + script: "getProto(this.target).data = copyField(this.source);", + immediate: (source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source), // Doc.aliasDocs(source), initialize: emptyFunction, }; _viewCommand = { @@ -352,7 +353,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro const c = { params: ["target"], title: CollectionViewType.stringFor(vtype), script: `this.target._viewType = ${NumCast(this.props.CollectionView.props.Document._viewType)}`, - immediate: (source: Doc[]) => Doc.setChildLayout(this.target, source?.[0]), + immediate: (source: Doc[]) => this.target = Doc.getTemplateDoc(source?.[0]), initialize: emptyFunction, }; DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 35e3a8958..afe269ec3 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -107,7 +107,6 @@ export class DockingViewButtonSelector extends React.Component<{ views: Document } @computed get flyout() { - trace(); return ( <div className="ParentDocumentSelector-flyout" title=" "> <DocumentButtonBar views={this.props.views} stack={this.props.Stack} /> @@ -116,7 +115,6 @@ export class DockingViewButtonSelector extends React.Component<{ views: Document } render() { - trace(); return <span title="Tap for menu, drag tab as document" onPointerDown={e => { this.props.views[0].select(false); e.stopPropagation(); }} className="buttonSelector"> <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> <FontAwesomeIcon icon={"cog"} size={"sm"} /> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 2871fe192..f12dd76d8 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -15,7 +15,7 @@ import { ScriptField } from "../../../../new_fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../new_fields/Types"; import { TraceMobx } from "../../../../new_fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { aggregateBounds, intersectRect, returnOne, Utils } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnOne, Utils, returnZero } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; @@ -88,9 +88,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; } @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } - @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc.xPadding, 10), NumCast(this.layoutDoc.yPadding, 10)); } - @computed get nativeWidth() { return this.Document._fitToContent ? 0 : NumCast(this.Document._nativeWidth); } - @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight); } + @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); } + @computed get nativeWidth() { return this.fitToContent ? 0 : NumCast(this.Document._nativeWidth, this.props.NativeWidth()); } + @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight, this.props.NativeHeight()); } private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } private easing = () => this.props.Document.panTransformType === "Ease"; @@ -99,6 +99,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private zoomScaling = () => (1 / this.parentScaling) * (this.fitToContent ? Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : this.Document.scale || 1) + private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); @@ -815,12 +816,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); } - zoomToScale = (scale: number) => { - this.Document.scale = scale; - } - - getScale = () => this.Document.scale || 1; - @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } backgroundHalo = () => BoolCast(this.Document.useClusters); @@ -828,10 +823,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { ...this.props, + NativeHeight: returnZero, + NativeWidth: returnZero, + fitToBox: false, DataDoc: childData, Document: childLayout, LibraryPath: this.libraryPath, + FreezeDimensions: this.props.freezeChildDimensions, layoutKey: undefined, + rootSelected: this.rootSelected, dropAction: StrCast(this.props.Document.childDropAction) as dropActionType, //onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them onClick: this.onChildClickHandler, @@ -847,11 +847,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { backgroundHalo: this.backgroundHalo, parentActive: this.props.active, bringToFront: this.bringToFront, - zoomToScale: this.zoomToScale, - getScale: this.getScale + addDocTab: this.addDocTab, }; } + addDocTab = (doc: Doc, where: string) => { + if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { + this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); + return true; + } + return this.props.addDocTab(doc, where); + } getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): PoolData { const result = this.Document.arrangeScript?.script.run(params, console.log); if (result?.success) { @@ -950,11 +956,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const elements: ViewDefResult[] = computedElementData.slice(); this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair => elements.push({ - ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)} + ele: <CollectionFreeFormDocumentView + key={pair.layout[Id]} + {...this.getChildDocumentViewProps(pair.layout, pair.data)} dataProvider={this.childDataProvider} LayoutDoc={this.childLayoutDocFunc} jitterRotation={NumCast(this.props.Document.jitterRotation)} - fitToBox={this.props.fitToBox || this.props.layoutEngine !== undefined} />, + fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} + FreezeDimensions={BoolCast(this.props.freezeChildDimensions)} + />, bounds: this.childDataProvider(pair.layout) })); @@ -1073,7 +1083,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (doc instanceof Doc) { const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); doc.x = xx, doc.y = yy; - this.props.addDocument && this.props.addDocument(doc); + this.props.addDocument?.(doc); } } } @@ -1116,8 +1126,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @computed get contentScaling() { if (this.props.annotationsKey) return 0; - const hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1; - const wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1; + const nw = NumCast(this.Document._nativeWidth, this.props.NativeWidth()); + const nh = NumCast(this.Document._nativeHeight, this.props.NativeHeight()); + const hscale = nh ? this.props.PanelHeight() / nh : 1; + const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; } render() { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 276a49570..503df10c2 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -5,7 +5,6 @@ import { InkField, InkData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; import { Cast, NumCast, FieldValue, StrCast } from "../../../../new_fields/Types"; -import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { Utils } from "../../../../Utils"; import { Docs, DocUtils } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index aa8e1fb43..7e511ae34 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -14,6 +14,7 @@ import "./collectionMulticolumnView.scss"; import ResizeBar from './MulticolumnResizer'; import WidthLabel from './MulticolumnWidthLabel'; import { List } from '../../../../new_fields/List'; +import { returnZero } from '../../../../Utils'; type MulticolumnDocument = makeInterface<[typeof documentSchema]>; const MulticolumnDocument = makeInterface(documentSchema); @@ -208,6 +209,10 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu {...this.props} Document={layout} DataDocument={layout.resolvedDataDoc as Doc} + NativeHeight={returnZero} + NativeWidth={returnZero} + fitToBox={BoolCast(this.props.Document._freezeChildDimensions)} + FreezeDimensions={BoolCast(this.props.Document._freezeChildDimensions)} backgroundColor={this.props.backgroundColor} CollectionDoc={this.props.Document} PanelWidth={width} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 5e59f8237..daf1fda6c 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -6,7 +6,7 @@ import * as React from "react"; import { Doc } from '../../../../new_fields/Doc'; import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types'; import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; -import { Utils } from '../../../../Utils'; +import { Utils, returnZero } from '../../../../Utils'; import "./collectionMultirowView.scss"; import { computed, trace, observable, action } from 'mobx'; import { Transform } from '../../../util/Transform'; @@ -208,6 +208,10 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) {...this.props} Document={layout} DataDocument={layout.resolvedDataDoc as Doc} + NativeHeight={returnZero} + NativeWidth={returnZero} + fitToBox={BoolCast(this.props.Document._freezeChildDimensions)} + FreezeDimensions={BoolCast(this.props.Document._freezeChildDimensions)} backgroundColor={this.props.backgroundColor} CollectionDoc={this.props.Document} PanelWidth={width} diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index 019f931f9..9d3d2e592 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -21,7 +21,7 @@ serif; // misc values $border-radius: 0.3em; // -$search-thumnail-size: 175; +$search-thumnail-size: 130; // dragged items $contextMenu-zindex: 100000; // context menu shows up over everything diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 03b2a2297..ff9630273 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -7,7 +7,7 @@ import { AudioField, nullAudio } from "../../../new_fields/URLField"; import { DocExtendableComponent } from "../DocComponent"; import { makeInterface, createSchema } from "../../../new_fields/Schema"; import { documentSchema } from "../../../new_fields/documentSchemas"; -import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent } from "../../../Utils"; +import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent, returnFalse, returnZero } from "../../../Utils"; import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx"; import { DateField } from "../../../new_fields/DateField"; import { SelectionManager } from "../../util/SelectionManager"; @@ -71,7 +71,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume scrollLinkId => { 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.anchor1_timecode) : NumCast(l.anchor2_timecode); + const linkTime = Doc.AreProtosEqual(l.anchor1 as Doc, this.dataDoc) ? NumCast((l.anchor1 as Doc).timecode) : NumCast((l.anchor2 as Doc).timecode); setTimeout(() => { this.playFromTime(linkTime); Doc.linkFollowHighlight(l); }, 250); }); Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false); @@ -92,10 +92,11 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration); DocListCast(this.dataDoc.links).map(l => { let la1 = l.anchor1 as Doc; - let linkTime = NumCast(l.anchor2_timecode); + const la2 = l.anchor2 as Doc; + let linkTime = NumCast(la2.timecode); if (Doc.AreProtosEqual(la1, this.dataDoc)) { + linkTime = NumCast(la1.timecode); la1 = l.anchor2 as Doc; - linkTime = NumCast(l.anchor1_timecode); } if (linkTime > NumCast(this.Document.currentTimecode) && linkTime < htmlEle.currentTime) { Doc.linkFollowHighlight(la1); @@ -249,18 +250,24 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume {DocListCast(this.dataDoc.links).map((l, i) => { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - let linkTime = NumCast(l.anchor2_timecode); + let linkTime = NumCast(la2.timecode); if (Doc.AreProtosEqual(la1, this.dataDoc)) { la1 = l.anchor2 as Doc; la2 = l.anchor1 as Doc; - linkTime = NumCast(l.anchor1_timecode); + linkTime = NumCast(la1.timecode); } return !linkTime ? (null) : <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)} + <DocumentView {...this.props} + Document={l} + NativeHeight={returnZero} + NativeWidth={returnZero} + rootSelected={returnFalse} + layoutKey={Doc.LinkEndpoint(l, la2)} ContainingCollectionDoc={this.props.Document} - parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne} + parentActive={returnTrue} + bringToFront={emptyFunction} backgroundColor={returnTransparent} /> </div> <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)} diff --git a/src/client/views/nodes/ButtonBox.scss b/src/client/views/nodes/ButtonBox.scss index 7c3825978..293af289d 100644 --- a/src/client/views/nodes/ButtonBox.scss +++ b/src/client/views/nodes/ButtonBox.scss @@ -11,18 +11,19 @@ width: 100%; height: 100%; border-radius: inherit; - text-align: center; - display: table; - overflow: hidden; - text-overflow: ellipsis; letter-spacing: 2px; text-transform: uppercase; + overflow: hidden; + display:flex; } .buttonBox-mainButtonCenter { - height: 100%; - display: table-cell; - vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline; + align-items: center; + margin: auto; } .buttonBox-params { @@ -33,4 +34,5 @@ .buttonBox-missingParam { width: 100%; background: lightgray; + border: dimGray solid 1px; }
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index eaab4086c..f9f5f449c 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -10,7 +10,6 @@ import { DocumentView, DocumentViewProps } from "./DocumentView"; import React = require("react"); import { PositionDocument } from "../../../new_fields/documentSchemas"; import { TraceMobx } from "../../../new_fields/util"; -import { returnFalse } from "../../../Utils"; import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { @@ -41,9 +40,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const hgt = this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym](); return (hgt === undefined && this.nativeWidth && this.nativeHeight) ? this.width * this.nativeHeight / this.nativeWidth : hgt; } + @computed get freezeDimensions() { return this.props.FreezeDimensions; } @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document) ? this.props.dataProvider(this.props.Document) : undefined; } - @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth); } - @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight); } + @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth, this.props.NativeWidth() || (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); } + @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight, this.props.NativeHeight() || (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0)); } @computed get renderScriptDim() { if (this.Document.renderScript) { @@ -60,14 +60,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF return undefined; } - contentScaling = () => this.nativeWidth > 0 && !this.props.fitToBox ? this.width / this.nativeWidth : 1; - panelWidth = () => (this.dataProvider?.width || this.props.PanelWidth()); - panelHeight = () => (this.dataProvider?.height || this.props.PanelHeight()); + contentScaling = () => this.nativeWidth > 0 && !this.props.fitToBox && !this.freezeDimensions ? this.width / this.nativeWidth : 1; + panelWidth = () => (this.dataProvider?.width || this.props.PanelWidth?.()); + panelHeight = () => (this.dataProvider?.height || this.props.PanelHeight?.()); getTransform = (): Transform => this.props.ScreenToLocalTransform() .translate(-this.X, -this.Y) .scale(1 / this.contentScaling()) focusDoc = (doc: Doc) => this.props.focus(doc, false); + NativeWidth = () => this.nativeWidth; + NativeHeight = () => this.nativeHeight; render() { TraceMobx(); return <div className="collectionFreeFormDocumentView-container" @@ -89,21 +91,26 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF pointerEvents: this.props.Document.isBackground ? "none" : undefined }} > - {!this.props.fitToBox ? <DocumentView {...this.props} - dragDivName={"collectionFreeFormDocumentView-container"} - ContentScaling={this.contentScaling} - ScreenToLocalTransform={this.getTransform} - backgroundColor={this.props.backgroundColor} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - /> : <ContentFittingDocumentView {...this.props} - CollectionDoc={this.props.ContainingCollectionDoc} - DataDocument={this.props.DataDoc} - getTransform={this.getTransform} - active={returnFalse} - focus={this.focusDoc} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} + {!this.props.fitToBox ? + <DocumentView {...this.props} + dragDivName={"collectionFreeFormDocumentView-container"} + ContentScaling={this.contentScaling} + ScreenToLocalTransform={this.getTransform} + backgroundColor={this.props.backgroundColor} + NativeHeight={this.NativeHeight} + NativeWidth={this.NativeWidth} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} /> + : <ContentFittingDocumentView {...this.props} + CollectionDoc={this.props.ContainingCollectionDoc} + DataDocument={this.props.DataDoc} + getTransform={this.getTransform} + NativeHeight={this.NativeHeight} + NativeWidth={this.NativeWidth} + active={this.props.parentActive} + focus={this.focusDoc} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} />} </div>; } diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index 9494a4bc4..641797cac 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { computed } from "mobx"; import { observer } from "mobx-react"; import "react-table/react-table.css"; -import { Doc, Opt } from "../../../new_fields/Doc"; +import { Doc, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; import { ScriptField } from "../../../new_fields/ScriptField"; import { NumCast, StrCast } from "../../../new_fields/Types"; import { TraceMobx } from "../../../new_fields/util"; @@ -15,11 +15,13 @@ import "./ContentFittingDocumentView.scss"; import { dropActionType } from "../../util/DragManager"; interface ContentFittingDocumentViewProps { - Document?: Doc; + Document: Doc; DataDocument?: Doc; LayoutDoc?: () => Opt<Doc>; + NativeWidth?: () => number; + NativeHeight?: () => number; + FreezeDimensions?: boolean; LibraryPath: Doc[]; - childDocs?: Doc[]; renderDepth: number; fitToBox?: boolean; layoutKey?: string; @@ -40,18 +42,20 @@ interface ContentFittingDocumentViewProps { addDocTab: (document: Doc, where: string) => boolean; pinToPres: (document: Doc) => void; dontRegisterView?: boolean; + rootSelected: (outsideReaction?: boolean) => boolean; } @observer export class ContentFittingDocumentView extends React.Component<ContentFittingDocumentViewProps>{ public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive - private get layoutDoc() { return this.props.Document && (this.props.LayoutDoc?.() || Doc.Layout(this.props.Document)); } - private get nativeWidth() { return NumCast(this.layoutDoc?._nativeWidth, this.props.PanelWidth()); } - private get nativeHeight() { return NumCast(this.layoutDoc?._nativeHeight, this.props.PanelHeight()); } + private get layoutDoc() { return this.props.LayoutDoc?.() || Doc.Layout(this.props.Document); } + @computed get freezeDimensions() { return this.props.FreezeDimensions; } + nativeWidth = () => NumCast(this.layoutDoc?._nativeWidth, this.props.NativeWidth?.() || (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[WidthSym]() : this.props.PanelWidth())); + nativeHeight = () => NumCast(this.layoutDoc?._nativeHeight, this.props.NativeHeight?.() || (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[HeightSym]() : this.props.PanelHeight())); @computed get scaling() { - const wscale = this.props.PanelWidth() / (this.nativeWidth || this.props.PanelWidth() || 1); - if (wscale * this.nativeHeight > this.props.PanelHeight()) { - return (this.props.PanelHeight() / (this.nativeHeight || this.props.PanelHeight() || 1)) || 1; + const wscale = this.props.PanelWidth() / this.nativeWidth(); + if (wscale * this.nativeHeight() > this.props.PanelHeight()) { + return (this.props.PanelHeight() / this.nativeHeight()) || 1; } return wscale || 1; } @@ -60,12 +64,12 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo private PanelWidth = () => this.panelWidth; private PanelHeight = () => this.panelHeight; - @computed get panelWidth() { return this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeWidth * this.contentScaling() : this.props.PanelWidth(); } - @computed get panelHeight() { return this.nativeHeight && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeHeight * this.contentScaling() : this.props.PanelHeight(); } + @computed get panelWidth() { return this.nativeWidth && !this.props.Document._fitWidth ? this.nativeWidth() * this.contentScaling() : this.props.PanelWidth(); } + @computed get panelHeight() { return this.nativeHeight && !this.props.Document._fitWidth ? this.nativeHeight() * this.contentScaling() : this.props.PanelHeight(); } private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, -this.centeringYOffset).scale(1 / this.contentScaling()); - private get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; } - private get centeringYOffset() { return Math.abs(this.centeringOffset) < 0.001 ? (this.props.PanelHeight() - this.nativeHeight * this.contentScaling()) / 2 : 0; } + private get centeringOffset() { return this.nativeWidth() && !this.props.Document._fitWidth ? (this.props.PanelWidth() - this.nativeWidth() * this.contentScaling()) / 2 : 0; } + private get centeringYOffset() { return Math.abs(this.centeringOffset) < 0.001 ? (this.props.PanelHeight() - this.nativeHeight() * this.contentScaling()) / 2 : 0; } @computed get borderRounding() { return StrCast(this.props.Document?.borderRounding); } @@ -80,7 +84,7 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo style={{ transform: `translate(${this.centeringOffset}px, 0px)`, borderRadius: this.borderRounding, - height: Math.abs(this.centeringYOffset) > 0.001 ? `${100 * this.nativeHeight / this.nativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%` : this.props.PanelHeight(), + height: Math.abs(this.centeringYOffset) > 0.001 ? `${100 * this.nativeHeight() / this.nativeWidth() * this.props.PanelWidth() / this.props.PanelHeight()}%` : this.props.PanelHeight(), width: Math.abs(this.centeringOffset) > 0.001 ? `${100 * (this.props.PanelWidth() - this.centeringOffset * 2) / this.props.PanelWidth()}%` : this.props.PanelWidth() }}> <DocumentView {...this.props} @@ -88,6 +92,11 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo DataDoc={this.props.DataDocument} LayoutDoc={this.props.LayoutDoc} LibraryPath={this.props.LibraryPath} + NativeWidth={this.nativeWidth} + NativeHeight={this.nativeHeight} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} + ContentScaling={this.contentScaling} fitToBox={this.props.fitToBox} layoutKey={this.props.layoutKey} dropAction={this.props.dropAction} @@ -104,14 +113,9 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo parentActive={this.props.active} ScreenToLocalTransform={this.getTransform} renderDepth={this.props.renderDepth} - ContentScaling={this.contentScaling} - PanelWidth={this.PanelWidth} - PanelHeight={this.PanelHeight} focus={this.props.focus || emptyFunction} bringToFront={emptyFunction} dontRegisterView={this.props.dontRegisterView} - zoomToScale={emptyFunction} - getScale={returnOne} /> </div>)} </div>); diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx index 81cf90f92..31ce58079 100644 --- a/src/client/views/nodes/DocuLinkBox.tsx +++ b/src/client/views/nodes/DocuLinkBox.tsx @@ -4,7 +4,7 @@ import { Doc, DocListCast } from "../../../new_fields/Doc"; import { documentSchema } from "../../../new_fields/documentSchemas"; import { makeInterface } from "../../../new_fields/Schema"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { Utils } from '../../../Utils'; +import { Utils, setupMoveUpEvents } from '../../../Utils'; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager } from "../../util/DragManager"; import { DocComponent } from "../DocComponent"; @@ -16,6 +16,7 @@ import { ContextMenu } from "../ContextMenu"; import { LinkEditor } from "../linking/LinkEditor"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SelectionManager } from "../../util/SelectionManager"; +import { TraceMobx } from "../../../new_fields/util"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -29,8 +30,6 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc _doubleTap = false; _lastTap: number = 0; _ref = React.createRef<HTMLDivElement>(); - _downX = 0; - _downY = 0; _isOpen = false; _timeout: NodeJS.Timeout | undefined; @observable _x = 0; @@ -40,56 +39,42 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc @observable _forceOpen = false; onPointerDown = (e: React.PointerEvent) => { - this._downX = e.clientX; - this._downY = e.clientY; - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointerup", this.onPointerUp); - (e.button === 0 && !e.ctrlKey) && e.stopPropagation(); + setupMoveUpEvents(this, e, this.onPointerMove, () => { }, this.onClick); } - onPointerMove = action((e: PointerEvent) => { + onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => { const cdiv = this._ref && this._ref.current && this._ref.current.parentElement; - if (!this._isOpen && cdiv && (Math.abs(e.clientX - this._downX) > 5 || Math.abs(e.clientY - this._downY) > 5)) { + if (!this._isOpen && cdiv) { const bounds = cdiv.getBoundingClientRect(); const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY); const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY)); - const dragdist = Math.sqrt((pt[0] - this._downX) * (pt[0] - this._downX) + (pt[1] - this._downY) * (pt[1] - this._downY)); + const dragdist = Math.sqrt((pt[0] - down[0]) * (pt[0] - down[0]) + (pt[1] - down[1]) * (pt[1] - down[1])); if (separation > 100) { - //DragManager.StartLinkTargetsDrag(this._ref.current!, pt[0], pt[1], Cast(this.props.Document[this.props.fieldKey], Doc) as Doc, [this.props.Document]); // Containging collection is the document, not a collection... hack. const dragData = new DragManager.DocumentDragData([this.props.Document]); dragData.dropAction = "alias"; - dragData.removeDropProperties = ["anchor1_x", "anchor1_y", "anchor2_x", "anchor2_y"]; - DragManager.StartDocumentDrag([this._ref.current!], dragData, this._downX, this._downY); - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); + dragData.removeDropProperties = ["anchor1_x", "anchor1_y", "anchor2_x", "anchor2_y", "isButton"]; + DragManager.StartDocumentDrag([this._ref.current!], dragData, down[0], down[1]); + return true; } else if (dragdist > separation) { this.props.Document[this.props.fieldKey + "_x"] = (pt[0] - bounds.left) / bounds.width * 100; this.props.Document[this.props.fieldKey + "_y"] = (pt[1] - bounds.top) / bounds.height * 100; } } + return false; }); - onPointerUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - if (Math.abs(e.clientX - this._downX) < 3 && Math.abs(e.clientY - this._downY) < 3 && (e.button === 2 || e.ctrlKey || !this.props.Document.isButton)) { + @action + onClick = (e: PointerEvent) => { + this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0); + this._lastTap = Date.now(); + if ((e.button === 2 || e.ctrlKey || !this.props.Document.isButton)) { this.props.select(false); } - this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); - this._lastTap = Date.now(); - } - - @action - onClick = (e: React.MouseEvent) => { if (!this._doubleTap) { + const anchorContainerDoc = this.props.ContainingCollectionDoc; // bcz: hack! need a better prop for passing the anchor's container this._editing = true; - this.props.ContainingCollectionDoc && this.props.bringToFront(this.props.ContainingCollectionDoc, false); - const {clientX, clientY} = e; - if (!this.props.Document.onClick && !this._isOpen) { + anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false); + if (anchorContainerDoc && !this.props.Document.onClick && !this._isOpen) { this._timeout = setTimeout(action(() => { - if (Math.abs(clientX - this._downX) < 3 && Math.abs(clientY - this._downY) < 3 && (e.button !== 2 && !e.ctrlKey && this.props.Document.isButton)) { - DocumentManager.Instance.FollowLink(this.props.Document, this.props.ContainingCollectionDoc as Doc, document => this.props.addDocTab(document, StrCast(this.props.Document.linkOpenLocation, "inTab")), false); - } + DocumentManager.Instance.FollowLink(this.props.Document, anchorContainerDoc, document => this.props.addDocTab(document, StrCast(this.props.Document.linkOpenLocation, "inTab")), false); this._editing = false; }), 300 - (Date.now() - this._lastTap)); } @@ -97,7 +82,6 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc this._timeout && clearTimeout(this._timeout); this._timeout = undefined; } - e.stopPropagation(); } openLinkDocOnRight = (e: React.MouseEvent) => { @@ -126,6 +110,7 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc } render() { + TraceMobx(); const x = this.props.PanelWidth() > 1 ? NumCast(this.props.Document[this.props.fieldKey + "_x"], 100) : 0; const y = this.props.PanelWidth() > 1 ? NumCast(this.props.Document[this.props.fieldKey + "_y"], 100) : 0; const c = StrCast(this.props.Document.backgroundColor, "lightblue"); @@ -143,7 +128,7 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc </div> ); const small = this.props.PanelWidth() <= 1; - return <div className={`docuLinkBox-cont${small ? "-small" : ""}`} onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu} + return <div className={`docuLinkBox-cont${small ? "-small" : ""}`} onPointerDown={this.onPointerDown} title={targetTitle} onContextMenu={this.specificContextMenu} ref={this._ref} style={{ background: c, left: !small ? `calc(${x}% - 7.5px)` : undefined, diff --git a/src/client/views/nodes/DocumentBox.tsx b/src/client/views/nodes/DocumentBox.tsx index debe104d7..0e2685d41 100644 --- a/src/client/views/nodes/DocumentBox.tsx +++ b/src/client/views/nodes/DocumentBox.tsx @@ -112,7 +112,7 @@ export class DocumentBox extends DocAnnotatableComponent<FieldViewProps, DocBoxS if (containedDoc && childTemplateName && !containedDoc["layout_" + childTemplateName]) { setTimeout(() => { DocumentView.createCustomView(containedDoc, Docs.Create.StackingDocument, childTemplateName); - Doc.expandTemplateLayout(Cast(containedDoc["layout_" + childTemplateName], Doc, null)!, containedDoc, undefined); + Doc.expandTemplateLayout(Cast(containedDoc["layout_" + childTemplateName], Doc, null), containedDoc, undefined); }, 0); } const contents = !(containedDoc instanceof Doc) ? (null) : <ContentFittingDocumentView @@ -122,6 +122,7 @@ export class DocumentBox extends DocAnnotatableComponent<FieldViewProps, DocBoxS CollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected fitToBox={this.props.fitToBox} layoutKey={"layout_" + childTemplateName} + rootSelected={this.props.isSelected} addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 68501fca2..dc71ba280 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,6 +1,6 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc, Opt } from "../../../new_fields/Doc"; import { Cast, StrCast } from "../../../new_fields/Types"; import { OmitKeys, Without } from "../../../Utils"; import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; @@ -35,6 +35,7 @@ import { WebBox } from "./WebBox"; import { InkingStroke } from "../InkingStroke"; import React = require("react"); 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? @@ -60,7 +61,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { forceLayout?: string, forceFieldKey?: string, hideOnLeave?: boolean, - makeLink?: () => Opt<Doc>; + makeLink?: () => Opt<Doc>, // function to call when a link is made }> { @computed get layout(): string { TraceMobx(); diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index d1d96f0a1..ce7bcd206 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -34,13 +34,18 @@ overflow-y: scroll; height: calc(100% - 20px); } - + .documentView-docuLinkAnchor { + display:flex; + overflow: hidden; + } .documentView-docuLinkWrapper { pointer-events: none; position: absolute; transform-origin: top left; width: 100%; height: 100%; + top:0; + left:0; z-index: 1; } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 02a1ac527..2df5c9bbd 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -3,7 +3,7 @@ import * as fa from '@fortawesome/free-solid-svg-icons'; 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"; +import { Doc, DocListCast, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; import { Document, PositionDocument } from '../../../new_fields/documentSchemas'; import { Id } from '../../../new_fields/FieldSymbols'; import { InkTool } from '../../../new_fields/InkField'; @@ -14,7 +14,7 @@ import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { AudioField, ImageField, PdfField, VideoField } from '../../../new_fields/URLField'; import { TraceMobx } from '../../../new_fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; -import { emptyFunction, returnOne, returnTransparent, returnTrue, Utils, OmitKeys } from "../../../Utils"; +import { emptyFunction, returnOne, returnTransparent, returnTrue, Utils, OmitKeys, returnZero } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from "../../DocServer"; import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; @@ -52,14 +52,19 @@ library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsA 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, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone); +export type DocFocusFunc = () => boolean; export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; + FreezeDimensions?: boolean; + NativeWidth: () => number; + NativeHeight: () => number; Document: Doc; DataDoc?: Doc; LayoutDoc?: () => Opt<Doc>; LibraryPath: Doc[]; fitToBox?: boolean; + rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected onClick?: ScriptField; onPointerDown?: ScriptField; onPointerUp?: ScriptField; @@ -73,16 +78,14 @@ export interface DocumentViewProps { ContentScaling: () => number; PanelWidth: () => number; PanelHeight: () => number; - focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => void; + focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: DocFocusFunc) => void; parentActive: (outsideReaction: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc, sendToBack?: boolean) => void; addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean; pinToPres: (document: Doc) => void; - zoomToScale: (scale: number) => void; backgroundHalo?: () => boolean; backgroundColor?: (doc: Doc) => string | undefined; - getScale: () => number; ChromeHeight?: () => number; dontRegisterView?: boolean; layoutKey?: string; @@ -107,19 +110,20 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive public get ContentDiv() { return this._mainCont.current; } - @computed get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); } + get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); } @computed get topMost() { return this.props.renderDepth === 0; } - @computed get nativeWidth() { return this.layoutDoc._nativeWidth || 0; } - @computed get nativeHeight() { return this.layoutDoc._nativeHeight || 0; } + @computed get freezeDimensions() { return this.props.FreezeDimensions; } + @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth, this.props.NativeWidth() || (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); } + @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight, this.props.NativeHeight() || (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 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; } + NativeWidth = () => this.nativeWidth; + NativeHeight = () => this.nativeHeight; private _firstX: number = -1; private _firstY: number = -1; - - handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { this.removeMoveListeners(); this.removeEndListeners(); @@ -275,7 +279,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClick = (e: React.MouseEvent | React.PointerEvent) => { if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { - e.stopPropagation(); + let stopPropagate = true; let preventDefault = true; this.props.bringToFront(this.props.Document); if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click @@ -288,32 +292,51 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu Doc.UnBrushDoc(this.props.Document); } else if (this.onClickHandler?.script) { SelectionManager.DeselectAll(); - UndoManager.RunInBatch(() => this.onClickHandler!.script.run({ + const func = () => this.onClickHandler!.script.run({ this: this.props.Document, self: Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document, - containingCollection: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey - }, console.log) && !this.props.Document.dontSelect && !this.props.Document.isButton && this.select(false), "on click"); + thisContainer: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey + }, console.log);// && !this.props.Document.isButton && this.select(false); + if (this.props.Document !== Doc.UserDoc().undoBtn && this.props.Document !== Doc.UserDoc().redoBtn) { + UndoManager.RunInBatch(func, "on click"); + } else func(); } else if (this.Document.type === DocumentType.BUTTON) { UndoManager.RunInBatch(() => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click"); } else if (this.Document.isButton) { - SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered. - UndoManager.RunInBatch(() => this.buttonClick(e.altKey, e.ctrlKey), "on link button follow"); + DocListCast(this.props.Document.links).length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey); } else { - SelectionManager.SelectDoc(this, e.ctrlKey); + if (this.props.Document.isTemplateForField && !(e.ctrlKey || e.button > 0)) { + stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template + } else { + SelectionManager.SelectDoc(this, e.ctrlKey); + } preventDefault = false; } + stopPropagate && e.stopPropagation(); preventDefault && e.preventDefault(); } } - buttonClick = async (altKey: boolean, ctrlKey: boolean) => { - const linkDocs = DocListCast(this.props.Document.links); - if (linkDocs.length) { - DocumentManager.Instance.FollowLink(undefined, this.props.Document, - // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards - (doc: Doc, maxLocation: string) => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, maxLocation)), - ctrlKey, altKey, this.props.ContainingCollectionDoc); - } + // follows a link - if the target is on screen, it highlights/pans to it. + // if the target isn't onscreen, then it will open up the target in a tab, on the right, or in place + // depending on the followLinkLocation property of the source (or the link itself as a fallback); + followLinkClick = async (altKey: boolean, ctrlKey: boolean, shiftKey: boolean) => { + const batch = UndoManager.StartBatch("follow link click"); + // open up target if it's not already in view ... + const createViewFunc = (doc: Doc, followLoc: string, finished: Opt<() => void>) => { + const targetFocusAfterDocFocus = () => { + const where = StrCast(this.Document.followLinkLocation) || followLoc; + const hackToCallFinishAfterFocus = () => { + setTimeout(() => finished?.(), 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout. + return false; // we must return false here so that the zoom to the document is not reversed. If it weren't for needing to call finished(), we wouldn't need this function at all since not having it is equivalent to returning false + }; + this.props.addDocTab(doc, where) && this.props.focus(doc, true, undefined, hackToCallFinishAfterFocus); // add the target and focus on it. + return where !== "inPlace"; // return true to reset the initial focus&zoom (return false for 'inPlace' since resetting the initial focus&zoom will negate the zoom into the target) + }; + // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target + this.props.focus(this.props.Document, true, 1, targetFocusAfterDocFocus); + }; + await DocumentManager.Instance.FollowLink(undefined, this.props.Document, createViewFunc, shiftKey, this.props.ContainingCollectionDoc, batch.end, altKey ? true : undefined); } handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { @@ -561,6 +584,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.Document.onClick = undefined; } else { this.Document.isButton = true; + this.Document.followLinkLocation = undefined; + } + } + + @undoBatch + toggleFollowInPlace = (): void => { + if (this.Document.isButton) { + this.Document.isButton = false; + } else { + this.Document.isButton = true; + this.Document.followLinkLocation = "inPlace"; } } @@ -640,20 +674,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @action - onContextMenu = async (e: React.MouseEvent): Promise<void> => { + onContextMenu = async (e: React.MouseEvent | Touch): Promise<void> => { // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 - if (e.button === 0 && !e.ctrlKey) { - e.preventDefault(); - return; - } - e.persist(); - e?.stopPropagation(); - if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 || - e.isDefaultPrevented()) { + if (!(e instanceof Touch)) { + if (e.button === 0 && !e.ctrlKey) { + e.preventDefault(); + return; + } + e.persist(); + e?.stopPropagation(); + + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 || + e.isDefaultPrevented()) { + e.preventDefault(); + return; + } e.preventDefault(); - return; } - e.preventDefault(); const cm = ContextMenu.Instance; const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); @@ -690,6 +727,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(this, "${this.props.Document.layoutKey}")`), icon: "window-restore" }); onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); + onClicks.push({ description: this.Document.isButton ? "Remove Follow Behavior" : "Follow Link in Place", event: this.toggleFollowInPlace, icon: "concierge-bell" }); onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.toggleButtonBehavior, icon: "concierge-bell" }); onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) }); !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); @@ -733,7 +771,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu recommender_subitems.push({ description: "Internal recommendations", - event: () => this.recommender(e), + event: () => this.recommender(), icon: "brain" }); @@ -794,7 +832,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu icon: "external-link-alt" }); - if (!this.topMost) { + if (!this.topMost && !(e instanceof Touch)) { // DocumentViews should stop propagation of this event e.stopPropagation(); } @@ -812,7 +850,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } - recommender = async (e: React.MouseEvent) => { + recommender = async () => { if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" }); const documents: Doc[] = []; const allDocs = await SearchUtil.GetAllDocs(); @@ -926,33 +964,40 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const fallback = Cast(this.props.Document.layoutKey, "string"); return typeof fallback === "string" ? fallback : "layout"; } + rootSelected = (outsideReaction: boolean) => { + return this.isSelected(outsideReaction) || (this.props.Document.forceActive && this.props.rootSelected?.(outsideReaction) ? true : false); + } childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); + panelWidth = () => this.props.PanelWidth(); + panelHeight = () => this.props.PanelHeight(); + screenToLocalTransform = () => this.props.ScreenToLocalTransform(); @computed get contents() { TraceMobx(); return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView} ContainingCollectionDoc={this.props.ContainingCollectionDoc} + NativeWidth={this.NativeWidth} + NativeHeight={this.NativeHeight} Document={this.props.Document} DataDoc={this.props.DataDoc} LayoutDoc={this.props.LayoutDoc} makeLink={this.makeLink} + rootSelected={this.rootSelected} fitToBox={this.props.fitToBox} LibraryPath={this.props.LibraryPath} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} + ScreenToLocalTransform={this.screenToLocalTransform} renderDepth={this.props.renderDepth} - PanelWidth={this.props.PanelWidth} - PanelHeight={this.props.PanelHeight} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} focus={this.props.focus} parentActive={this.props.parentActive} whenActiveChanged={this.props.whenActiveChanged} bringToFront={this.props.bringToFront} addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} - zoomToScale={this.props.zoomToScale} backgroundColor={this.props.backgroundColor} - getScale={this.props.getScale} ContentScaling={this.childScaling} ChromeHeight={this.chromeHeight} isSelected={this.isSelected} @@ -962,13 +1007,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document); - // used to decide whether a link document should be created or not. + // used to decide whether a link anchor view should be created or not. // if it's a tempoarl link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here. // would be good to generalize this some way. isNonTemporalLink = (linkDoc: Doc) => { const anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc; - const ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1_timecode : linkDoc.anchor2_timecode; - return anchor.type === DocumentType.AUDIO && NumCast(ept) ? false : true; + return anchor.type === DocumentType.AUDIO ? false : true; } @observable _link: Opt<Doc>; @@ -976,27 +1020,36 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return this._link; } + hideLinkAnchor = (doc: Doc) => undoBatch(doc => doc.hidden = true)(); + anchorPanelWidth = () => this.props.PanelWidth() || 1; + anchorPanelHeight = () => this.props.PanelHeight() || 1; + @computed get anchors() { + TraceMobx(); + return DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => + <div className="documentView-docuLinkWrapper" key={d[Id]}> + <DocumentView {...this.props} + Document={d} + ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the DocuLinkBox + PanelWidth={this.anchorPanelWidth} + PanelHeight={this.anchorPanelHeight} + layoutKey={this.linkEndpoint(d)} + ContentScaling={returnOne} + backgroundColor={returnTransparent} + removeDocument={this.hideLinkAnchor} /> + </div>); + } @computed get innards() { TraceMobx(); - if (!this.props.PanelWidth()) { - return <div style={{ display: "flex", overflow: "hidden" }}> + if (!this.props.PanelWidth()) { // this happens when the document is a tree view label + return <div className="documentView-docuLinkAnchor" > {StrCast(this.props.Document.title)} - {this.Document.links && DocListCast(this.Document.links).filter(d => !d.hidden).filter(this.isNonTemporalLink).map((d, i) => - <div className="documentView-docuLinkWrapper" style={{ position: "absolute", top: 0, left: 0 }} key={`${d[Id]}`}> - <DocumentView {...this.props} Document={d} ContainingCollectionDoc={this.props.Document} - PanelWidth={returnOne} PanelHeight={returnOne} layoutKey={this.linkEndpoint(d)} ContentScaling={returnOne} - backgroundColor={returnTransparent} removeDocument={undoBatch(doc => doc.hidden = true)} /> - </div>)} + {this.anchors} </div>; } const showTitle = StrCast(this.layoutDoc._showTitle); const showTitleHover = StrCast(this.layoutDoc._showTitleHover); const showCaption = StrCast(this.layoutDoc._showCaption); const showTextTitle = showTitle && (StrCast(this.layoutDoc.layout).indexOf("PresBox") !== -1 || StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1) ? showTitle : undefined; - const searchHighlight = (!this.Document.searchFields ? (null) : - <div className="documentView-searchHighlight"> - {this.Document.searchFields} - </div>); const captionView = (!showCaption ? (null) : <div className="documentView-captionWrapper"> <DocumentContentsView {...OmitKeys(this.props, ['children']).omit} @@ -1023,26 +1076,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu /> </div>); return <> - {this.Document.links && DocListCast(this.Document.links).filter(d => !d.hidden).filter(this.isNonTemporalLink).map((d, i) => - <div className="documentView-docuLinkWrapper" key={`${d[Id]}`}> - <DocumentView {...this.props} ContentScaling={returnOne} ContainingCollectionDoc={this.props.Document} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => doc.hidden = true)} /> - </div>)} + {this.anchors} {!showTitle && !showCaption ? - this.Document.searchFields ? - (<div className="documentView-searchWrapper"> - {this.contents} - {searchHighlight} - </div>) - : - this.contents - : + this.contents : <div className="documentView-styleWrapper" > <div className="documentView-styleContentWrapper" style={{ height: showTextTitle ? `calc(100% - ${this.chromeHeight()}px)` : "100%", top: showTextTitle ? this.chromeHeight() : undefined }}> {this.contents} </div> {titleView} {captionView} - {searchHighlight} </div> } </>; diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 0305f43d5..a3790d38b 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -29,6 +29,7 @@ export interface FieldViewProps { dropAction: dropActionType; isSelected: (outsideReaction?: boolean) => boolean; select: (isCtrlPressed: boolean) => void; + rootSelected: (outsideReaction?: boolean) => boolean; renderDepth: number; addDocument?: (document: Doc) => boolean; addDocTab: (document: Doc, where: string) => boolean; @@ -44,6 +45,8 @@ export interface FieldViewProps { focus: (doc: Doc) => void; PanelWidth: () => number; PanelHeight: () => number; + NativeHeight: () => number; + NativeWidth: () => number; setVideoBox?: (player: VideoBox) => void; ContentScaling: () => number; ChromeHeight?: () => number; diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 526939438..7d40b3149 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -39,11 +39,6 @@ position: absolute; } } - -.collectionfreeformview-container { - position: relative; -} - .formattedTextBox-outer { position: relative; overflow: auto; @@ -75,6 +70,10 @@ position: absolute; right: 0; + .collectionfreeformview-container { + position: relative; + } + >.formattedTextBox-sidebar-handle { right: unset; left: -5; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 241345f3e..37770a2e1 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -22,7 +22,7 @@ import { RichTextUtils } from '../../../new_fields/RichTextUtils'; import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { Cast, NumCast, StrCast, BoolCast, DateCast } from "../../../new_fields/Types"; import { TraceMobx } from '../../../new_fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, Utils, returnTrue } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, Utils, returnTrue, returnZero } from '../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from '../../documents/Documents'; @@ -292,7 +292,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & const linkDoc = data.linkDocument!; const anchor1Title = linkDoc.anchor1 instanceof Doc ? StrCast(linkDoc.anchor1.title) : "-untitled-"; const anchor1Id = linkDoc.anchor1 instanceof Doc ? linkDoc.anchor1[Id] : ""; - this.makeLinkToSelection(linkDoc[Id], anchor1Title, "onRight", anchor1Id) + this.makeLinkToSelection(linkDoc[Id], anchor1Title, "onRight", anchor1Id); } getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null { @@ -1216,6 +1216,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.sidebarWidth} + NativeHeight={returnZero} + NativeWidth={returnZero} annotationsKey={this.annotationKey} isAnnotationOverlay={false} focus={this.props.focus} diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx index 61df188f8..20d734244 100644 --- a/src/client/views/nodes/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -84,7 +84,7 @@ export class FormattedTextBoxComment { const textBox = FormattedTextBoxComment.textBox; if (FormattedTextBoxComment.linkDoc && !keep && textBox) { DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, - (doc: Doc, maxLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : "onRight")); + (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) { textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400 }), "onRight"); } @@ -181,6 +181,7 @@ export class FormattedTextBoxComment { LibraryPath={emptyPath} fitToBox={true} moveDocument={returnFalse} + rootSelected={returnFalse} getTransform={Transform.Identity} active={returnFalse} addDocument={returnFalse} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 00057055f..8818b8098 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -14,7 +14,7 @@ import { ComputedField } from '../../../new_fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; import { AudioField, ImageField } from '../../../new_fields/URLField'; import { TraceMobx } from '../../../new_fields/util'; -import { emptyFunction, returnOne, Utils } from '../../../Utils'; +import { emptyFunction, returnOne, Utils, returnZero } from '../../../Utils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs } from '../../documents/Documents'; import { Networking } from '../../Network'; @@ -392,8 +392,10 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum const fadepath = this.paths[Math.min(1, this.paths.length - 1)]; const { nativeWidth, nativeHeight } = this.nativeSize; const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]); - const aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1; - const shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; + const aspect = (rotation % 180) ? nativeHeight / nativeWidth : 1; + const pwidth = this.props.PanelWidth(); + const pheight = this.props.PanelHeight(); + const shift = (rotation % 180) ? (pheight - pwidth) / aspect / 2 + (pheight - pwidth) / 2 : 0; this.resize(srcpath); @@ -433,18 +435,22 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum contentFunc = () => [this.content]; render() { TraceMobx(); + const { nativeWidth, nativeHeight } = this.nativeSize; + const aspect = nativeWidth / nativeHeight; const dragging = !SelectionManager.GetIsDragging() ? "" : "-dragging"; return (<div className={`imageBox${dragging}`} onContextMenu={this.specificContextMenu} style={{ - transform: `scale(${this.props.ContentScaling()})`, - width: `${100 / this.props.ContentScaling()}%`, - height: `${100 / this.props.ContentScaling()}%`, + transform: this.props.PanelWidth() ? undefined : `scale(${this.props.ContentScaling()})`, + width: this.props.PanelWidth() ? `${this.props.PanelWidth()}px` : `${100 / this.props.ContentScaling()}%`, + height: this.props.PanelWidth() ? `${this.props.PanelWidth() / aspect}px` : `${100 / this.props.ContentScaling()}%`, pointerEvents: this.props.Document.isBackground ? "none" : undefined, borderRadius: `${Number(StrCast(this.layoutDoc.borderRounding).replace("px", "")) / this.props.ContentScaling()}px` }} > <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} + NativeHeight={returnZero} + NativeWidth={returnZero} annotationsKey={this.annotationKey} isAnnotationOverlay={true} focus={this.props.focus} diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 3d59ea61a..6dc4ae578 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -59,15 +59,18 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { ContainingCollectionView: undefined, ContainingCollectionDoc: undefined, fieldKey: this.props.keyName, + rootSelected: returnFalse, isSelected: returnFalse, select: emptyFunction, - dropAction:"alias", - bringToFront:emptyFunction, + dropAction: "alias", + bringToFront: emptyFunction, renderDepth: 1, active: returnFalse, whenActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, + NativeHeight: returnZero, + NativeWidth: returnZero, PanelWidth: this.props.PanelWidth, PanelHeight: this.props.PanelHeight, addDocTab: returnFalse, diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 0e327e130..542c86049 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -23,6 +23,8 @@ export class LinkBox extends DocExtendableComponent<FieldViewProps, LinkDocument <CollectionTreeView {...this.props} ChromeHeight={returnZero} overrideDocuments={[this.dataDoc]} + NativeHeight={returnZero} + NativeWidth={returnZero} ignoreFields={Cast(this.props.Document.linkBoxExcludedKeys, listSpec("string"), null)} annotationsKey={""} CollectionView={undefined} diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index a39c337ca..73d09b4e1 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -49,14 +49,15 @@ export class PresBox extends React.Component<FieldViewProps> { this.updateCurrentPresentation(); if (this.childDocs[this.currentIndex + 1] !== undefined) { let nextSelected = this.currentIndex + 1; + this.gotoDocument(nextSelected, this.currentIndex); - for (; nextSelected < this.childDocs.length - 1; nextSelected++) { - if (!this.childDocs[nextSelected + 1].groupButton) { + for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) { + if (!this.childDocs[nextSelected].groupButton) { break; + } else { + this.gotoDocument(nextSelected, this.currentIndex); } } - - this.gotoDocument(nextSelected, this.currentIndex); } } back = () => { @@ -72,13 +73,6 @@ export class PresBox extends React.Component<FieldViewProps> { } prevSelected = Math.max(0, prevSelected - 1); - if (this.currentIndex > 0 && didZoom) { - const prevScale = NumCast(this.childDocs[prevSelected].viewScale); - const curScale = DocumentManager.Instance.getScaleOfDocView(docAtCurrent); - if (prevScale && prevScale !== curScale) { - DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale); - } - } this.gotoDocument(prevSelected, this.currentIndex); } } @@ -171,31 +165,15 @@ export class PresBox extends React.Component<FieldViewProps> { if (curDoc.navButton && target) { DocumentManager.Instance.jumpToDocument(target, false, undefined, srcContext); } else if (curDoc.zoomButton && target) { - const curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc); //awaiting jump so that new scale can be found, since jumping is async await DocumentManager.Instance.jumpToDocument(target, true, undefined, srcContext); - curDoc.viewScale = DocumentManager.Instance.getScaleOfDocView(target); - - //saving the scale user was on before zooming in - if (curScale !== 1) { - fromDoc.viewScale = curScale; - } - } return; } - const curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc); //awaiting jump so that new scale can be found, since jumping is async const presTargetDoc = await docToJump.presentationTargetDoc as Doc; await DocumentManager.Instance.jumpToDocument(presTargetDoc, willZoom, undefined, srcContext); - const newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc); - curDoc.viewScale = newScale; - //saving the scale that user was on - if (curScale !== 1) { - fromDoc.viewScale = curScale; - } - } @@ -246,10 +224,9 @@ export class PresBox extends React.Component<FieldViewProps> { //stops the presentaton. resetPresentation = () => { this.updateCurrentPresentation(); - this.childDocs.forEach(doc => doc.opacity = doc.viewScale = 1); + this.childDocs.forEach(doc => (doc.presentationTargetDoc as Doc).opacity = 1); this.props.Document._itemIndex = 0; this.props.Document.presStatus = false; - this.childDocs.length && DocumentManager.Instance.zoomIntoScale(this.childDocs[0], 1); } //The function that starts the presentation, also checking if actions should be applied @@ -258,13 +235,13 @@ export class PresBox extends React.Component<FieldViewProps> { this.updateCurrentPresentation(); this.childDocs.map(doc => { if (doc.hideTillShownButton && this.childDocs.indexOf(doc) > startIndex) { - doc.opacity = 0; + (doc.presentationTargetDoc as Doc).opacity = 0; } if (doc.hideAfterButton && this.childDocs.indexOf(doc) < startIndex) { - doc.opacity = 0; + (doc.presentationTargetDoc as Doc).opacity = 0; } if (doc.fadeButton && this.childDocs.indexOf(doc) < startIndex) { - doc.opacity = 0.5; + (doc.presentationTargetDoc as Doc).opacity = 0.5; } }); } @@ -284,16 +261,11 @@ export class PresBox extends React.Component<FieldViewProps> { } })); - /** - * Initially every document starts with a viewScale 1, which means - * that they will be displayed in a canvas with scale 1. - */ - initializeScaleViews = (docList: Doc[], viewtype: number) => { + initializeViewAliases = (docList: Doc[], viewtype: number) => { const hgt = (viewtype === CollectionViewType.Tree) ? 50 : 46; docList.forEach(doc => { doc.presBox = this.props.Document; // give contained documents a reference to the presentation doc.collapsedHeight = hgt; // set the collpased height for documents based on the type of view (Tree or Stack) they will be displaye din - !NumCast(doc.viewScale) && (doc.viewScale = 1); }); } @@ -319,7 +291,7 @@ export class PresBox extends React.Component<FieldViewProps> { childLayoutTemplate = () => this.props.Document._viewType === CollectionViewType.Stacking ? Cast(Doc.UserDoc().presentationTemplate, Doc, null) : undefined; render() { const mode = NumCast(this.props.Document._viewType, CollectionViewType.Invalid); - this.initializeScaleViews(this.childDocs, mode); + this.initializeViewAliases(this.childDocs, mode); return <div className="presBox-cont" style={{ minWidth: this.props.Document.inOverlay ? 240 : undefined, pointerEvents: this.active() || this.props.Document.inOverlay ? "all" : "none" }} > <div className="presBox-buttons" style={{ display: this.props.Document._chromeStatus === "disabled" ? "none" : undefined }}> <select className="collectionViewBaseChrome-viewPicker" diff --git a/src/client/views/nodes/QueryBox.scss b/src/client/views/nodes/QueryBox.scss index e69de29bb..82f64054c 100644 --- a/src/client/views/nodes/QueryBox.scss +++ b/src/client/views/nodes/QueryBox.scss @@ -0,0 +1,6 @@ +.queryBox, .queryBox-dragging { + width: 100%; + height: 100%; + position: absolute; + pointer-events: all; +}
\ No newline at end of file diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx index 99b5810fc..7016b4f04 100644 --- a/src/client/views/nodes/QueryBox.tsx +++ b/src/client/views/nodes/QueryBox.tsx @@ -1,35 +1,34 @@ 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 { IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; -import { FilterBox } from "../search/FilterBox"; +import { documentSchema } from "../../../new_fields/documentSchemas"; +import { Id } from '../../../new_fields/FieldSymbols'; +import { makeInterface } from "../../../new_fields/Schema"; +import { StrCast } from "../../../new_fields/Types"; +import { SelectionManager } from "../../util/SelectionManager"; +import { DocAnnotatableComponent } from '../DocComponent'; +import { SearchBox } from "../search/SearchBox"; import { FieldView, FieldViewProps } from './FieldView'; -import "./PresBox.scss"; +import "./QueryBox.scss"; -library.add(faArrowLeft); -library.add(faArrowRight); -library.add(faPlay); -library.add(faStop); -library.add(faPlus); -library.add(faTimes); -library.add(faMinus); -library.add(faEdit); +type QueryDocument = makeInterface<[typeof documentSchema]>; +const QueryDocument = makeInterface(documentSchema); @observer -export class QueryBox extends React.Component<FieldViewProps> { +export class QueryBox extends DocAnnotatableComponent<FieldViewProps, QueryDocument>(QueryDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(QueryBox, fieldKey); } _docListChangedReaction: IReactionDisposer | undefined; componentDidMount() { } componentWillUnmount() { - this._docListChangedReaction && this._docListChangedReaction(); + this._docListChangedReaction?.(); } render() { - return <div style={{ width: "100%", height: "100%", position: "absolute", pointerEvents: "all" }}> - <FilterBox></FilterBox> - </div>; + const dragging = !SelectionManager.GetIsDragging() ? "" : "-dragging"; + return <div className={`queryBox${dragging}`} onWheel={(e) => e.stopPropagation()} > + <SearchBox id={this.props.Document[Id]} searchQuery={StrCast(this.dataDoc.searchQuery)} filterQquery={StrCast(this.dataDoc.filterQuery)} /> + </div >; } }
\ No newline at end of file diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 7c58a5148..58ff4971a 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -10,7 +10,7 @@ import { makeInterface } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; import { Cast, StrCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; -import { emptyFunction, returnFalse, returnOne, Utils } from "../../../Utils"; +import { emptyFunction, returnFalse, returnOne, Utils, returnZero } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { ContextMenu } from "../ContextMenu"; @@ -170,6 +170,8 @@ export class ScreenshotBox extends DocAnnotatableComponent<FieldViewProps, Scree <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} + NativeHeight={returnZero} + NativeWidth={returnZero} annotationsKey={this.annotationKey} focus={this.props.focus} isSelected={this.props.isSelected} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 24b66d8f7..d384ad12f 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -11,7 +11,7 @@ import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; -import { Utils, emptyFunction, returnOne } from "../../../Utils"; +import { Utils, emptyFunction, returnOne, returnZero } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { ContextMenu } from "../ContextMenu"; @@ -352,6 +352,8 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} + NativeHeight={returnZero} + NativeWidth={returnZero} annotationsKey={this.annotationKey} focus={this.props.focus} isSelected={this.props.isSelected} diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 838fbefb1..ea5d601ec 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -9,7 +9,7 @@ import { InkTool } from "../../../new_fields/InkField"; import { makeInterface } from "../../../new_fields/Schema"; import { Cast, NumCast } from "../../../new_fields/Types"; import { WebField } from "../../../new_fields/URLField"; -import { Utils, returnOne, emptyFunction } from "../../../Utils"; +import { Utils, returnOne, emptyFunction, returnZero } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; @@ -335,6 +335,8 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} annotationsKey={this.annotationKey} + NativeHeight={returnZero} + NativeWidth={returnZero} focus={this.props.focus} isSelected={this.props.isSelected} isAnnotationOverlay={true} diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index d23c81065..71b19f3a6 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -97,9 +97,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { else if (e.button === 0) { const annoGroup = await Cast(this.props.document.group, Doc); if (annoGroup) { - DocumentManager.Instance.FollowLink(undefined, annoGroup, - (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : "onRight"), - false, false, undefined); + DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation), false, undefined); e.stopPropagation(); } } diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 5913c5a82..2a6eff7ff 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -64,7 +64,7 @@ export default class PDFMenu extends AntimodeMenu { togglePin = action((e: React.MouseEvent) => { this.Pinned = !this.Pinned; !this.Pinned && (this.Highlighting = false); - }) + }); @action highlightClicked = (e: React.MouseEvent) => { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 71495d95f..720f09fe0 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -9,7 +9,7 @@ import { List } from "../../../new_fields/List"; import { makeInterface, createSchema } from "../../../new_fields/Schema"; import { ScriptField, ComputedField } from "../../../new_fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from "../../../Utils"; +import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules, returnZero } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { CompiledScript, CompileScript } from "../../util/Scripting"; @@ -31,8 +31,6 @@ 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"; import { DocumentView } from "../nodes/DocumentView"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); const pdfjsLib = require("pdfjs-dist"); @@ -61,6 +59,7 @@ interface IViewerProps { PanelHeight: () => number; ContentScaling: () => number; select: (isCtrlPressed: boolean) => void; + rootSelected: (outsideReaction?: boolean) => boolean; startupLive: boolean; renderDepth: number; focus: (doc: Doc) => void; @@ -586,7 +585,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument dragComplete: e => { if (!e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc) { const link = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); - if (link) link.maximizeLocation = "onRight"; + if (link) link.followLinkLocation = "onRight"; } } }); @@ -649,6 +648,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument setPreviewCursor={this.setPreviewCursor} PanelHeight={this.panelWidth} PanelWidth={this.panelHeight} + NativeHeight={returnZero} + NativeWidth={returnZero} dropAction={"alias"} VisibleHeight={this.visibleHeight} focus={this.props.focus} diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index 758795ed5..289d3a9a1 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -9,7 +9,7 @@ import { documentSchema } from '../../../new_fields/documentSchemas'; import { Id } from "../../../new_fields/FieldSymbols"; import { createSchema, makeInterface } from '../../../new_fields/Schema'; import { Cast, NumCast } from "../../../new_fields/Types"; -import { emptyFunction, emptyPath, returnFalse } from "../../../Utils"; +import { emptyFunction, emptyPath, returnFalse, returnTrue } from "../../../Utils"; import { Transform } from "../../util/Transform"; import { CollectionViewType } from '../collections/CollectionView'; import { DocExtendableComponent } from '../DocComponent'; @@ -175,6 +175,7 @@ export class PresElementBox extends DocExtendableComponent<FieldViewProps, PresD DataDocument={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]} LibraryPath={emptyPath} fitToBox={true} + rootSelected={returnTrue} addDocument={returnFalse} removeDocument={returnFalse} addDocTab={returnFalse} @@ -196,7 +197,7 @@ export class PresElementBox extends DocExtendableComponent<FieldViewProps, PresD const treecontainer = this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Tree; const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : ""); const pbi = "presElementBox-interaction"; - return !this.presElementDoc ? (null) : ( + return !(this.presElementDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : ( <div className={className} key={this.props.Document[Id] + this.indexInPres} style={{ outlineWidth: Doc.IsBrushed(this.targetDoc) ? `1px` : "0px", }} onClick={e => { this.props.focus(this.presElementDoc); e.stopPropagation(); }}> diff --git a/src/client/views/search/CheckBox.tsx b/src/client/views/search/CheckBox.tsx index a9d48219a..8c97d5dbc 100644 --- a/src/client/views/search/CheckBox.tsx +++ b/src/client/views/search/CheckBox.tsx @@ -17,8 +17,8 @@ interface CheckBoxProps { export class CheckBox extends React.Component<CheckBoxProps>{ // true = checked, false = unchecked @observable private _status: boolean; - @observable private uncheckTimeline: anime.AnimeTimelineInstance; - @observable private checkTimeline: anime.AnimeTimelineInstance; + // @observable private uncheckTimeline: anime.AnimeTimelineInstance; + // @observable private checkTimeline: anime.AnimeTimelineInstance; @observable private checkRef: any; @observable private _resetReaction?: IReactionDisposer; @@ -28,87 +28,87 @@ export class CheckBox extends React.Component<CheckBoxProps>{ this._status = this.props.originalStatus; this.checkRef = React.createRef(); - this.checkTimeline = anime.timeline({ - loop: false, - autoplay: false, - direction: "normal", - }); this.uncheckTimeline = anime.timeline({ - loop: false, - autoplay: false, - direction: "normal", - }); + // this.checkTimeline = anime.timeline({ + // loop: false, + // autoplay: false, + // direction: "normal", + // }); this.uncheckTimeline = anime.timeline({ + // loop: false, + // autoplay: false, + // direction: "normal", + // }); } - componentDidMount = () => { - this.uncheckTimeline.add({ - targets: this.checkRef.current, - easing: "easeInOutQuad", - duration: 500, - opacity: 0, - }); - this.checkTimeline.add({ - targets: this.checkRef.current, - easing: "easeInOutQuad", - duration: 500, - strokeDashoffset: [anime.setDashoffset, 0], - opacity: 1 - }); + // componentDidMount = () => { + // this.uncheckTimeline.add({ + // targets: this.checkRef.current, + // easing: "easeInOutQuad", + // duration: 500, + // opacity: 0, + // }); + // this.checkTimeline.add({ + // targets: this.checkRef.current, + // easing: "easeInOutQuad", + // duration: 500, + // strokeDashoffset: [anime.setDashoffset, 0], + // opacity: 1 + // }); - if (this.props.originalStatus) { - this.checkTimeline.play(); - this.checkTimeline.restart(); - } + // if (this.props.originalStatus) { + // this.checkTimeline.play(); + // this.checkTimeline.restart(); + // } - this._resetReaction = reaction( - () => this.props.parent._resetBoolean, - () => { - if (this.props.parent._resetBoolean) { - runInAction(() => { - this.reset(); - this.props.parent._resetCounter++; - if (this.props.parent._resetCounter === this.props.numCount) { - this.props.parent._resetCounter = 0; - this.props.parent._resetBoolean = false; - } - }); - } - }, - ); - } + // this._resetReaction = reaction( + // () => this.props.parent._resetBoolean, + // () => { + // if (this.props.parent._resetBoolean) { + // runInAction(() => { + // this.reset(); + // this.props.parent._resetCounter++; + // if (this.props.parent._resetCounter === this.props.numCount) { + // this.props.parent._resetCounter = 0; + // this.props.parent._resetBoolean = false; + // } + // }); + // } + // }, + // ); + // } - @action.bound - reset() { - if (this.props.default) { - if (!this._status) { - this._status = true; - this.checkTimeline.play(); - this.checkTimeline.restart(); - } - } - else { - if (this._status) { - this._status = false; - this.uncheckTimeline.play(); - this.uncheckTimeline.restart(); - } - } + // @action.bound + // reset() { + // if (this.props.default) { + // if (!this._status) { + // this._status = true; + // this.checkTimeline.play(); + // this.checkTimeline.restart(); + // } + // } + // else { + // if (this._status) { + // this._status = false; + // this.uncheckTimeline.play(); + // this.uncheckTimeline.restart(); + // } + // } - this.props.updateStatus(this.props.default); - } + // this.props.updateStatus(this.props.default); + // } @action.bound onClick = () => { - if (this._status) { - this.uncheckTimeline.play(); - this.uncheckTimeline.restart(); - } - else { - this.checkTimeline.play(); - this.checkTimeline.restart(); + // if (this._status) { + // this.uncheckTimeline.play(); + // this.uncheckTimeline.restart(); + // } + // else { + // this.checkTimeline.play(); + // this.checkTimeline.restart(); - } - this._status = !this._status; - this.props.updateStatus(this._status); + // } + // this._status = !this._status; + // this.props.updateStatus(this._status); } diff --git a/src/client/views/search/FilterBox.scss b/src/client/views/search/FilterBox.scss index ebb39460d..094ea9cc5 100644 --- a/src/client/views/search/FilterBox.scss +++ b/src/client/views/search/FilterBox.scss @@ -4,7 +4,6 @@ .filter-form { padding: 25px; width: 440px; - background: whitesmoke; position: relative; right: 1px; color: grey; @@ -12,9 +11,7 @@ display: inline-block; transform-origin: top; overflow: auto; - border-radius: 15px; - box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; - border: solid #BBBBBBBB 1px; + border-bottom: solid black 3px; .top-filter-header { @@ -124,7 +121,7 @@ max-width: 40px; flex: initial; - &.icon{ + &.icon { width: 40px; text-align: center; margin-bottom: 5px; @@ -150,7 +147,7 @@ transition: all 0.2s ease-in-out; } - &.icon:hover + .description { + &.icon:hover+.description { opacity: 1; } } diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index d4c9e67fb..1c05ff864 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -387,7 +387,7 @@ export class FilterBox extends React.Component { {/* {this.getActiveFilters()} */} </div> {this._filterOpen ? ( - <div className="filter-form" onPointerDown={this.stopProp} id="filter-form" style={this._filterOpen ? { display: "flex" } : { display: "none" }}> + <div className="filter-form" onPointerDown={this.stopProp} id="filter-form" style={this._filterOpen ? { display: "flex", background: "black" } : { display: "none" }}> <div className="top-filter-header" style={{ display: "flex", width: "100%" }}> <div id="header">Filter Search Results</div> <div style={{ marginLeft: "auto" }}></div> diff --git a/src/client/views/search/IconBar.scss b/src/client/views/search/IconBar.scss index 2555ad271..013dcd57e 100644 --- a/src/client/views/search/IconBar.scss +++ b/src/client/views/search/IconBar.scss @@ -2,10 +2,9 @@ .icon-bar { display: flex; + flex-wrap: wrap; justify-content: space-evenly; - align-items: center; - height: 35px; + height: auto; width: 100%; - flex-wrap: wrap; - margin-bottom: 10px; + flex-direction: row-reverse; }
\ No newline at end of file diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx index cff397407..46c109934 100644 --- a/src/client/views/search/IconBar.tsx +++ b/src/client/views/search/IconBar.tsx @@ -9,7 +9,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { library } from '@fortawesome/fontawesome-svg-core'; import * as _ from "lodash"; import { IconButton } from './IconButton'; -import { FilterBox } from './FilterBox'; +import { DocumentType } from "../../documents/DocumentTypes"; + library.add(faSearch); library.add(faObjectGroup); @@ -25,6 +26,9 @@ library.add(faBan); @observer export class IconBar extends React.Component { + public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB]; + + @observable private _icons: string[] = this._allIcons; static Instance: IconBar; @@ -33,16 +37,22 @@ export class IconBar extends React.Component { @observable public _reset: number = 0; @observable public _select: number = 0; + @action.bound + updateIcon(newArray: string[]) { this._icons = newArray; } + + @action.bound + getIcons(): string[] { return this._icons; } + constructor(props: any) { super(props); IconBar.Instance = this; } @action.bound - getList(): string[] { return FilterBox.Instance.getIcons(); } + getList(): string[] { return this.getIcons(); } @action.bound - updateList(newList: string[]) { FilterBox.Instance.updateIcon(newList); } + updateList(newList: string[]) { this.updateIcon(newList); } @action.bound resetSelf = () => { @@ -53,13 +63,13 @@ export class IconBar extends React.Component { @action.bound selectAll = () => { this._selectAllClicked = true; - this.updateList(FilterBox.Instance._allIcons); + this.updateList(this._allIcons); } render() { return ( <div className="icon-bar"> - {FilterBox.Instance._allIcons.map((type: string) => + {this._allIcons.map((type: string) => <IconButton key={type.toString()} type={type} /> )} </div> diff --git a/src/client/views/search/IconButton.scss b/src/client/views/search/IconButton.scss index 4a3107676..4ec03c7c9 100644 --- a/src/client/views/search/IconButton.scss +++ b/src/client/views/search/IconButton.scss @@ -5,7 +5,6 @@ flex-direction: column; align-items: center; width: 30px; - height: 60px; .type-icon { height: 30px; diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx index f01508141..4f94139d9 100644 --- a/src/client/views/search/IconButton.tsx +++ b/src/client/views/search/IconButton.tsx @@ -11,7 +11,6 @@ import '../globalCssVariables.scss'; import * as _ from "lodash"; import { IconBar } from './IconBar'; import { props } from 'bluebird'; -import { FilterBox } from './FilterBox'; import { Search } from '../../../server/Search'; import { gravity } from 'sharp'; @@ -34,7 +33,7 @@ interface IconButtonProps { @observer export class IconButton extends React.Component<IconButtonProps>{ - @observable private _isSelected: boolean = FilterBox.Instance.getIcons().indexOf(this.props.type) !== -1; + @observable private _isSelected: boolean = IconBar.Instance.getIcons().indexOf(this.props.type) !== -1; @observable private _hover = false; private _resetReaction?: IReactionDisposer; private _selectAllReaction?: IReactionDisposer; @@ -108,7 +107,7 @@ export class IconButton extends React.Component<IconButtonProps>{ @action.bound onClick = () => { - const newList: string[] = FilterBox.Instance.getIcons(); + const newList: string[] = IconBar.Instance.getIcons(); if (!this._isSelected) { this._isSelected = true; @@ -119,21 +118,24 @@ export class IconButton extends React.Component<IconButtonProps>{ _.pull(newList, this.props.type); } - FilterBox.Instance.updateIcon(newList); + IconBar.Instance.updateIcon(newList); } selected = { opacity: 1, - backgroundColor: "rgb(128, 128, 128)" + backgroundColor: "#121721", + //backgroundColor: "rgb(128, 128, 128)" }; notSelected = { opacity: 0.2, + backgroundColor: "#121721", }; hoverStyle = { opacity: 1, - backgroundColor: "rgb(178, 206, 248)" //$darker-alt-accent + backgroundColor: "rgb(128, 128, 128)" + //backgroundColor: "rgb(178, 206, 248)" //$darker-alt-accent }; @action.bound @@ -186,7 +188,7 @@ export class IconButton extends React.Component<IconButtonProps>{ > {this.getFA()} </div> - <div className="filter-description">{this.props.type}</div> + {/* <div className="filter-description">{this.props.type}</div> */} </div> ); } diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index f492ea773..f0223ca76 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -4,20 +4,21 @@ .searchBox-container { display: flex; flex-direction: column; - width:100%; - height:100%; + width: 100%; + height: 100%; position: absolute; font-size: 10px; line-height: 1; - overflow: hidden; + overflow: auto; + background: lightgrey, } + .searchBox-bar { height: 32px; display: flex; justify-content: flex-end; align-items: center; padding-left: 2px; - padding-right: 2px; .searchBox-barChild { @@ -33,8 +34,7 @@ -webkit-transition: width 0.4s; transition: width 0.4s; align-self: stretch; - margin-left: 2px; - margin-right: 2px + } .searchBox-input:focus { @@ -44,6 +44,10 @@ &.searchBox-filter { align-self: stretch; + button:hover{ + transform:scale(1.0); + background:"#121721"; + } } &.searchBox-submit { @@ -65,7 +69,7 @@ } .searchBox-results { - display:flex; + display: flex; flex-direction: column; top: 300px; display: flex; @@ -83,6 +87,249 @@ text-transform: uppercase; text-align: left; font-weight: bold; - margin-left: 28px; + } +} + +.filter-form { + position: relative; + background: #121721; + flex-direction: column; + transform-origin: top; + transition: height 0.3s ease, display 0.6s ease; + height:0px; + overflow:hidden; + + + .filter-header { + display: flex; + position: relative; + flex-wrap:wrap; + right: 1px; + color: grey; + flex-direction: row-reverse; + transform-origin: top; + justify-content: space-evenly; + margin-bottom: 5px; + overflow:hidden; + transition:height 0.3s ease-out; + + + + .filter-item { + position: relative; + border:1px solid grey; + border-radius: 16px; + + } + } + + .filter-body { + position: relative; + right: 1px; + color: grey; + transform-origin: top; + border-top: 0px; + //padding-top: 5px; + margin-left: 10px; + margin-right: 10px; + overflow:hidden; + transition:height 0.3s ease-out; + height:0px; + + } + .filter-key { + position: relative; + right: 1px; + color: grey; + transform-origin: top; + border-top: 0px; + //padding-top: 5px; + margin-left: 10px; + margin-right: 10px; + overflow:hidden; + transition:height 0.3s ease-out; + height:0px; + .filter-keybar { + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; + height: auto; + width: 100%; + flex-direction: row-reverse; + margin-top:5px; + + .filter-item { + position: relative; + border:1px solid grey; + border-radius: 16px; + + } + } + + + } +} + +// .top-filter-header { + +// #header { +// text-transform: uppercase; +// letter-spacing: 2px; +// font-size: 13; +// width: 80%; +// } + +// .close-icon { +// width: 20%; +// opacity: .6; +// position: relative; +// display: block; + +// .line { +// display: block; +// background: $alt-accent; +// width: 20; +// height: 3; +// position: absolute; +// right: 0; +// border-radius: ($height-line / 2); + +// &.line-1 { +// transform: rotate(45deg); +// top: 45%; +// } + +// &.line-2 { +// transform: rotate(-45deg); +// top: 45%; +// } +// } +// } + +// .close-icon:hover { +// opacity: 1; +// } + +// } + +// .filter-options { + +// .filter-div { +// margin-top: 10px; +// margin-bottom: 10px; +// display: inline-block; +// width: 100%; +// border-color: rgba(178, 206, 248, .2); // $darker-alt-accent +// border-top-style: solid; + +// .filter-header { +// display: flex; +// align-items: center; +// margin-bottom: 10px; +// letter-spacing: 2px; + +// .filter-title { +// font-size: 13; +// text-transform: uppercase; +// margin-top: 10px; +// margin-bottom: 10px; +// -webkit-transition: all 0.2s ease-in-out; +// -moz-transition: all 0.2s ease-in-out; +// -o-transition: all 0.2s ease-in-out; +// transition: all 0.2s ease-in-out; +// } +// } + +// .filter-header:hover .filter-title { +// transform: scale(1.05); +// } + +// .filter-panel { +// max-height: 0px; +// width: 100%; +// overflow: hidden; +// opacity: 0; +// transform-origin: top; +// -webkit-transition: all 0.2s ease-in-out; +// -moz-transition: all 0.2s ease-in-out; +// -o-transition: all 0.2s ease-in-out; +// transition: all 0.2s ease-in-out; +// text-align: center; +// } +// } +// } + +// .filter-buttons { +// border-color: rgba(178, 206, 248, .2); // $darker-alt-accent +// border-top-style: solid; +// padding-top: 10px; +// } + + +.active-filters { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + width: 100%; + margin-right: 30px; + position: relative; + + .active-icon { + max-width: 40px; + flex: initial; + + &.icon { + width: 40px; + text-align: center; + margin-bottom: 5px; + position: absolute; + } + + &.container { + display: flex; + flex-direction: column; + width: 40px; + } + + &.description { + text-align: center; + top: 40px; + position: absolute; + width: 40px; + font-size: 9px; + opacity: 0; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + } + + &.icon:hover+.description { + opacity: 1; + } + } + + .col-icon { + height: 35px; + margin-left: 5px; + width: 35px; + background-color: black; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + + .save-filter, + .reset-filter, + .all-filter { + background-color: gray; + } + + .save-filter:hover, + .reset-filter:hover, + .all-filter:hover { + background-color: $darker-alt-accent; + } } }
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 586365f6e..67af661c9 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,37 +1,51 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, observable, runInAction, IReactionDisposer, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as rp from 'request-promise'; import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; -import { Cast, NumCast } from '../../../new_fields/Types'; +import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; import { Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { SetupDrag } from '../../util/DragManager'; import { SearchUtil } from '../../util/SearchUtil'; -import { FilterBox } from './FilterBox'; -import "./FilterBox.scss"; import "./SearchBox.scss"; import { SearchItem } from './SearchItem'; import { IconBar } from './IconBar'; +import { FieldView } from '../nodes/FieldView'; +import { DocumentType } from "../../documents/DocumentTypes"; +import { DocumentView } from '../nodes/DocumentView'; +import { SelectionManager } from '../../util/SelectionManager'; library.add(faTimes); +export interface SearchProps { + id: string; + searchQuery?: string; + filterQquery?: string; +} + +export enum Keys { + TITLE = "title", + AUTHOR = "author", + DATA = "data" +} + @observer -export class SearchBox extends React.Component { +export class SearchBox extends React.Component<SearchProps> { @observable private _searchString: string = ""; @observable private _resultsOpen: boolean = false; @observable private _searchbarOpen: boolean = false; @observable private _results: [Doc, string[], string[]][] = []; - private _resultsSet = new Map<Doc, number>(); @observable private _openNoResults: boolean = false; @observable private _visibleElements: JSX.Element[] = []; - private resultsRef = React.createRef<HTMLDivElement>(); + private _resultsSet = new Map<Doc, number>(); + private _resultsRef = React.createRef<HTMLDivElement>(); public inputRef = React.createRef<HTMLInputElement>(); private _isSearch: ("search" | "placeholder" | undefined)[] = []; @@ -42,10 +56,18 @@ export class SearchBox extends React.Component { private _maxSearchIndex: number = 0; private _curRequest?: Promise<any> = undefined; + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } + + + //if true, any keywords can be used. if false, all keywords are required. + //this also serves as an indicator if the word status filter is applied + @observable private _basicWordStatus: boolean = false; + @observable private _nodeStatus: boolean = false; + @observable private _keyStatus: boolean = false; + constructor(props: any) { super(props); - SearchBox.Instance = this; this.resultsScrolled = this.resultsScrolled.bind(this); } @@ -53,21 +75,21 @@ export class SearchBox extends React.Component { componentDidMount = () => { if (this.inputRef.current) { this.inputRef.current.focus(); + runInAction(() => this._searchbarOpen = true); + } + if (this.props.searchQuery && this.props.filterQquery) { + console.log(this.props.searchQuery); + const sq = this.props.searchQuery; runInAction(() => { - this._searchbarOpen = true; + this._searchString = sq; + this.submitSearch(); }); } } + @action - getViews = async (doc: Doc) => { - const results = await SearchUtil.GetViewsOfDocument(doc); - let toReturn: Doc[] = []; - await runInAction(() => { - toReturn = results; - }); - return toReturn; - } + getViews = (doc: Doc) => SearchUtil.GetViewsOfDocument(doc) @action.bound onChange(e: React.ChangeEvent<HTMLInputElement>) { @@ -106,33 +128,186 @@ export class SearchBox extends React.Component { } } + public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB]; + //if true, any keywords can be used. if false, all keywords are required. + //this also serves as an indicator if the word status filter is applied + @observable private _filterOpen: boolean = false; + //if icons = all icons, then no icon filter is applied + @observable private _icons: string[] = this._allIcons; + //if all of these are true, no key filter is applied + @observable private _titleFieldStatus: boolean = true; + @observable private _authorFieldStatus: boolean = true; + //this also serves as an indicator if the collection status filter is applied + @observable public _deletedDocsStatus: boolean = false; + @observable private _collectionStatus = false; + + + getFinalQuery(query: string): string { + //alters the query so it looks in the correct fields + //if this is true, then not all of the field boxes are checked + //TODO: data + if (this.fieldFiltersApplied) { + query = this.applyBasicFieldFilters(query); + query = query.replace(/\s+/g, ' ').trim(); + } + + //alters the query based on if all words or any words are required + //if this._wordstatus is false, all words are required and a + is added before each + if (!this._basicWordStatus) { + query = this.basicRequireWords(query); + query = query.replace(/\s+/g, ' ').trim(); + } + + //if should be searched in a specific collection + if (this._collectionStatus) { + query = this.addCollectionFilter(query); + query = query.replace(/\s+/g, ' ').trim(); + } + return query; + } + + basicRequireWords(query: string): string { + const oldWords = query.split(" "); + const newWords: string[] = []; + oldWords.forEach(word => { + const newWrd = "+" + word; + newWords.push(newWrd); + }); + query = newWords.join(" "); + + return query; + } + + @action + filterDocsByType(docs: Doc[]) { + if (this._icons.length === this._allIcons.length) { + return docs; + } + const finalDocs: Doc[] = []; + docs.forEach(doc => { + const layoutresult = Cast(doc.type, "string"); + if (layoutresult && this._icons.includes(layoutresult)) { + finalDocs.push(doc); + } + }); + return finalDocs; + } + + addCollectionFilter(query: string): string { + const collections: Doc[] = this.getCurCollections(); + const oldWords = query.split(" "); + + const collectionString: string[] = []; + collections.forEach(doc => { + const proto = doc.proto; + const protoId = (proto || doc)[Id]; + const colString: string = "{!join from=data_l to=id}id:" + protoId + " "; + collectionString.push(colString); + }); + + let finalColString = collectionString.join(" "); + finalColString = finalColString.trim(); + return "+(" + finalColString + ")" + query; + } + + get filterTypes() { + return this._icons.length === this._allIcons.length ? undefined : this._icons; + } + + @action.bound + updateIcon(newArray: string[]) { this._icons = newArray; } + + @action.bound + getIcons(): string[] { return this._icons; } + + //TODO: basically all of this + //gets all of the collections of all the docviews that are selected + //if a collection is the only thing selected, search only in that collection (not its container) + getCurCollections(): Doc[] { + const selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments(); + const collections: Doc[] = []; + + selectedDocs.forEach(async element => { + const layout: string = StrCast(element.props.Document.layout); + //checks if selected view (element) is a collection. if it is, adds to list to search through + if (layout.indexOf("Collection") > -1) { + //makes sure collections aren't added more than once + if (!collections.includes(element.props.Document)) { + collections.push(element.props.Document); + } + } + //makes sure collections aren't added more than once + if (element.props.ContainingCollectionDoc && !collections.includes(element.props.ContainingCollectionDoc)) { + collections.push(element.props.ContainingCollectionDoc); + } + }); + + return collections; + } + + + applyBasicFieldFilters(query: string) { + let finalQuery = ""; + + if (this._titleFieldStatus) { + finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TITLE); + } + if (this._authorFieldStatus) { + finalQuery = finalQuery + this.basicFieldFilters(query, Keys.AUTHOR); + } + if (this._deletedDocsStatus) { + finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA); + } + return finalQuery; + } + + basicFieldFilters(query: string, type: string): string { + const oldWords = query.split(" "); + let mod = ""; + + if (type === Keys.AUTHOR) { + mod = " author_t:"; + } if (type === Keys.DATA) { + //TODO + } if (type === Keys.TITLE) { + mod = " title_t:"; + } + + const newWords: string[] = []; + oldWords.forEach(word => { + const newWrd = mod + word; + newWords.push(newWrd); + }); + + query = newWords.join(" "); + + return query; + } + + get fieldFiltersApplied() { return !(this._authorFieldStatus && this._titleFieldStatus); } + + @action submitSearch = async () => { - let query = this._searchString; - query = FilterBox.Instance.getFinalQuery(query); + const query = this._searchString; + this.getFinalQuery(query); this._results = []; this._resultsSet.clear(); this._isSearch = []; this._visibleElements = []; - FilterBox.Instance.closeFilter(); - - //if there is no query there should be no result - if (query === "") { - return; - } - else { + if (query !== "") { this._endIndex = 12; this._maxSearchIndex = 0; this._numTotalResults = -1; await this.getResults(query); - } - runInAction(() => { - this._resultsOpen = true; - this._searchbarOpen = true; - this._openNoResults = true; - this.resultsScrolled(); - }); + runInAction(() => { + this._resultsOpen = true; + this._searchbarOpen = true; + this._openNoResults = true; + this.resultsScrolled(); + }); + } } getAllResults = async (query: string) => { @@ -140,11 +315,14 @@ export class SearchBox extends React.Component { } private get filterQuery() { - const types = FilterBox.Instance.filterTypes; - const includeDeleted = FilterBox.Instance.getDataStatus(); - return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted_b:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}" OR type_t:"extension"`).join(" ")})` : ""); + const types = this.filterTypes; + const includeDeleted = this.getDataStatus() ? "" : " AND NOT deleted_b:true"; + const includeIcons = this.getDataStatus() ? "" : " AND NOT type_t:fonticonbox"; + return "NOT baseProto_b:true" + includeDeleted + includeIcons + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : ""); } + getDataStatus() { return this._deletedDocsStatus; } + private NumResults = 25; private lockPromise?: Promise<void>; @@ -155,7 +333,6 @@ export class SearchBox extends React.Component { this.lockPromise = new Promise(async res => { while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) { this._curRequest = SearchUtil.Search(query, true, { fq: this.filterQuery, start: this._maxSearchIndex, rows: this.NumResults, hl: true, "hl.fl": "*" }).then(action(async (res: SearchUtil.DocSearchResult) => { - // happens at the beginning if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { this._numTotalResults = res.numFound; @@ -168,9 +345,9 @@ export class SearchBox extends React.Component { const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc)); const highlights: typeof res.highlighting = {}; docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); - const filteredDocs = FilterBox.Instance.filterDocsByType(docs); + const filteredDocs = this.filterDocsByType(docs); runInAction(() => { - // this._results.push(...filteredDocs); + //this._results.push(...filteredDocs); filteredDocs.forEach(doc => { const index = this._resultsSet.get(doc); const highlight = highlights[doc[Id]]; @@ -200,9 +377,8 @@ export class SearchBox extends React.Component { collectionRef = React.createRef<HTMLSpanElement>(); startDragCollection = async () => { - const res = await this.getAllResults(FilterBox.Instance.getFinalQuery(this._searchString)); - const filtered = FilterBox.Instance.filterDocsByType(res.docs); - // console.log(this._results) + const res = await this.getAllResults(this.getFinalQuery(this._searchString)); + const filtered = this.filterDocsByType(res.docs); const docs = filtered.map(doc => { const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); if (isProto) { @@ -234,22 +410,19 @@ export class SearchBox extends React.Component { y += 300; } } - return Docs.Create.TreeDocument(docs, { _width: 200, _height: 400, title: `Search Docs: "${this._searchString}"` }); + return Docs.Create.QueryDocument({ _autoHeight: true, title: this._searchString, filterQuery: this.filterQuery, searchQuery: this._searchString }); } @action.bound openSearch(e: React.SyntheticEvent) { e.stopPropagation(); this._openNoResults = false; - FilterBox.Instance.closeFilter(); this._resultsOpen = true; this._searchbarOpen = true; - FilterBox.Instance._pointerTime = e.timeStamp; } @action.bound closeSearch = () => { - FilterBox.Instance.closeFilter(); this.closeResults(); this._searchbarOpen = false; } @@ -267,11 +440,11 @@ export class SearchBox extends React.Component { @action resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => { - if (!this.resultsRef.current) return; - const scrollY = e ? e.currentTarget.scrollTop : this.resultsRef.current ? this.resultsRef.current.scrollTop : 0; + if (!this._resultsRef.current) return; + const scrollY = e ? e.currentTarget.scrollTop : this._resultsRef.current ? this._resultsRef.current.scrollTop : 0; const itemHght = 53; const startIndex = Math.floor(Math.max(0, scrollY / itemHght)); - const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this.resultsRef.current.getBoundingClientRect().height / itemHght))); + const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this._resultsRef.current.getBoundingClientRect().height / itemHght))); this._endIndex = endIndex === -1 ? 12 : endIndex; @@ -337,9 +510,131 @@ export class SearchBox extends React.Component { @computed get resultHeight() { return this._numTotalResults * 70; } + //if true, any keywords can be used. if false, all keywords are required. + @action.bound + handleWordQueryChange = () => { + this._basicWordStatus = !this._basicWordStatus; + } + + @action.bound + handleNodeChange = () => { + this._nodeStatus = !this._nodeStatus; + if (this._nodeStatus) { + this.expandSection(`node${this.props.id}`); + } + else { + this.collapseSection(`node${this.props.id}`); + } + } + + @action.bound + handleKeyChange = () => { + this._keyStatus = !this._keyStatus; + if (this._keyStatus) { + this.expandSection(`key${this.props.id}`); + } + else { + this.collapseSection(`key${this.props.id}`); + } + } + + @action.bound + handleFilterChange = () => { + this._filterOpen = !this._filterOpen; + if (this._filterOpen) { + this.expandSection(`filterhead${this.props.id}`); + document.getElementById(`filterhead${this.props.id}`)!.style.padding = "5"; + } + else { + this.collapseSection(`filterhead${this.props.id}`); + + + } + } + + @computed + get menuHeight() { + return document.getElementById("hi")?.clientHeight; + } + + + collapseSection(thing: string) { + const id = this.props.id; + const element = document.getElementById(thing)!; + // get the height of the element's inner content, regardless of its actual size + const sectionHeight = element.scrollHeight; + + // temporarily disable all css transitions + const elementTransition = element.style.transition; + element.style.transition = ''; + + // on the next frame (as soon as the previous style change has taken effect), + // explicitly set the element's height to its current pixel height, so we + // aren't transitioning out of 'auto' + requestAnimationFrame(function () { + element.style.height = sectionHeight + 'px'; + element.style.transition = elementTransition; + + // on the next frame (as soon as the previous style change has taken effect), + // have the element transition to height: 0 + requestAnimationFrame(function () { + element.style.height = 0 + 'px'; + thing === `filterhead${id}` ? document.getElementById(`filterhead${id}`)!.style.padding = "0" : null; + }); + }); + + // mark the section as "currently collapsed" + element.setAttribute('data-collapsed', 'true'); + } + + expandSection(thing: string) { + console.log("expand"); + const element = document.getElementById(thing)!; + // get the height of the element's inner content, regardless of its actual size + const sectionHeight = element.scrollHeight; + + // have the element transition to the height of its inner content + element.style.height = sectionHeight + 'px'; + + // when the next css transition finishes (which should be the one we just triggered) + element.addEventListener('transitionend', function handler(e) { + // remove this event listener so it only gets triggered once + console.log("autoset"); + element.removeEventListener('transitionend', handler); + + // remove "height" from the element's inline styles, so it can return to its initial value + element.style.height = "auto"; + //element.style.height = undefined; + }); + + // mark the section as "currently not collapsed" + element.setAttribute('data-collapsed', 'false'); + + } + + autoset(thing: string) { + const element = document.getElementById(thing)!; + console.log("autoset"); + element.removeEventListener('transitionend', function (e) { }); + + // remove "height" from the element's inline styles, so it can return to its initial value + element.style.height = "auto"; + //element.style.height = undefined; + } + + @action.bound + updateTitleStatus() { this._titleFieldStatus = !this._titleFieldStatus; } + + @action.bound + updateAuthorStatus() { this._authorFieldStatus = !this._authorFieldStatus; } + + @action.bound + updateDataStatus() { this._deletedDocsStatus = !this._deletedDocsStatus; } + render() { + return ( - <div className="searchBox-container" onPointerDown={e => { e.stopPropagation(); e.preventDefault(); }}> + <div className="searchBox-container"> <div className="searchBox-bar"> <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, () => this._searchString ? this.startDragCollection() : undefined)} ref={this.collectionRef} title="Drag Results as Collection"> <FontAwesomeIcon icon="object-group" size="lg" /> @@ -347,16 +642,31 @@ export class SearchBox extends React.Component { <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef} className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch} style={{ width: this._searchbarOpen ? "500px" : "100px" }} /> - <button className="searchBox-barChild searchBox-filter" title="Advanced Filtering Options" onClick={() => { }} onPointerDown={FilterBox.Instance.stopProp}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button> + <button className="searchBox-barChild searchBox-filter" title="Advanced Filtering Options" onClick={() => this.handleFilterChange()}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button> </div> - <div className="searchBox-quickFilter" onPointerDown={this.openSearch}> - <div className="filter-panel"><IconBar /></div> + + <div id={`filterhead${this.props.id}`} className="filter-form" > + <div id={`filterhead2${this.props.id}`} className="filter-header" style={this._filterOpen ? {} : {}}> + <button className="filter-item" style={this._basicWordStatus ? { background: "#aaaaa3", } : {}} onClick={this.handleWordQueryChange}>Keywords</button> + <button className="filter-item" style={this._keyStatus ? { background: "#aaaaa3" } : {}} onClick={this.handleKeyChange}>Keys</button> + <button className="filter-item" style={this._nodeStatus ? { background: "#aaaaa3" } : {}} onClick={this.handleNodeChange}>Nodes</button> + </div> + <div id={`node${this.props.id}`} className="filter-body" style={this._nodeStatus ? { borderTop: "grey 1px solid" } : { borderTop: "0px" }}> + <IconBar /> + </div> + <div className="filter-key" id={`key${this.props.id}`} style={this._keyStatus ? { borderTop: "grey 1px solid" } : { borderTop: "0px" }}> + <div className="filter-keybar"> + <button className="filter-item" style={this._titleFieldStatus ? { background: "#aaaaa3", } : {}} onClick={this.updateTitleStatus}>Title</button> + <button className="filter-item" style={this._deletedDocsStatus ? { background: "#aaaaa3", } : {}} onClick={this.updateDataStatus}>Deleted Docs</button> + <button className="filter-item" style={this._authorFieldStatus ? { background: "#aaaaa3", } : {}} onClick={this.updateAuthorStatus}>Author</button> + </div> + </div> </div> <div className="searchBox-results" onScroll={this.resultsScrolled} style={{ display: this._resultsOpen ? "flex" : "none", height: this.resFull ? "auto" : this.resultHeight, overflow: "visibile" // this.resFull ? "auto" : "visible" - }} ref={this.resultsRef}> + }} ref={this._resultsRef}> {this._visibleElements} </div> </div> diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 63cef5101..0d77026ad 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -7,7 +7,7 @@ import { observer } from "mobx-react"; import { Doc } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, emptyPath, returnFalse, Utils } from "../../../Utils"; +import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue } from "../../../Utils"; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, SetupDrag } from "../../util/DragManager"; @@ -158,6 +158,7 @@ export class SearchItem extends React.Component<SearchItemProps> { <ContentFittingDocumentView Document={this.props.doc} LibraryPath={emptyPath} + rootSelected={returnFalse} fitToBox={StrCast(this.props.doc.type).indexOf(DocumentType.COL) !== -1} addDocument={returnFalse} removeDocument={returnFalse} diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index 1d2d57b96..c87ac719c 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -7,7 +7,7 @@ import { Doc, DocListCast } from '../new_fields/Doc'; import { Docs } from '../client/documents/Documents'; import { CollectionView } from '../client/views/collections/CollectionView'; import { DocumentView } from '../client/views/nodes/DocumentView'; -import { emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue } from '../Utils'; +import { emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, returnZero } from '../Utils'; import { Transform } from '../client/util/Transform'; import { library } from '@fortawesome/fontawesome-svg-core'; import { faPenNib, faHighlighter, faEraser, faMousePointer, faBreadSlice, faTrash, faCheck, faLongArrowAltLeft } from '@fortawesome/free-solid-svg-icons'; @@ -124,10 +124,13 @@ export default class MobileInterface extends React.Component { addDocument={returnFalse} addDocTab={returnFalse} pinToPres={emptyFunction} + rootSelected={returnFalse} removeDocument={undefined} onClick={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={() => window.screen.width} PanelHeight={() => window.screen.height} renderDepth={0} @@ -137,10 +140,7 @@ export default class MobileInterface extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne}> - </DocumentView>; + ContainingCollectionDoc={undefined} />; } return "hello"; } @@ -175,6 +175,8 @@ export default class MobileInterface extends React.Component { e.stopPropagation(); } + panelHeight = () => window.innerHeight; + panelWidth = () => window.innerWidth; renderInkingContent = () => { console.log("rendering inking content"); // TODO: support panning and zooming @@ -200,11 +202,13 @@ export default class MobileInterface extends React.Component { LibraryPath={emptyPath} fieldKey={""} dropAction={"alias"} - bringToFront={emptyFunction } + bringToFront={emptyFunction} addDocTab={returnFalse} pinToPres={emptyFunction} - PanelHeight={() => window.innerHeight} - PanelWidth={() => window.innerWidth} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + NativeHeight={returnZero} + NativeWidth={returnZero} focus={emptyFunction} isSelected={returnFalse} select={emptyFunction} @@ -214,7 +218,8 @@ export default class MobileInterface extends React.Component { ScreenToLocalTransform={Transform.Identity} renderDepth={0} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined}> + ContainingCollectionDoc={undefined} + rootSelected={returnTrue}> </CollectionView> </div> ); @@ -285,10 +290,13 @@ export default class MobileInterface extends React.Component { addDocument={returnFalse} addDocTab={returnFalse} pinToPres={emptyFunction} + rootSelected={returnFalse} removeDocument={undefined} onClick={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={() => window.screen.width} PanelHeight={() => window.screen.height} renderDepth={0} @@ -298,10 +306,7 @@ export default class MobileInterface extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne}> - </DocumentView> + ContainingCollectionDoc={undefined} /> </div> ); } diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 85926e393..d381447c5 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -80,7 +80,7 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { } export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { - return Cast(field, Doc); + return await Cast(field, Doc); } export function DocListCast(field: FieldResult): Doc[] { @@ -713,7 +713,7 @@ export namespace Doc { export function SetUserDoc(doc: Doc) { manager._user_doc = doc; } export function IsBrushed(doc: Doc) { return computedFn(function IsBrushed(doc: Doc) { - return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); + return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); })(doc); } // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message) @@ -787,15 +787,10 @@ export namespace Doc { brushManager.BrushedDoc.clear(); } - export function setChildLayout(target: Doc, source?: Doc) { - target.childLayout = source && source.isTemplateDoc ? source : source && - source.dragFactory instanceof Doc && source.dragFactory.isTemplateDoc ? source.dragFactory : - source && source.layout instanceof Doc && source.layout.isTemplateDoc ? source.layout : undefined; - } - export function setChildDetailedLayout(target: Doc, source?: Doc) { - target.childDetailed = source && source.isTemplateDoc ? source : source && - source.dragFactory instanceof Doc && source.dragFactory.isTemplateDoc ? source.dragFactory : - source && source.layout instanceof Doc && source.layout.isTemplateDoc ? source.layout : undefined; + export function getDocTemplate(doc?: Doc) { + return doc?.isTemplateDoc ? doc : + Cast(doc?.dragFactory, Doc, null)?.isTemplateDoc ? doc?.dragFactory : + Cast(doc?.layout, Doc, null)?.isTemplateDoc ? doc?.layout : undefined; } export function matchFieldValue(doc: Doc, key: string, value: any): boolean { @@ -904,8 +899,7 @@ export namespace Doc { Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; }); Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); }); -Scripting.addGlobal(function setChildLayout(target: any, source: any) { Doc.setChildLayout(target, source); }); -Scripting.addGlobal(function setChildDetailedLayout(target: any, source: any) { Doc.setChildDetailedLayout(target, source); }); +Scripting.addGlobal(function getDocTemplate(doc?: any) { Doc.getDocTemplate(doc); }); Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); }); Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); }); Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); }); @@ -917,6 +911,7 @@ Scripting.addGlobal(function undo() { return UndoManager.Undo(); }); Scripting.addGlobal(function redo() { return UndoManager.Redo(); }); Scripting.addGlobal(function DOC(id: string) { console.log("Can't parse a document id in a script"); return "invalid"; }); Scripting.addGlobal(function assignDoc(doc: Doc, field: string, id: string) { return Doc.assignDocToField(doc, field, id); }); +Scripting.addGlobal(function docCast(doc: FieldResult): any { return DocCastAsync(doc); }); Scripting.addGlobal(function curPresentationItem() { const curPres = Doc.UserDoc().curPresentation as Doc; return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)]; diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts index 148886848..4a3119aeb 100644 --- a/src/new_fields/ScriptField.ts +++ b/src/new_fields/ScriptField.ts @@ -131,7 +131,8 @@ export class ScriptField extends ObjectField { export class ComputedField extends ScriptField { _lastComputedResult: any; //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc - value = computedFn((doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult }, console.log).result); + value = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); + _valueOutsideReaction = (doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult }, console.log).result; public static MakeScript(script: string, params: object = {}) { const compiled = ScriptField.CompileScript(script, params, false); return compiled.compiled ? new ComputedField(compiled) : undefined; @@ -166,7 +167,7 @@ export namespace ComputedField { export function initPlugin() { Plugins.addGetterPlugin((doc, _, value) => { if (useComputed && value instanceof ComputedField) { - return { value: value.value(doc), shouldReturn: true }; + return { value: value._valueOutsideReaction(doc), shouldReturn: true }; } }); } diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts index 91ea32bee..03519cb94 100644 --- a/src/new_fields/documentSchemas.ts +++ b/src/new_fields/documentSchemas.ts @@ -28,7 +28,8 @@ export const documentSchema = createSchema({ _pivotField: "string", // specifies which field should be used as the timeline/pivot axis _replacedChrome: "string", // what the default chrome is replaced with. Currently only supports the value of 'replaced' for PresBox's. _chromeStatus: "string", // determines the state of the collection chrome. values allowed are 'replaced', 'enabled', 'disabled', 'collapsed' - _freezeOnDrop: "boolean", // whether a document without native dimensions should have its width/height frozen as native dimensions on drop. Used by Timeline view to make sure documents are scaled to fit the display thumbnail + _freezeChildDimensions: "boolean", // freezes child document dimensions (e.g., used by time/pivot view to make sure all children will be scaled to fit their display rectangle) + isInPlaceContainer: "boolean",// whether the marked object will display addDocTab() calls that target "inPlace" destinations color: "string", // foreground color of document backgroundColor: "string", // background color of document opacity: "number", // opacity of document @@ -42,17 +43,15 @@ export const documentSchema = createSchema({ removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped isTemplateForField: "string",// when specifies a field key, then the containing document is a template that renders the specified field isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee) - dontSelect: "boolean", // whether document should be selected when clicked (usually set to false for buttons) treeViewOpen: "boolean", // flag denoting whether the documents sub-tree (contents) is visible or hidden treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree treeViewPreventOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) currentTimecode: "number", // current play back time of a temporal document (video / audio) - maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab) + followLinkLocation: "string",// flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab, ) lockedPosition: "boolean", // whether the document can be moved (dragged) lockedTransform: "boolean", // whether the document can be panned/zoomed inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently borderRounding: "string", // border radius rounding of document - searchFields: "string", // the search fields to display when this document matches a search in its metadata heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc) isButton: "boolean", // whether document functions as a button (overiding native interactions of its content) ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events) diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts index 1ed98cdbe..5cbba13de 100644 --- a/src/server/DashSession/DashSessionAgent.ts +++ b/src/server/DashSession/DashSessionAgent.ts @@ -226,4 +226,4 @@ export namespace DashSessionAgent { export const notificationRecipient = "brownptcdash@gmail.com"; -}
\ No newline at end of file +} diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 4f82da44d..529f8d56d 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -14,7 +14,7 @@ import { Utils } from "../../../Utils"; import { nullAudio, ImageField } from "../../../new_fields/URLField"; import { DragManager } from "../../../client/util/DragManager"; import { InkingControl } from "../../../client/views/InkingControl"; -import { Scripting } from "../../../client/util/Scripting"; +import { Scripting, CompileScript } from "../../../client/util/Scripting"; import { CollectionViewType } from "../../../client/views/collections/CollectionView"; import { makeTemplate } from "../../../client/util/DropConverter"; import { RichTextField } from "../../../new_fields/RichTextField"; @@ -89,16 +89,23 @@ export class CurrentUserUtils { { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc }, { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc }, + { title: "query", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.QueryDocument({ _width: 200, title: "an image of a cat" })' }, + + ]; return docProtoData.filter(d => !alreadyCreatedButtons?.includes(d.title)).map(data => Docs.Create.FontIconDocument({ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, icon: data.icon, title: data.title, + isButton: true, ignoreClick: data.ignoreClick, dropAction: data.click ? "copy" : undefined, - onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, - ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen, dontSelect: true, - backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory, + onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, + onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, + ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, + activePen: data.activePen, + backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), + dragFactory: data.dragFactory, })); } @@ -163,9 +170,12 @@ export class CurrentUserUtils { static setupThumbDoc(userDoc: Doc) { if (!userDoc.thumbDoc) { const thumbDoc = Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), { - _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5, linearViewIsExpanded: true, backgroundColor: "white" + _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", + _autoHeight: true, _yMargin: 5, linearViewIsExpanded: true, backgroundColor: "white" + }); + thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], { + _width: 300, _height: 25, _autoHeight: true, _chromeStatus: "disabled", linearViewIsExpanded: true, flexDirection: "column" }); - thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], { _width: 300, _height: 25, _autoHeight: true, _chromeStatus: "disabled", linearViewIsExpanded: true, flexDirection: "column" }); userDoc.thumbDoc = thumbDoc; } return Cast(userDoc.thumbDoc, Doc); @@ -207,11 +217,12 @@ export class CurrentUserUtils { }); return Docs.Create.ButtonDocument({ - _width: 35, _height: 25, title: "Tools", fontSize: 10, targetContainer: sidebarContainer, dontSelect: true, + _width: 35, _height: 25, title: "Tools", fontSize: 10, targetContainer: sidebarContainer, letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", sourcePanel: Docs.Create.StackingDocument([dragCreators, color], { - _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack" + _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack", forceActive: true }), + isButton: true, onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"), }); } @@ -233,11 +244,12 @@ export class CurrentUserUtils { }); return Docs.Create.ButtonDocument({ - _width: 50, _height: 25, title: "Library", fontSize: 10, dontSelect: true, + _width: 50, _height: 25, title: "Library", fontSize: 10, letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, Docs.Prototypes.MainLinkDocument(), doc, doc.recentlyClosed as Doc], { title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "place", lockedPosition: true, boxShadow: "0 0", dontRegisterChildren: true }), + isButton: true, targetContainer: sidebarContainer, onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;") }); @@ -246,13 +258,12 @@ export class CurrentUserUtils { // setup the Search button which will display the search panel. static setupSearchPanel(sidebarContainer: Doc) { return Docs.Create.ButtonDocument({ - _width: 50, _height: 25, title: "Search", fontSize: 10, dontSelect: true, + _width: 50, _height: 25, title: "Search", fontSize: 10, letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", - sourcePanel: Docs.Create.QueryDocument({ - title: "search stack", ignoreClick: true - }), + sourcePanel: Docs.Create.QueryDocument({ title: "search stack", }), targetContainer: sidebarContainer, lockedPosition: true, + isButton: true, onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel") }); } @@ -277,21 +288,31 @@ export class CurrentUserUtils { /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window static setupExpandingButtons(doc: Doc) { + const queryTemplate = Docs.Create.MulticolumnDocument( + [ + Docs.Create.QueryDocument({ title: "query", _height: 200, forceActive: true }), + Docs.Create.FreeformDocument([], { title: "data", _height: 100, _LODdisable: true, forceActive: true }) + ], + { _width: 400, _height: 300, title: "queryView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, _autoHeight: false, forceActive: true, hideFilterView: true }); + queryTemplate.isTemplateDoc = makeTemplate(queryTemplate); const slideTemplate = Docs.Create.MultirowDocument( [ - Docs.Create.MulticolumnDocument([], { title: "data", _height: 200 }), - Docs.Create.TextDocument("", { title: "text", _height: 100 }) + Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, forceActive: true }), + Docs.Create.TextDocument("", { title: "text", _height: 100, forceActive: true }) ], - { _width: 400, _height: 300, title: "slideView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, _autoHeight: false }); + { _width: 400, _height: 300, title: "slideView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, _autoHeight: false, forceActive: true, hideFilterView: true }); slideTemplate.isTemplateDoc = makeTemplate(slideTemplate); const descriptionTemplate = Docs.Create.TextDocument("", { title: "text", _height: 100, _showTitle: "title" }); Doc.GetProto(descriptionTemplate).layout = FormattedTextBox.LayoutString("description"); descriptionTemplate.isTemplateDoc = makeTemplate(descriptionTemplate, true, "descriptionView"); - const ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ ...opts, dontSelect: true, dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100 })) as any as Doc; + const ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ + ...opts, isButton: true, + dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100 + })) as any as Doc; const blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, { ...opts, - _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", dontSelect: true, forceActive: true, + _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", isButton: true, forceActive: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), backgroundColor: "black", treeViewPreventOpen: true, lockedPosition: true, _chromeStatus: "disabled", linearViewIsExpanded: true })) as any as Doc; @@ -300,8 +321,9 @@ export class CurrentUserUtils { doc.redoBtn = ficon({ onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" }); doc.slidesBtn = ficon({ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: slideTemplate, removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "sticky-note" }); doc.descriptionBtn = ficon({ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: descriptionTemplate, removeDropProperties: new List<string>(["dropAction"]), title: "description view", icon: "sticky-note" }); - doc.templateButtons = blist({ title: "template buttons" }, [doc.slidesBtn as Doc, doc.descriptionBtn as Doc]); - doc.expandingButtons = blist({ title: "expanding buttons" }, [doc.undoBtn as Doc, doc.redoBtn as Doc, doc.templateButtons as Doc]); + doc.queryBtn = ficon({ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: queryTemplate, removeDropProperties: new List<string>(["dropAction"]), title: "query view", icon: "sticky-note" }); + doc.templateButtons = blist({ title: "template buttons", ignoreClick: true }, [doc.slidesBtn as Doc, doc.descriptionBtn as Doc, doc.queryBtn as Doc]); + doc.expandingButtons = blist({ title: "expanding buttons", ignoreClick: true }, [doc.undoBtn as Doc, doc.redoBtn as Doc, doc.templateButtons as Doc]); doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([doc.noteTypes as Doc, doc.templateButtons as Doc], { title: "template layouts", _xPadding: 0, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }) @@ -323,6 +345,13 @@ export class CurrentUserUtils { doc.optionalRightCollection = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "New mobile uploads" })); } + static setupChildClicks(doc: Doc) { + const openInTarget = Docs.Create.TextDocument("", { title: "On Child Clicked (open in target)" }); + const text = "docCast(thisContainer.target).then((target) => { target && docCast(this.source).then((source) => { target.proto.data = new List([source || this]); } ); } )"; + openInTarget.script = ScriptField.MakeScript(text, { thisContainer: Doc.name }); + doc.childClickFuncs = Docs.Create.TreeDocument([openInTarget], { title: "on Child Click function templates" }); + } + static updateUserDocument(doc: Doc) { doc.title = Doc.CurrentUserEmail; new InkingControl(); @@ -333,7 +362,10 @@ export class CurrentUserUtils { (doc.expandingButtons === undefined) && CurrentUserUtils.setupExpandingButtons(doc); (doc.curPresentation === undefined) && CurrentUserUtils.setupDefaultPresentation(doc); (doc.sidebarButtons === undefined) && CurrentUserUtils.setupSidebarButtons(doc); + (doc.childClickFuncs === undefined) && CurrentUserUtils.setupChildClicks(doc); + // this is equivalent to using PrefetchProxies to make sure all the childClickFuncs have been retrieved. + PromiseValue(Cast(doc.childClickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast)); // this is equivalent to using PrefetchProxies to make sure the recentlyClosed doc is ready PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => recent && PromiseValue(recent.data).then(DocListCast)); // this is equivalent to using PrefetchProxies to make sure all the sidebarButtons and noteType internal Doc's have been retrieved. |