diff options
Diffstat (limited to 'src')
87 files changed, 2171 insertions, 1165 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index c7dfb0b23..2a7a7c59a 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -31,16 +31,19 @@ export namespace DocServer { export enum WriteMode { Default = 0, //Anything goes - Playground = 1, - LiveReadonly = 2, - LivePlayground = 3, + Playground = 1, //Playground (write own/no read) + LiveReadonly = 2,//Live Readonly (no write/read others) + LivePlayground = 3,//Live Playground (write own/read others) } - - export let AclsMode = WriteMode.Default; - const fieldWriteModes: { [field: string]: WriteMode } = {}; const docsWithUpdates: { [field: string]: Set<Doc> } = {}; + export var PlaygroundFields: string[]; + export function setPlaygroundFields(livePlayougroundFields: string[]) { + DocServer.PlaygroundFields = livePlayougroundFields; + livePlayougroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground)); + } + export function setFieldWriteMode(field: string, writeMode: WriteMode) { fieldWriteModes[field] = writeMode; if (writeMode !== WriteMode.Playground) { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index b642e18ab..f71984ca6 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -13,17 +13,19 @@ import { Cast, NumCast, StrCast } from "../../fields/Types"; import { AudioField, ImageField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField"; import { MessageStore } from "../../server/Message"; import { OmitKeys, Utils } from "../../Utils"; +import { YoutubeBox } from "../apis/youtube/YoutubeBox"; import { DocServer } from "../DocServer"; +import { DocumentManager } from "../util/DocumentManager"; import { dropActionType } from "../util/DragManager"; +import { DirectoryImportBox } from "../util/Import & Export/DirectoryImportBox"; import { LinkManager } from "../util/LinkManager"; import { Scripting } from "../util/Scripting"; import { UndoManager } from "../util/UndoManager"; -import { DocumentType } from "./DocumentTypes"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; import { ContextMenu } from "../views/ContextMenu"; import { ContextMenuProps } from "../views/ContextMenuItem"; -import { ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from "../views/InkingStroke"; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from "../views/InkingStroke"; import { AudioBox } from "../views/nodes/AudioBox"; import { ColorBox } from "../views/nodes/ColorBox"; import { ComparisonBox } from "../views/nodes/ComparisonBox"; @@ -45,9 +47,7 @@ import { WebBox } from "../views/nodes/WebBox"; import { PresElementBox } from "../views/presentationview/PresElementBox"; import { RecommendationsBox } from "../views/RecommendationsBox"; import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; -import { YoutubeBox } from "../apis/youtube/YoutubeBox"; -import { DocumentManager } from "../util/DocumentManager"; -import { DirectoryImportBox } from "../util/Import & Export/DirectoryImportBox"; +import { DocumentType } from "./DocumentTypes"; const path = require('path'); export interface DocumentOptions { @@ -92,7 +92,7 @@ export interface DocumentOptions { label?: string; // short form of title for use as an icon label style?: string; page?: number; - scale?: number; + _viewScale?: number; isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents forceActive?: boolean; layout?: string | Doc; // default layout string for a document @@ -168,6 +168,7 @@ export interface DocumentOptions { treeViewOpen?: boolean; // whether this document is expanded in a tree view treeViewExpandedView?: string; // which field/thing is displayed when this item is opened in tree view treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked + treeViewTruncateTitleWidth?: number; limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents // [key: string]: Opt<Field>; pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown @@ -227,7 +228,7 @@ export namespace Docs { }], [DocumentType.COL, { layout: { view: CollectionView, dataField: defaultDataKey }, - options: { _panX: 0, _panY: 0, scale: 1 } // , _width: 500, _height: 500 } + options: { _panX: 0, _panY: 0, _viewScale: 1 } // , _width: 500, _height: 500 } }], [DocumentType.KVP, { layout: { view: KeyValueBox, dataField: defaultDataKey }, @@ -526,6 +527,8 @@ export namespace Docs { const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey); const viewDoc = Doc.MakeDelegate(dataDoc, delegId); + proto.links = ComputedField.MakeFunction("links(self)"); + viewDoc.author = Doc.CurrentUserEmail; viewDoc.type !== DocumentType.LINK && DocUtils.MakeLinkToActiveAudio(viewDoc); @@ -624,7 +627,7 @@ export namespace Docs { export function LinkDocument(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, options: DocumentOptions = {}, id?: string) { const doc = InstanceFromProto(Prototypes.get(DocumentType.LINK), undefined, { - isLinkButton: true, treeViewHideTitle: true, treeViewOpen: false, + isLinkButton: true, treeViewHideTitle: true, treeViewOpen: false, backgroundColor: "lightBlue", // lightBlue is default color for linking dot and link documents text comment area removeDropProperties: new List(["isBackground", "isLinkButton"]), ...options }, id); const linkDocProto = Doc.GetProto(doc); @@ -645,13 +648,17 @@ export namespace Docs { return doc; } - export function InkDocument(color: string, tool: string, strokeWidth: string, strokeBezier: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) { + export function InkDocument(color: string, tool: string, strokeWidth: string, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) { const I = new Doc(); I.type = DocumentType.INK; I.layout = InkingStroke.LayoutString("data"); I.color = color; I.strokeWidth = strokeWidth; I.strokeBezier = strokeBezier; + I.fillColor = fillColor; + I.arrowStart = arrowStart; + I.arrowEnd = arrowEnd; + I.dash = dash; I.tool = tool; I.title = "ink"; I.x = options.x; @@ -877,7 +884,7 @@ export namespace DocUtils { created = Docs.Create.AudioDocument((field).url.href, resolved); layout = AudioBox.LayoutString; } else if (field instanceof InkField) { - created = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), (field).inkData, resolved); + created = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), (field).inkData, resolved); layout = InkingStroke.LayoutString; } else if (field instanceof List && field[0] instanceof Doc) { created = Docs.Create.StackingDocument(DocListCast(field), resolved); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 37eaa5f5e..2b0b2c738 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -21,6 +21,7 @@ import { MainView } from "../views/MainView"; import { DocumentType } from "../documents/DocumentTypes"; import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView"; +import { LabelBox } from "../views/nodes/LabelBox"; export class CurrentUserUtils { private static curr_id: string; @@ -86,11 +87,11 @@ export class CurrentUserUtils { }); } - if (doc["template-button-link"] === undefined) { + if (doc["template-button-link"] === undefined) { // set _backgroundColor to transparent to prevent link dot from obscuring document it's attached to. const linkTemplate = Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header"); // text needs to be a space to allow templateText to be created Doc.GetProto(linkTemplate).layout = "<div>" + - " <FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`lightBlue`}' fieldKey={'header'}/>" + + " <FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`lightGray`}' fieldKey={'header'}/>" + " <FormattedTextBox {...props} position='absolute' top='{(this._headerHeight||75)*scale}px' height='calc({100/scale}% - {this._headerHeight||75}px)' fieldKey={'text'}/>" + "</div>"; linkTemplate.isTemplateDoc = makeTemplate(linkTemplate, true, "linkView"); @@ -305,16 +306,20 @@ export class CurrentUserUtils { // setup templates for different document types when they are iconified from Document Decorations static setupDefaultIconTemplates(doc: Doc) { if (doc["template-icon-view"] === undefined) { - const iconView = Docs.Create.TextDocument("", { - title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") + const iconView = Docs.Create.LabelDocument({ + title: "icon", textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("title"), _backgroundColor: "dimGray", + _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") }); - Doc.GetProto(iconView).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', ""); + // Docs.Create.TextDocument("", { + // title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") + // }); + // Doc.GetProto(iconView).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', ""); iconView.isTemplateDoc = makeTemplate(iconView); doc["template-icon-view"] = new PrefetchProxy(iconView); } if (doc["template-icon-view-rtf"] === undefined) { const iconRtfView = Docs.Create.LabelDocument({ - title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset", + title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("text"), _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") }); iconRtfView.isTemplateDoc = makeTemplate(iconRtfView, true, "icon_" + DocumentType.RTF); @@ -334,11 +339,11 @@ export class CurrentUserUtils { } if (doc["template-icons"] === undefined) { doc["template-icons"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, - doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc], { title: "icon templates", _height: 75 })); + doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc, doc["template-icon-view-pdf"] as Doc], { title: "icon templates", _height: 75 })); } else { const templateIconsDoc = Cast(doc["template-icons"], Doc, null); const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, - doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc]; + doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc, doc["template-icon-view-pdf"] as Doc]; DocListCastAsync(templateIconsDoc.data).then(async curIcons => { await Promise.all(curIcons!); requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype)); @@ -567,7 +572,7 @@ export class CurrentUserUtils { if (doc.myCatalog === undefined) { doc.myCatalog = new PrefetchProxy(Docs.Create.SchemaDocument([], [], { title: "CATALOG", _height: 1000, _fitWidth: true, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, - childDropAction: "alias", targetDropAction: "same", treeViewExpandedView: "layout", stayInCollection: true, + childDropAction: "alias", targetDropAction: "same", stayInCollection: true, })); } return doc.myCatalog as Doc; @@ -596,6 +601,7 @@ export class CurrentUserUtils { if (doc["tabs-button-library"] === undefined) { const libraryStack = new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], { title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 150, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" })) as any as Doc; doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({ @@ -748,11 +754,16 @@ export class CurrentUserUtils { } static async updateUserDocument(doc: Doc) { + doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode; doc.title = Doc.CurrentUserEmail; doc.activeInkPen = doc; doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)"); doc.activeInkWidth = StrCast(doc.activeInkWidth, "1"); - doc.activeInkBezier = StrCast(doc.activeInkBezier, ""); + doc.activeInkBezier = StrCast(doc.activeInkBezier, "0"); + doc.activeFillColor = StrCast(doc.activeFillColor, "none"); + doc.activeArrowStart = StrCast(doc.activeArrowStart, "none"); + doc.activeArrowEnd = StrCast(doc.activeArrowEnd, "none"); + doc.activeDash = StrCast(doc.activeDash, "0"); doc.fontSize = NumCast(doc.fontSize, 12); doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index d8a5657c3..28b1ca6cf 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -335,7 +335,7 @@ export namespace DictationManager { const prompt = "Press alt + r to start dictating here..."; const head = 3; const anchor = head + prompt.length; - const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`; + const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`; proto.data = new RichTextField(proseMirrorState); proto.backgroundColor = "#eeffff"; target.props.addDocTab(newBox, "onRight"); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 78c05f572..fb5d1717e 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -130,7 +130,7 @@ export class DocumentManager { willZoom: boolean, // whether to zoom doc to take up most of screen createViewFunc = DocumentManager.addRightSplit, // how to create a view of the doc if it doesn't exist docContext?: Doc, // context to load that should contain the target - linkId?: string, // link that's being followed + linkDoc?: Doc, // link that's being followed closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc finished?: () => void @@ -140,7 +140,7 @@ export class DocumentManager { const highlight = () => { const finalDocView = getFirstDocView(targetDoc); if (finalDocView) { - finalDocView.layoutDoc.scrollToLinkID = linkId; + finalDocView.layoutDoc.scrollToLinkID = linkDoc?.[Id]; Doc.linkFollowHighlight(finalDocView.props.Document); } }; @@ -170,7 +170,7 @@ export class DocumentManager { const targetDocContextView = getFirstDocView(targetDocContext); targetDocContext._scrollY = 0; // this will force PDFs to activate and load their annotations / allow scrolling if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first.. - targetDocContext.panTransformType = "Ease"; + targetDocContext._viewTransition = "transform 500ms"; targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom); // now find the target document within the context @@ -195,7 +195,7 @@ export class DocumentManager { 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, createViewFunc, undefined, linkId, true, undefined, finished), // pass true this time for closeContextIfNotFound + this.jumpToDocument(targetDoc, willZoom, createViewFunc, undefined, linkDoc, 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); } @@ -224,7 +224,7 @@ export class DocumentManager { containerDoc.currentTimecode = targetTimecode; 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); + DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "onRight"), finished), targetNavContext, linkDoc, undefined, doc, finished); } else { finished?.(); } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 21564c92e..0db3963b2 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -19,8 +19,6 @@ export function SetupDrag( docFunc: () => Doc | Promise<Doc> | undefined, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, - treeViewId?: string, - dontHideOnDrop?: boolean, dragStarted?: () => void ) { const onRowMove = async (e: PointerEvent) => { @@ -34,8 +32,6 @@ export function SetupDrag( const dragData = new DragManager.DocumentDragData([doc]); dragData.dropAction = dropAction; dragData.moveDocument = moveFunc; - dragData.treeViewId = treeViewId; - dragData.dontHideOnDrop = dontHideOnDrop; DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y); dragStarted?.(); } @@ -128,7 +124,7 @@ export namespace DragManager { draggedDocuments: Doc[]; droppedDocuments: Doc[]; dragDivName?: string; - treeViewId?: string; + treeViewDoc?: Doc; dontHideOnDrop?: boolean; offset: number[]; dropAction: dropActionType; @@ -215,9 +211,11 @@ export namespace DragManager { dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : docDragData.dropAction === "alias" ? Doc.MakeAlias(d) : docDragData.dropAction === "copy" ? Doc.MakeDelegate(d) : d); - docDragData.dropAction !== "same" && docDragData.droppedDocuments.forEach((drop: Doc, i: number) => - (dragData?.removeDropProperties || []).concat(Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), [])).map(prop => drop[prop] = undefined) - ); + docDragData.dropAction !== "same" && docDragData.droppedDocuments.forEach((drop: Doc, i: number) => { + const dragProps = Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []); + const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps)); + remProps.map(prop => drop[prop] = undefined); + }); batch.end(); } return e; @@ -328,7 +326,7 @@ export namespace DragManager { dragLabel.style.zIndex = "100001"; dragLabel.style.fontSize = "10"; dragLabel.style.position = "absolute"; - dragLabel.innerText = "press 'a' to embed on drop"; + // dragLabel.innerText = "press 'a' to embed on drop"; // bcz: need to move this to a status bar dragDiv.appendChild(dragLabel); DragManager.Root().appendChild(dragDiv); } @@ -351,7 +349,7 @@ export namespace DragManager { const dragElement = ele.parentNode === dragDiv ? ele : ele.cloneNode(true) as HTMLElement; const rect = ele.getBoundingClientRect(); const scaleX = rect.width / ele.offsetWidth, - scaleY = rect.height / ele.offsetHeight; + scaleY = ele.offsetHeight ? rect.height / ele.offsetHeight : scaleX; elesCont.left = Math.min(rect.left, elesCont.left); elesCont.top = Math.min(rect.top, elesCont.top); elesCont.right = Math.max(rect.right, elesCont.right); diff --git a/src/client/util/InteractionUtils.scss b/src/client/util/InteractionUtils.scss new file mode 100644 index 000000000..6707157d4 --- /dev/null +++ b/src/client/util/InteractionUtils.scss @@ -0,0 +1,4 @@ +.halo { + opacity: 0.2; + stroke: black; +}
\ No newline at end of file diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index df792c9c0..edeb461e0 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -1,6 +1,8 @@ import React = require("react"); import * as beziercurve from 'bezier-curve'; import * as fitCurve from 'fit-curve'; +import "./InteractionUtils.scss"; +import { Utils } from "../../Utils"; export namespace InteractionUtils { export const MOUSETYPE = "mouse"; @@ -89,15 +91,17 @@ export namespace InteractionUtils { return myTouches; } - export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number, strokeWidth: number, bezier: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean) { + export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, + color: string, width: number, strokeWidth: number, bezier: string, fill: string, arrowStart: string, arrowEnd: string, + dash: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean, nodefs: boolean) { let pts: { X: number; Y: number; }[] = []; if (shape) { //if any of the shape are true pts = makePolygon(shape, points); } else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) { //pointer is up (first and last points are the same) - points.pop(); const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]); + newPoints.pop(); const bezierCurves = fitCurve(newPoints, parseInt(bezier)); for (const curve of bezierCurves) { @@ -111,25 +115,55 @@ export namespace InteractionUtils { } const strpts = pts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${(pt.X - left - width / 2) * scalex + width / 2}, - ${(pt.Y - top - width / 2) * scaley + width / 2} `, ""); + ${(pt.Y - top - width / 2) * scaley + width / 2} `, ""); + const dashArray = String(Number(width) * Number(dash)); + const defGuid = Utils.GenerateGuid(); + const arrowDim = Math.max(0.5, 8 / Math.log(Math.max(2, strokeWidth))); + return (<svg fill={fill === "none" ? color : fill}> {/* setting the svg fill sets the arrowhead fill */} + {nodefs ? (null) : <defs> + {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : <marker id={`dot${defGuid}`} orient="auto" overflow="visible"> + <circle r={1} fill="context-stroke" /> + </marker>} + {arrowStart !== "arrowHead" && arrowEnd !== "arrowHead" ? (null) : <marker id={`arrowHead${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7"> + <polygon points={`${arrowDim} ${-Math.max(1, arrowDim / 2)}, ${arrowDim} ${Math.max(1, arrowDim / 2)}, -1 0`} /> + </marker>} + {arrowStart !== "arrowEnd" && arrowEnd !== "arrowEnd" ? (null) : <marker id={`arrowEnd${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7"> + <polygon points={`${2 - arrowDim} ${-Math.max(1, arrowDim / 2)}, ${2 - arrowDim} ${Math.max(1, arrowDim / 2)}, 3 0`} /> + </marker>} + </defs>} - return ( <polyline points={strpts} style={{ - filter: drawHalo ? "url(#dangerShine)" : undefined, - fill: "none", + filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, + fill, opacity: strokeWidth !== width ? 0.5 : undefined, pointerEvents: pevents as any, stroke: color ?? "rgb(0, 0, 0)", strokeWidth: strokeWidth, strokeLinejoin: "round", - strokeLinecap: "round" + strokeLinecap: "round", + strokeDasharray: dashArray }} + markerStart={`url(#${arrowStart + defGuid})`} + markerEnd={`url(#${arrowEnd + defGuid})`} /> - ); + + </svg>); } + // export function makeArrow() { + // return ( + // InkOptionsMenu.Instance.getColors().map(color => { + // const id1 = "arrowHeadTest" + color; + // console.log(color); + // <marker id={id1} orient="auto" overflow="visible" refX="0" refY="1" markerWidth="10" markerHeight="7"> + // <polygon points="0 0, 3 1, 0 2" fill={"#" + color} /> + // </marker>; + // }) + // ); + // } + export function makePolygon(shape: string, points: { X: number, Y: number }[]) { if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) { //pointer is up (first and last points are the same) @@ -199,24 +233,24 @@ export namespace InteractionUtils { } points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top }); return points; - case "arrow": - const x1 = left; - const y1 = top; - const x2 = right; - const y2 = bottom; - const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + (Math.pow(Math.abs(y1 - y2), 2))); - const L2 = L1 / 5; - const angle = 0.785398; - const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle)); - const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle)); - const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle)); - const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle)); - points.push({ X: x1, Y: y1 }); - points.push({ X: x2, Y: y2 }); - points.push({ X: x3, Y: y3 }); - points.push({ X: x4, Y: y4 }); - points.push({ X: x2, Y: y2 }); - return points; + // case "arrow": + // const x1 = left; + // const y1 = top; + // const x2 = right; + // const y2 = bottom; + // const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + (Math.pow(Math.abs(y1 - y2), 2))); + // const L2 = L1 / 5; + // const angle = 0.785398; + // const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle)); + // const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle)); + // const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle)); + // const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle)); + // points.push({ X: x1, Y: y1 }); + // points.push({ X: x2, Y: y2 }); + // points.push({ X: x3, Y: y3 }); + // points.push({ X: x4, Y: y4 }); + // points.push({ X: x2, Y: y2 }); + // return points; case "line": points.push({ X: left, Y: top }); points.push({ X: right, Y: bottom }); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 47b2541bd..0aec81ab0 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -70,6 +70,9 @@ export class LinkManager { const protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null)); return protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor); }); + DocListCast(anchor[Doc.LayoutFieldKey(anchor) + "-annotations"]).map(anno => { + related.push(...LinkManager.Instance.getAllRelatedLinks(anno)); + }); return related; } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 05515e502..024532f90 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -3,6 +3,8 @@ import { Doc } from "../../fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { computedFn } from "mobx-utils"; import { List } from "../../fields/List"; +import { Scripting } from "./Scripting"; +import { DocumentManager } from "./DocumentManager"; export namespace SelectionManager { @@ -10,7 +12,6 @@ export namespace SelectionManager { @observable IsDragging: boolean = false; SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap(); - @action SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it @@ -82,3 +83,9 @@ export namespace SelectionManager { } } + +Scripting.addGlobal(function selectDoc(doc: any) { + const view = DocumentManager.Instance.getDocumentView(doc); + view && SelectionManager.SelectDoc(view, false); + //Doc.UserDoc().activeSelection = new List([doc]); +});
\ No newline at end of file diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss index d4a76ee17..e56574bb7 100644 --- a/src/client/views/AntimodeMenu.scss +++ b/src/client/views/AntimodeMenu.scss @@ -1,7 +1,10 @@ +@import "./globalCssVariables"; + + .antimodeMenu-cont { position: absolute; z-index: 10000; - height: 35px; + height: $antimodemenu-height; background: #323232; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); border-radius: 0px 6px 6px 6px; diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index 9a6121d20..3e4d20fea 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -131,15 +131,15 @@ export default abstract class AntimodeMenu extends React.Component { } protected getDragger = () => { - return <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />; + return <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} />; } protected getElement(buttons: JSX.Element[]) { return ( <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} style={{ left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay }}> + <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} /> {buttons} - <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> </div> ); } @@ -147,9 +147,13 @@ export default abstract class AntimodeMenu extends React.Component { protected getElementWithRows(rows: JSX.Element[], numRows: number, hasDragger: boolean = true) { return ( <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} - style={{ left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, height: "auto" }}> + style={{ + left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, + transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, height: "auto", + flexDirection: this.Pinned ? "row" : undefined, position: this.Pinned ? "unset" : undefined + }}> + {hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} /> : (null)} {rows} - {hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> : <></>} </div> ); } diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 5b66b63ed..941d7b44a 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -236,7 +236,7 @@ export class ContextMenu extends React.Component { <span className="icon-background"> <FontAwesomeIcon icon="search" size="lg" /> </span> - <input className="contextMenu-item contextMenu-description search" type="text" placeholder="Search . . ." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> + <input className="contextMenu-item contextMenu-description search" type="text" placeholder="Search Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> </span> {this.menuItems} </> diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 62a95116f..c05ca33fb 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -1,28 +1,28 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowAltCircleDown, faPhotoVideo, faArrowAltCircleUp, faArrowAltCircleRight, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { faArrowAltCircleDown, faArrowAltCircleRight, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faPhotoVideo, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../fields/Doc"; import { RichTextField } from '../../fields/RichTextField'; -import { NumCast, StrCast, Cast } from "../../fields/Types"; +import { Cast, NumCast } from "../../fields/Types"; import { emptyFunction, setupMoveUpEvents } from "../../Utils"; +import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; +import { Docs, DocUtils } from '../documents/Documents'; +import { DragManager } from '../util/DragManager'; import { UndoManager } from "../util/UndoManager"; import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView'; import { ParentDocSelector } from './collections/ParentDocumentSelector'; import './collections/ParentDocumentSelector.scss'; import './DocumentButtonBar.scss'; -import { LinkMenu } from "./linking/LinkMenu"; +import { MetadataEntryMenu } from './MetadataEntryMenu'; import { DocumentView } from './nodes/DocumentView'; import { GoogleRef } from "./nodes/formattedText/FormattedTextBox"; import { TemplateMenu } from "./TemplateMenu"; import { Template, Templates } from "./Templates"; import React = require("react"); -import { DragManager } from '../util/DragManager'; -import { MetadataEntryMenu } from './MetadataEntryMenu'; -import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; -import { Docs } from '../documents/Documents'; +import { DocumentLinksButton } from './nodes/DocumentLinksButton'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -113,39 +113,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get view0() { return this.props.views()?.[0]; } - @action - onLinkButtonMoved = (e: PointerEvent) => { - if (this._linkButton.current !== null) { - const linkDrag = UndoManager.StartBatch("Drag Link"); - this.view0 && DragManager.StartLinkDrag(this._linkButton.current, this.view0.props.Document, e.pageX, e.pageY, { - dragComplete: dropEv => { - const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop - if (this.view0 && linkDoc) { - !linkDoc.linkRelationship && (Doc.GetProto(linkDoc).linkRelationship = "hyperlink"); - - // we want to allow specific views to handle the link creation in their own way (e.g., rich text makes text hyperlinks) - // the dragged view can regiser a linkDropCallback to be notified that the link was made and to update their data structures - // however, the dropped document isn't so accessible. What we do is set the newly created link document on the documentView - // The documentView passes a function prop returning this link doc to its descendants who can react to changes to it. - dropEv.linkDragData?.linkDropCallback?.(dropEv.linkDragData); - runInAction(() => this.view0!._link = linkDoc); - setTimeout(action(() => this.view0!._link = undefined), 0); - } - linkDrag?.end(); - }, - hideSource: false - }); - return true; - } - return false; - } - - - onLinkButtonDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, emptyFunction); - } - - @computed get considerGoogleDocsPush() { const targetDoc = this.view0?.props.Document; @@ -236,20 +203,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV } @computed - get linkButton() { - const view0 = this.view0; - const linkCount = view0 && DocListCast(view0.props.Document.links).length; - return !view0 ? (null) : <div title="Drag(create link) Tap(view links)" className="documentButtonBar-linkFlyout" ref={this._linkButton}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} - content={<LinkMenu docView={view0} addDocTab={view0.props.addDocTab} changeFlyout={emptyFunction} />}> - <div className={"documentButtonBar-linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} > - {linkCount ? linkCount : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />} - </div> - </Flyout> - </div>; - } - - @computed get metadataButton() { const view0 = this.view0; return !view0 ? (null) : <div title="Show metadata panel" className="documentButtonBar-linkFlyout"> @@ -318,7 +271,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const considerPush = isText && this.considerGoogleDocsPush; return <div className="documentButtonBar"> <div className="documentButtonBar-button"> - {this.linkButton} + <DocumentLinksButton View={this.view0} AlwaysOn={true} /> </div> <div className="documentButtonBar-button"> {this.templateButton} diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 29204569b..5948ada88 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -22,11 +22,20 @@ $linkGap : 3px; } + .documentDecorations-rotation { + pointer-events: auto; + // cursor: grabbing; + cursor: ns-resize; + width: 10px; + height: 10px; + } + .documentDecorations-resizer { pointer-events: auto; background: $alt-accent; opacity: 0.1; } + .documentDecorations-resizer:hover { opacity: 1; } @@ -87,14 +96,17 @@ $linkGap : 3px; background: unset; opacity: 1; } + #documentDecorations-topLeftResizer { - border-left: 2px solid; - border-top: solid 2px; + border-left: 2px solid; + border-top: solid 2px; } + #documentDecorations-bottomRightResizer { - border-right: 2px solid; - border-bottom: solid 2px; + border-right: 2px solid; + border-bottom: solid 2px; } + #documentDecorations-topLeftResizer:hover, #documentDecorations-bottomRightResizer:hover { opacity: 1; @@ -110,14 +122,17 @@ $linkGap : 3px; background: unset; opacity: 1; } + #documentDecorations-topRightResizer { - border-right: 2px solid; - border-top: 2px solid; + border-right: 2px solid; + border-top: 2px solid; } + #documentDecorations-bottomLeftResizer { - border-left: 2px solid; - border-bottom: 2px solid; + border-left: 2px solid; + border-bottom: 2px solid; } + #documentDecorations-topRightResizer:hover, #documentDecorations-bottomLeftResizer:hover { cursor: nesw-resize; @@ -139,10 +154,11 @@ $linkGap : 3px; width: 25px; height: calc(100% + 8px); // 8px for the height of the top resizer bar grid-column-start: 2; - grid-column-end : 2; + grid-column-end: 2; pointer-events: all; padding-left: 5px; } + .documentDecorations-title { opacity: 1; grid-column-start: 3; @@ -150,14 +166,16 @@ $linkGap : 3px; pointer-events: auto; overflow: hidden; text-align: center; - display: flex; + display: flex; border-bottom: solid 1px; - margin-left:10px; + margin-left: 10px; width: calc(100% - 10px); } + .focus-visible { - margin-left:0px; + margin-left: 0px; } + .publishBox { width: 20px; height: 22px; @@ -172,13 +190,30 @@ $linkGap : 3px; } +.documentDecorations-iconifyButton { + opacity: 1; + grid-column-start: 4; + grid-column-end: 5; + pointer-events: all; + text-align: center; + left: -25px; + top: -2px; + cursor: pointer; + position: absolute; + background: transparent; + width: 20px; +} + .documentDecorations-closeButton { opacity: 1; grid-column-start: 4; - grid-column-end: 6; + grid-column-end: 5; pointer-events: all; text-align: center; cursor: pointer; + width: 15px; + margin-left: -8px; + margin-top: auto; } .documentDecorations-minimizeButton { @@ -194,8 +229,9 @@ $linkGap : 3px; width: 8px; height: $MINIMIZED_ICON_SIZE; max-height: 20px; - > svg { - margin:0; + + >svg { + margin: 0; } } @@ -334,7 +370,8 @@ $linkGap : 3px; padding: 2px 12px; list-style: none; - .templateToggle, .chromeToggle { + .templateToggle, + .chromeToggle { text-align: left; } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 0d4667135..4fda10926 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -17,11 +17,14 @@ import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; import { DocumentView } from "./nodes/DocumentView"; import React = require("react"); -import { Id } from '../../fields/FieldSymbols'; +import { Id, Copy, Update } from '../../fields/FieldSymbols'; import e = require('express'); import { CollectionDockingView } from './collections/CollectionDockingView'; import { SnappingManager } from '../util/SnappingManager'; import { HtmlField } from '../../fields/HtmlField'; +import { InkData, InkField, InkTool } from "../../fields/InkField"; +import { update } from 'serializr'; +import { Transform } from "../util/Transform"; library.add(faCaretUp); library.add(faObjectGroup); @@ -55,6 +58,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> private _resizeUndo?: UndoManager.Batch; private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border private _snapX = 0; _snapY = 0; // last snapped location of resize border + private _prevX = 0; + private _prevY = 0; + private _centerPoints: { X: number, Y: number }[] = []; + @observable private _accumulatedTitle = ""; @observable private _titleControlString: string = "#title"; @observable private _edtingTitle = false; @@ -75,7 +82,7 @@ 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.treeViewId || + documentView.props.treeViewDoc || Doc.AreProtosEqual(documentView.props.Document, Doc.UserDoc())) { return bounds; } @@ -180,7 +187,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } @undoBatch @action - onCloseClick = async (e: PointerEvent | undefined) => { + onCloseClick = async (e: React.MouseEvent | undefined) => { if (!e?.button) { const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; const selected = SelectionManager.SelectedDocuments().slice(); @@ -243,6 +250,82 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return false; } + @action + onRotateDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, this.onRotateMove, this.onRotateUp, (e) => { }); + this._prevX = e.clientX; + this._prevY = e.clientY; + SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { + const doc = Document(element.rootDoc); + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + const xs = ink.map(p => p.X); + const ys = ink.map(p => p.Y); + const left = Math.min(...xs); + const top = Math.min(...ys); + const right = Math.max(...xs); + const bottom = Math.max(...ys); + // this._centerPoints.push({ X: ((right - left) / 2) + left, Y: ((bottom - top) / 2) + bottom }); + this._centerPoints.push({ X: left, Y: top }); + } + } + })); + + } + @action + onRotateMove = (e: PointerEvent, down: number[]): boolean => { + + // const distance = Math.sqrt((this._prevY - e.clientY) * (this._prevY - e.clientY) + (this._prevX - e.clientX) * (this._prevX - e.clientX)); + const distance = Math.abs(this._prevY - e.clientY); + var angle = 0; + //think of a better condition later... + // if ((down[0] < e.clientX && this._prevY < e.clientY) || (down[0] > e.clientX && this._prevY > e.clientY)) { + if (e.clientY > this._prevY) { + angle = distance * (Math.PI / 180); + // } else if ((down[0] < e.clientX && this._prevY > e.clientY) || (down[0] > e.clientX && this._prevY <= e.clientY)) { + } else if (e.clientY < this._prevY) { + angle = - distance * (Math.PI / 180); + } + this._prevX = e.clientX; + this._prevY = e.clientY; + var index = 0; + SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { + const doc = Document(element.rootDoc); + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + + const newPoints: { X: number, Y: number }[] = []; + for (var i = 0; i < ink.length; i++) { + const newX = Math.cos(angle) * (ink[i].X - this._centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - this._centerPoints[index].Y) + this._centerPoints[index].X; + const newY = Math.sin(angle) * (ink[i].X - this._centerPoints[index].X) + Math.cos(angle) * (ink[i].Y - this._centerPoints[index].Y) + this._centerPoints[index].Y; + newPoints.push({ X: newX, Y: newY }); + } + doc.data = new InkField(newPoints); + const xs = newPoints.map(p => p.X); + const ys = newPoints.map(p => p.Y); + const left = Math.min(...xs); + const top = Math.min(...ys); + const right = Math.max(...xs); + const bottom = Math.max(...ys); + + doc._height = (bottom - top) * element.props.ScreenToLocalTransform().Scale; + doc._width = (right - left) * element.props.ScreenToLocalTransform().Scale; + + } + index++; + } + })); + return false; + } + + onRotateUp = (e: PointerEvent) => { + this._centerPoints = []; + } + + + _initialAutoHeight = false; _dragHeights = new Map<Doc, number>(); @action @@ -465,7 +548,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}> <FontAwesomeIcon size="lg" icon="cog" /> </div>) : ( - <div className="documentDecorations-minimizeButton" title="Iconify" onPointerDown={this.onIconifyDown}> + <div className="documentDecorations-minimizeButton" title="Iconify" onClick={this.onCloseClick}> {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> </div>); @@ -474,16 +557,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <> <input ref={this._keyinput} className="documentDecorations-title" type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle} onBlur={e => this.titleBlur(true)} onChange={action(e => this._accumulatedTitle = e.target.value)} onKeyPress={this.titleEntered} /> - {minimal ? (null) : <div className="publishBox" title="make document referenceable by its title" - onPointerDown={action(e => { - if (!seldoc.props.Document.customTitle) { - seldoc.props.Document.customTitle = true; - StrCast(Doc.GetProto(seldoc.props.Document).title).startsWith("-") && (Doc.GetProto(seldoc.props.Document).title = StrCast(seldoc.props.Document.title).substring(1)); - this._accumulatedTitle = StrCast(seldoc.props.Document.title); - } - DocUtils.Publish(seldoc.props.Document, this._accumulatedTitle, seldoc.props.addDocument, seldoc.props.removeDocument); - })}> - <FontAwesomeIcon size="lg" color={SelectionManager.SelectedDocuments()[0].props.Document.title === SelectionManager.SelectedDocuments()[0].props.Document[Id] ? "green" : undefined} icon="sticky-note"></FontAwesomeIcon> + {minimal ? (null) : <div className="publishBox" // title="make document referenceable by its title" + // onPointerDown={action(e => { + // if (!seldoc.props.Document.customTitle) { + // seldoc.props.Document.customTitle = true; + // StrCast(Doc.GetProto(seldoc.props.Document).title).startsWith("-") && (Doc.GetProto(seldoc.props.Document).title = StrCast(seldoc.props.Document.title).substring(1)); + // this._accumulatedTitle = StrCast(seldoc.props.Document.title); + // } + // DocUtils.Publish(seldoc.props.Document, this._accumulatedTitle, seldoc.props.addDocument, seldoc.props.removeDocument); + // })} + > + {/* <FontAwesomeIcon size="lg" color={SelectionManager.SelectedDocuments()[0].props.Document.title === SelectionManager.SelectedDocuments()[0].props.Document[Id] ? "green" : undefined} icon="sticky-note"></FontAwesomeIcon> */} </div>} </> : <> @@ -524,9 +608,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> }}> {maximizeIcon} {titleArea} + {SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) : + <div className="documentDecorations-iconifyButton" title={`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`} onPointerDown={this.onIconifyDown}> + {"_"} + </div>} <div className="documentDecorations-closeButton" title="Open Document in Tab" onPointerDown={this.onMaximizeDown}> {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."} </div> + <div id="documentDecorations-rotation" className="documentDecorations-rotation" + onPointerDown={this.onRotateDown}> ⟲ </div> <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-topResizer" className="documentDecorations-resizer" @@ -553,7 +643,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div> </div > - <div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2, top: bounds.b + this._resizeBorderWidth / 2 }}> + <div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2 + 10, top: bounds.b + this._resizeBorderWidth / 2 }}> <DocumentButtonBar views={SelectionManager.SelectedDocuments} /> </div> </div > diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index ee3ce1cf3..bab3a1634 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -31,6 +31,7 @@ export interface EditableProps { fontStyle?: string; fontSize?: number; height?: number | "auto"; + sizeToContent?: boolean; maxHeight?: number; display?: string; autosuggestProps?: { @@ -60,13 +61,11 @@ export interface EditableProps { */ @observer export class EditableView extends React.Component<EditableProps> { - public static loadId = ""; @observable _editing: boolean = false; constructor(props: EditableProps) { super(props); this._editing = this.props.editing ? true : false; - EditableView.loadId = ""; } // @action @@ -150,37 +149,44 @@ export class EditableView extends React.Component<EditableProps> { @action setIsFocused = (value: boolean) => { const wasFocused = this._editing; - this._editing = value; + //this._editing = value; return wasFocused !== this._editing; } _ref = React.createRef<HTMLDivElement>(); + renderEditor() { + return this.props.autosuggestProps + ? <Autosuggest + {...this.props.autosuggestProps.autosuggestProps} + inputProps={{ + className: "editableView-input", + onKeyDown: this.onKeyDown, + autoFocus: true, + onBlur: e => this.finalizeEdit(e.currentTarget.value, false, true), + onPointerDown: this.stopPropagation, + onClick: this.stopPropagation, + onPointerUp: this.stopPropagation, + value: this.props.autosuggestProps.value, + onChange: this.props.autosuggestProps.onChange + }} + /> + : <input className="editableView-input" + defaultValue={this.props.GetValue()} + onKeyDown={this.onKeyDown} + autoFocus={true} + onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true)} + onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} + style={{ display: this.props.display, fontSize: this.props.fontSize, minWidth: 20 }} + placeholder={this.props.placeholder} + />; + } render() { if (this._editing && this.props.GetValue() !== undefined) { - return this.props.autosuggestProps - ? <Autosuggest - {...this.props.autosuggestProps.autosuggestProps} - inputProps={{ - className: "editableView-input", - onKeyDown: this.onKeyDown, - autoFocus: true, - onBlur: e => this.finalizeEdit(e.currentTarget.value, false, true), - onPointerDown: this.stopPropagation, - onClick: this.stopPropagation, - onPointerUp: this.stopPropagation, - value: this.props.autosuggestProps.value, - onChange: this.props.autosuggestProps.onChange - }} - /> - : <input className="editableView-input" - defaultValue={this.props.GetValue()} - onKeyDown={this.onKeyDown} - autoFocus={true} - onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true)} - onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} - style={{ display: this.props.display, fontSize: this.props.fontSize }} - placeholder={this.props.placeholder} - />; + return this.props.sizeToContent ? + <div style={{ display: "grid", minWidth: 100 }}> + <div style={{ display: "inline-block", position: "relative", height: 0, width: "100%", overflow: "hidden" }}>{this.props.GetValue()}</div> + {this.renderEditor()} + </div> : this.renderEditor(); } else { this.props.autosuggestProps?.resetValue(); return (this.props.contents instanceof ObjectField ? (null) : diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index aeac1d4a9..85695bbac 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -18,12 +18,13 @@ import { LinkManager } from "../util/LinkManager"; import { Scripting } from "../util/Scripting"; import { Transform } from "../util/Transform"; import "./GestureOverlay.scss"; -import { ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke"; +import { ActiveInkBezierApprox, ActiveArrowStart, ActiveArrowEnd, ActiveFillColor, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth, SetActiveFillColor, SetActiveArrowStart, SetActiveArrowEnd, ActiveDash, SetActiveDash } from "./InkingStroke"; import { DocumentView } from "./nodes/DocumentView"; import { RadialMenu } from "./nodes/RadialMenu"; import HorizontalPalette from "./Palette"; import { Touchable } from "./Touchable"; import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu"; +import HeightLabel from "./collections/collectionMulticolumn/MultirowHeightLabel"; @observer export default class GestureOverlay extends Touchable { @@ -32,6 +33,10 @@ export default class GestureOverlay extends Touchable { @observable public InkShape: string = ""; @observable public SavedColor?: string; @observable public SavedWidth?: string; + @observable public SavedFill?: string; + @observable public SavedArrowStart: string = "none"; + @observable public SavedArrowEnd: string = "none"; + @observable public SavedDash: String = "0"; @observable public Tool: ToolglassTools = ToolglassTools.None; @observable private _thumbX?: number; @@ -627,7 +632,9 @@ export default class GestureOverlay extends Touchable { this.makePolygon(this.InkShape, false); this.dispatchGesture(GestureUtils.Gestures.Stroke); this._points = []; - this.InkShape = ""; + if (this.InkShape !== "noRec") { + this.InkShape = ""; + } } // if we're not drawing in a toolglass try to recognize as gesture else { @@ -677,6 +684,10 @@ export default class GestureOverlay extends Touchable { } else { this._points = []; } + SetActiveArrowStart("none"); + GestureOverlay.Instance.SavedArrowStart = ActiveArrowStart(); + SetActiveArrowEnd("none"); + GestureOverlay.Instance.SavedArrowEnd = ActiveArrowEnd(); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); } @@ -688,7 +699,9 @@ export default class GestureOverlay extends Touchable { var left = Math.min(...xs); var bottom = Math.max(...ys); var top = Math.min(...ys); - + if (shape === "noRec") { + return; + } if (!gesture) { //if shape options is activated in inkOptionMenu //take second to last point because _point[length-1] is _points[0] @@ -747,7 +760,7 @@ export default class GestureOverlay extends Touchable { case "line": this._points.push({ X: left, Y: top }); this._points.push({ X: right, Y: bottom }); - this._points.push({ X: right, Y: bottom - 1 }); + // this._points.push({ X: right, Y: bottom - 1 }); break; case "arrow": const x1 = left; @@ -766,7 +779,7 @@ export default class GestureOverlay extends Touchable { this._points.push({ X: x3, Y: y3 }); this._points.push({ X: x4, Y: y4 }); this._points.push({ X: x2, Y: y2 }); - this._points.push({ X: x1, Y: y1 - 1 }); + // this._points.push({ X: x1, Y: y1 - 1 }); } } @@ -802,19 +815,28 @@ export default class GestureOverlay extends Touchable { } @computed get elements() { - const B = this.svgBounds; const width = Number(ActiveInkWidth()); + const B = this.svgBounds; + B.left = B.left - width / 2; + B.right = B.right + width / 2; + B.top = B.top - width / 2; + B.bottom = B.bottom + width / 2; + B.width += width; + B.height += width; return [ this.props.children, this._palette, [this._strokes.map((l, i) => { const b = this.getBounds(l); return <svg key={i} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> - {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)} + {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), width, width, + ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), + ActiveDash(), 1, 1, this.InkShape, "none", false, false)} </svg>; }), - this._points.length <= 1 ? (null) : <svg key="svg" width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> - {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)} + this._points.length <= 1 ? (null) : <svg key="svg" width={B.width} height={B.height} + style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> + {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", false, false)} </svg>] ]; } @@ -903,12 +925,20 @@ Scripting.addGlobal("GestureOverlay", GestureOverlay); Scripting.addGlobal(function setToolglass(tool: any) { runInAction(() => GestureOverlay.Instance.Tool = tool); }); -Scripting.addGlobal(function setPen(width: any, color: any) { +Scripting.addGlobal(function setPen(width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) { runInAction(() => { GestureOverlay.Instance.SavedColor = ActiveInkColor(); SetActiveInkColor(color); GestureOverlay.Instance.SavedWidth = ActiveInkWidth(); SetActiveInkWidth(width); + GestureOverlay.Instance.SavedFill = ActiveFillColor(); + SetActiveFillColor(fill); + GestureOverlay.Instance.SavedArrowStart = ActiveArrowStart(); + SetActiveArrowStart(arrowStart); + GestureOverlay.Instance.SavedArrowEnd = ActiveArrowEnd(); + SetActiveArrowStart(arrowEnd); + GestureOverlay.Instance.SavedDash = ActiveDash(); + SetActiveDash(dash); }); }); Scripting.addGlobal(function resetPen() { diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index f5adeeb00..e3546dece 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -15,10 +15,11 @@ import { SelectionManager } from "../util/SelectionManager"; import SharingManager from "../util/SharingManager"; import { undoBatch, UndoManager } from "../util/UndoManager"; import { CollectionDockingView } from "./collections/CollectionDockingView"; -import { MarqueeView } from "./collections/collectionFreeForm/MarqueeView"; import { DocumentDecorations } from "./DocumentDecorations"; import { MainView } from "./MainView"; import { DocumentView } from "./nodes/DocumentView"; +import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; +import PDFMenu from "./pdf/PDFMenu"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -74,9 +75,10 @@ export default class KeyManager { case "a": DragManager.CanEmbed = true; break; case " ": - MarqueeView.DragMarquee = !MarqueeView.DragMarquee; + // MarqueeView.DragMarquee = !MarqueeView.DragMarquee; // bcz: this needs a better disclosure UI break; case "escape": + DocumentLinksButton.StartLink = undefined; const main = MainView.Instance; Doc.SetSelectedTool(InkTool.None); if (main.isPointerDown) { @@ -103,6 +105,7 @@ export default class KeyManager { } UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.removeDocument?.(dv.props.Document)), "delete"); + SelectionManager.DeselectAll(); break; case "arrowleft": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(-1, 0)), "nudge left"); @@ -252,8 +255,8 @@ export default class KeyManager { case "x": if (SelectionManager.SelectedDocuments().length) { const bds = DocumentDecorations.Instance.Bounds; - const pt = [bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2]; - const text = `__DashDocId(${pt[0]},${pt[1]}):` + SelectionManager.SelectedDocuments().map(dv => dv.Document[Id]).join(":"); + const pt = SelectionManager.SelectedDocuments()[0].props.ScreenToLocalTransform().transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2); + const text = `__DashDocId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.SelectedDocuments().map(dv => dv.Document[Id]).join(":"); SelectionManager.SelectedDocuments().length && navigator.clipboard.writeText(text); DocumentDecorations.Instance.onCloseClick(undefined); stopPropagation = false; @@ -261,10 +264,10 @@ export default class KeyManager { } break; case "c": - if (DocumentDecorations.Instance.Bounds.r - DocumentDecorations.Instance.Bounds.x > 2) { + if (!PDFMenu.Instance.Active && DocumentDecorations.Instance.Bounds.r - DocumentDecorations.Instance.Bounds.x > 2) { const bds = DocumentDecorations.Instance.Bounds; const pt = SelectionManager.SelectedDocuments()[0].props.ScreenToLocalTransform().transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2); - const text = `__DashDocId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.SelectedDocuments().map(dv => dv.Document[Id]).join(":"); + const text = `__DashCloneId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.SelectedDocuments().map(dv => dv.Document[Id]).join(":"); SelectionManager.SelectedDocuments().length && navigator.clipboard.writeText(text); stopPropagation = false; } @@ -279,10 +282,12 @@ export default class KeyManager { }); public paste(e: ClipboardEvent) { - if (e.clipboardData?.getData("text/plain") !== "" && e.clipboardData?.getData("text/plain").startsWith("__DashDocId(")) { + const plain = e.clipboardData?.getData("text/plain"); + const clone = plain?.startsWith("__DashCloneId("); + if (plain && (plain.startsWith("__DashDocId(") || clone)) { const first = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; if (first?.props.Document.type === DocumentType.COL) { - const docids = e.clipboardData.getData("text/plain").split(":"); + const docids = plain.split(":"); let count = 1; const list: Doc[] = []; const targetDataDoc = Doc.GetProto(first.props.Document); @@ -294,7 +299,7 @@ export default class KeyManager { list.push(doc); } if (count === docids.length) { - const added = list.filter(d => !docList.includes(d)); + const added = list.filter(d => !docList.includes(d)).map(d => clone ? Doc.MakeClone(d) : d); if (added.length) { added.map(doc => doc.context = targetDataDoc); undoBatch(() => { diff --git a/src/client/views/InkingStroke.scss b/src/client/views/InkingStroke.scss index 433433a42..30ab1967e 100644 --- a/src/client/views/InkingStroke.scss +++ b/src/client/views/InkingStroke.scss @@ -4,4 +4,8 @@ stroke-linecap: round; overflow: visible !important; transform-origin: top left; + + svg:not(:root) { + overflow: visible !important; + } }
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 7e3bd1c17..c32e76cec 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -54,10 +54,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const scaleY = (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); const strokeColor = StrCast(this.layoutDoc.color, ActiveInkColor()); const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, - StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5); + StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), StrCast(this.layoutDoc.fillColor, ActiveFillColor()), + StrCast(this.layoutDoc.arrowStart, ActiveArrowStart()), StrCast(this.layoutDoc.arrowEnd, ActiveArrowEnd()), + StrCast(this.layoutDoc.dash, ActiveDash()), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false); const hpoints = InteractionUtils.CreatePolyline(data, left, top, this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, (strokeWidth + 15), - StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", this.props.active() ? "visiblestroke" : "none", false); + StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), StrCast(this.layoutDoc.fillColor, ActiveFillColor()), + "none", "none", "0", scaleX, scaleY, "", this.props.active() ? "visiblepainted" : "none", false, true); return ( <svg className="inkingStroke" width={width} @@ -65,29 +68,14 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume style={{ pointerEvents: this.props.Document.isInkMask ? "all" : "none", transform: this.props.Document.isInkMask ? "translate(2500px, 2500px)" : undefined, - mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset" + mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset", + overflow: "visible", }} onContextMenu={() => { ContextMenu.Instance.addItem({ description: "Analyze Stroke", event: this.analyzeStrokes, icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Make Mask", event: this.makeMask, icon: "paint-brush" }); }} ><defs> - <filter id="dangerShine"> - <feColorMatrix type="matrix" - result="color" - values="1 0 0 0 0 - 0 0 0 0 0 - 0 0 0 0 0 - 0 0 0 1 0"> - </feColorMatrix> - <feGaussianBlur in="color" stdDeviation="4" result="blur"></feGaussianBlur> - <feOffset in="blur" dx="0" dy="0" result="offset"></feOffset> - <feMerge> - <feMergeNode in="bg"></feMergeNode> - <feMergeNode in="offset"></feMergeNode> - <feMergeNode in="SourceGraphic"></feMergeNode> - </feMerge> - </filter> </defs> {hpoints} {points} @@ -100,17 +88,33 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume export function SetActiveInkWidth(width: string): void { !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); } export function SetActiveBezierApprox(bezier: string): void { ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? "" : bezier); } export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeInkColor = value); } +export function SetActiveFillColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeFillColor = value); } +export function SetActiveArrowStart(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); } +export function SetActiveArrowEnd(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); } +export function SetActiveDash(dash: string): void { !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); } export function ActiveInkPen(): Doc { return Cast(Doc.UserDoc().activeInkPen, Doc, null); } export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); } +export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, "none"); } +export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, "none"); } +export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, "none"); } +export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); } export function ActiveInkWidth(): string { return StrCast(ActiveInkPen()?.activeInkWidth, "1"); } export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } -Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { +Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) { Doc.SetSelectedTool(pen ? InkTool.Highlighter : InkTool.None); SetActiveInkWidth(width); SetActiveInkColor(color); + SetActiveFillColor(fill); + SetActiveArrowStart(arrowStart); + SetActiveArrowEnd(arrowEnd); + SetActiveDash(dash); }); Scripting.addGlobal(function activateEraser(pen: any) { return Doc.SetSelectedTool(pen ? InkTool.Eraser : InkTool.None); }); Scripting.addGlobal(function activateStamp(pen: any) { return Doc.SetSelectedTool(pen ? InkTool.Stamp : InkTool.None); }); Scripting.addGlobal(function deactivateInk() { return Doc.SetSelectedTool(InkTool.None); }); Scripting.addGlobal(function setInkWidth(width: any) { return SetActiveInkWidth(width); }); -Scripting.addGlobal(function setInkColor(color: any) { return SetActiveInkColor(color); });
\ No newline at end of file +Scripting.addGlobal(function setInkColor(color: any) { return SetActiveInkColor(color); }); +Scripting.addGlobal(function setFillColor(fill: any) { return SetActiveFillColor(fill); }); +Scripting.addGlobal(function setActiveArrowStart(arrowStart: any) { return SetActiveArrowStart(arrowStart); }); +Scripting.addGlobal(function setActiveArrowEnd(arrowEnd: any) { return SetActiveArrowStart(arrowEnd); }); +Scripting.addGlobal(function setActiveDash(dash: any) { return SetActiveDash(dash); }); diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 6878658a8..77e37834d 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -6,12 +6,14 @@ import * as React from 'react'; import { DocServer } from "../DocServer"; import { AssignAllExtensions } from "../../extensions/General/Extensions"; import { Networking } from "../Network"; +import { CollectionView } from "./collections/CollectionView"; AssignAllExtensions(); export let resolvedPorts: { server: number, socket: number }; (async () => { + window.location.search.includes("safe") && CollectionView.SetSafeMode(true); const info = await CurrentUserUtils.loadCurrentUser(); resolvedPorts = JSON.parse(await Networking.FetchFromServer("/resolvedPorts")); DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, info.email); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index fcaa349cd..68b81ab4f 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -5,8 +5,9 @@ import { faQuestionCircle, faArrowLeft, faArrowRight, 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, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye + faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown } from '@fortawesome/free-solid-svg-icons'; +import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -54,6 +55,9 @@ import { TimelineMenu } from './animationtimeline/TimelineMenu'; import { SnappingManager } from '../util/SnappingManager'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { DocumentManager } from '../util/DocumentManager'; +import { DocumentLinksButton } from './nodes/DocumentLinksButton'; +import { LinkMenu } from './linking/LinkMenu'; +import { LinkDocPreview } from './nodes/LinkDocPreview'; @observer export class MainView extends React.Component { @@ -77,9 +81,17 @@ export class MainView extends React.Component { public isPointerDown = false; + componentDidMount() { + DocServer.setPlaygroundFields(["dataTransition", "_viewTransition", "_panX", "_panY", "_viewScale", "_viewType"]); // can play with these fields on someone else's + const tag = document.createElement('script'); + const proto = DocServer.GetRefField("rtfProto").then(proto => { + (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), + msg => msg && alert(msg)); + }); + tag.src = "https://www.youtube.com/iframe_api"; const firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag); @@ -129,7 +141,7 @@ export class MainView extends React.Component { faQuestionCircle, faArrowLeft, faArrowRight, 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, faTrashAlt, faAngleRight, faBell, - faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye); + faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -321,9 +333,7 @@ export class MainView extends React.Component { const width = this.flyoutWidth; return <Measure offset onResize={this.onResize}> {({ measureRef }) => - <div ref={measureRef} className="mainContent-div" onDragEnter={e => { - console.log("ENTERING"); - }} onDrop={this.onDrop} style={{ width: `calc(100% - ${width}px)` }}> + <div ref={measureRef} className="mainContent-div" onDrop={this.onDrop} style={{ width: `calc(100% - ${width}px)` }}> {!mainContainer ? (null) : this.mainDocView} </div> } @@ -454,28 +464,33 @@ export class MainView extends React.Component { @computed get mainContent() { const sidebar = this.userDoc?.["tabs-panelContainer"]; return !this.userDoc || !(sidebar instanceof Doc) ? (null) : ( - <div className="mainView-mainContent" style={{ color: this.darkScheme ? "rgb(205,205,205)" : "black" }} > - <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}> - <div className="mainView-libraryHandle" onPointerDown={this.onPointerDown} onPointerOver={this.pointerOverDragger} - style={{ backgroundColor: this.defaultBackgroundColors(sidebar) }}> - <span title="library View Dragger" style={{ - width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw", - //height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh", - position: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "absolute" : "fixed", - top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "0" - }} /> - </div> - <div className="mainView-libraryFlyout" style={{ - //transformOrigin: this._flyoutTranslate ? "" : "left center", - transition: this._flyoutTranslate ? "" : "width .5s", - //transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`, - boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.8vw" - }}> - {this.flyout} - {this.expandButton} + <div className="mainView-mainContent" style={{ + color: this.darkScheme ? "rgb(205,205,205)" : "black", + height: RichTextMenu.Instance?.Pinned ? `calc(100% - ${ANTIMODEMENU_HEIGHT})` : "100%" + }} > + <div style={{ display: "contents", flexDirection: "row", position: "relative" }}> + <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}> + <div className="mainView-libraryHandle" onPointerDown={this.onPointerDown} onPointerOver={this.pointerOverDragger} + style={{ backgroundColor: this.defaultBackgroundColors(sidebar) }}> + <span title="library View Dragger" style={{ + width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw", + //height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh", + position: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "absolute" : "fixed", + top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "0" + }} /> + </div> + <div className="mainView-libraryFlyout" style={{ + //transformOrigin: this._flyoutTranslate ? "" : "left center", + transition: this._flyoutTranslate ? "" : "width .5s", + //transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`, + boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.8vw" + }}> + {this.flyout} + {this.expandButton} + </div> </div> + {this.dockingContent} </div> - {this.dockingContent} </div>); } @@ -557,23 +572,51 @@ export class MainView extends React.Component { </div>; } + @computed get inkResources() { + return <svg width={0} height={0}> + <defs> + <filter id="inkSelectionHalo"> + <feColorMatrix type="matrix" + result="color" + values="1 0 0 0 0 + 0 0 0 0 0 + 0 0 0 0 0 + 0 0 0 1 0"> + </feColorMatrix> + <feGaussianBlur in="color" stdDeviation="4" result="blur"></feGaussianBlur> + <feOffset in="blur" dx="0" dy="0" result="offset"></feOffset> + <feMerge> + <feMergeNode in="bg"></feMergeNode> + <feMergeNode in="offset"></feMergeNode> + <feMergeNode in="SourceGraphic"></feMergeNode> + </feMerge> + </filter> + </defs> + </svg>; + } + render() { return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} ref={this._mainViewRef}> + {this.inkResources} <DictationOverlay /> <SharingManager /> <SettingsManager /> <GoogleAuthenticationManager /> <DocumentDecorations /> <GestureOverlay> + <RichTextMenu key="rich" /> {this.mainContent} </GestureOverlay> <PreviewCursor /> + {DocumentLinksButton.EditLink ? <LinkMenu location={DocumentLinksButton.EditLinkLoc} docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)} + {LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors} + linkDoc={LinkDocPreview.LinkInfo.linkDoc} linkSrc={LinkDocPreview.LinkInfo.linkSrc} href={LinkDocPreview.LinkInfo.href} + addDocTab={LinkDocPreview.LinkInfo.addDocTab} /> : (null)} <ContextMenu /> <RadialMenu /> <PDFMenu /> <MarqueeOptionsMenu /> <InkOptionsMenu /> - <RichTextMenu /> <OverlayView /> <TimelineMenu /> {this.snapLines} diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx index e100d3f52..b0752ffb2 100644 --- a/src/client/views/MetadataEntryMenu.tsx +++ b/src/client/views/MetadataEntryMenu.tsx @@ -197,19 +197,23 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ render() { return (<div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}> <div className="metadataEntry-inputArea"> - Key: - <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()} > - <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} - getSuggestionValue={this.getSuggestionValue} - suggestions={emptyPath} - alwaysRenderSuggestions={false} - renderSuggestion={this.renderSuggestion} - onSuggestionsFetchRequested={emptyFunction} - onSuggestionsClearRequested={emptyFunction} - ref={this.autosuggestRef} /> + <div style={{ display: "flex", flexDirection: "row" }}> + <span>Key:</span> + <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()} > + <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} + getSuggestionValue={this.getSuggestionValue} + suggestions={emptyPath} + alwaysRenderSuggestions={false} + renderSuggestion={this.renderSuggestion} + onSuggestionsFetchRequested={emptyFunction} + onSuggestionsClearRequested={emptyFunction} + ref={this.autosuggestRef} /> + </div> </div> - Value: + <div style={{ display: "flex", flexDirection: "row" }}> + <span>Value:</span> <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} /> + </div> {this.considerChildOptions} </div> <div className="metadataEntry-keys" > diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index dd65681d4..1ab99881d 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -9,6 +9,7 @@ import { Transform } from "../util/Transform"; import { DocServer } from '../DocServer'; import { undoBatch } from '../util/UndoManager'; import { NumCast } from '../../fields/Types'; +import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; @observer export class PreviewCursor extends React.Component<{}> { @@ -32,10 +33,11 @@ export class PreviewCursor extends React.Component<{}> { // tests for URL and makes web document const re: any = /^https?:\/\//g; - if (e.clipboardData.getData("text/plain") !== "") { + const plain = e.clipboardData.getData("text/plain"); + if (plain) { // tests for youtube and makes video document - if (e.clipboardData.getData("text/plain").indexOf("www.youtube.com/watch") !== -1) { - const url = e.clipboardData.getData("text/plain").replace("youtube.com/watch?v=", "youtube.com/embed/"); + if (plain.indexOf("www.youtube.com/watch") !== -1) { + const url = plain.replace("youtube.com/watch?v=", "youtube.com/embed/"); undoBatch(() => PreviewCursor._addDocument(Docs.Create.VideoDocument(url, { title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5, @@ -43,8 +45,8 @@ export class PreviewCursor extends React.Component<{}> { })))(); } - else if (re.test(e.clipboardData.getData("text/plain"))) { - const url = e.clipboardData.getData("text/plain"); + else if (re.test(plain)) { + const url = plain; undoBatch(() => PreviewCursor._addDocument(Docs.Create.WebDocument(url, { title: url, _width: 500, _height: 300, UseCors: true, // nativeWidth: 300, nativeHeight: 472.5, @@ -52,10 +54,11 @@ export class PreviewCursor extends React.Component<{}> { })))(); } - else if (e.clipboardData.getData("text/plain").startsWith("__DashDocId(")) { - const docids = e.clipboardData.getData("text/plain").split(":"); + else if (plain.startsWith("__DashDocId(") || plain.startsWith("__DashCloneId(")) { + const clone = plain.startsWith("__DashCloneId("); + const docids = plain.split(":"); const strs = docids[0].split(","); - const ptx = Number(strs[0].substring("__DashDocId(".length)); + const ptx = Number(strs[0].substring((clone ? "__DashCloneId(" : "__DashDocId(").length)); const pty = Number(strs[1].substring(0, strs[1].length - 1)); let count = 1; const list: Doc[] = []; @@ -65,7 +68,7 @@ export class PreviewCursor extends React.Component<{}> { count++; if (doc instanceof Doc) { i === 1 && (first = doc); - const alias = Doc.MakeClone(doc); + const alias = clone ? Doc.MakeClone(doc) : doc; const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx; const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty; alias.x = newPoint[0] + deltaX; @@ -79,6 +82,7 @@ export class PreviewCursor extends React.Component<{}> { e.stopPropagation(); } else { // creates text document + FormattedTextBox.PasteOnLoad = e; undoBatch(() => PreviewCursor._addLiveTextDoc(Docs.Create.TextDocument("", { _width: 500, limitHeight: 400, @@ -115,9 +119,9 @@ export class PreviewCursor extends React.Component<{}> { (e.keyCode < 112 || e.keyCode > 123) && // F1 thru F12 keys !e.key.startsWith("Arrow") && !e.defaultPrevented) { - if ((!e.ctrlKey || (e.keyCode >= 48 && e.keyCode <= 57)) && !e.metaKey) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) { + if ((!e.metaKey && !e.ctrlKey) || (e.keyCode >= 48 && e.keyCode <= 57) || (e.keyCode >= 65 && e.keyCode <= 90)) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) { PreviewCursor.Visible && PreviewCursor._onKeyPress?.(e); - PreviewCursor.Visible = false; + ((!e.ctrlKey && !e.metaKey) || e.key !== "v") && (PreviewCursor.Visible = false); } } else if (PreviewCursor.Visible) { if (e.key === "ArrowRight") { diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index ab984f727..f54bd3aff 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -288,13 +288,13 @@ export class Timeline extends React.Component<FieldViewProps> { resetView(doc: Doc) { doc._panX = doc._customOriginX ?? 0; doc._panY = doc._customOriginY ?? 0; - doc.scale = doc._customOriginScale ?? 1; + doc._viewScale = doc._customOriginScale ?? 1; } setView(doc: Doc) { doc._customOriginX = doc._panX; doc._customOriginY = doc._panY; - doc._customOriginScale = doc.scale; + doc._customOriginScale = doc._viewScale; } /** * zooming mechanism (increment and spacing changes) diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index a969e302d..79c577b6d 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -11,7 +11,7 @@ import { Id } from '../../../fields/FieldSymbols'; import { FieldId } from "../../../fields/RefField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter } from "../../../Utils"; +import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter, setupMoveUpEvents, returnFalse } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; @@ -521,13 +521,15 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.setActive(true); }; const onDown = (e: React.PointerEvent) => { - if (!(e.nativeEvent as any).defaultPrevented) { - e.preventDefault(); - e.stopPropagation(); - const dragData = new DragManager.DocumentDragData([doc]); - dragData.dropAction = doc.dropAction as dropActionType; - DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY); - } + setupMoveUpEvents(this, e, (e) => { + if (!(e as any).defaultPrevented) { + const dragData = new DragManager.DocumentDragData([doc]); + dragData.dropAction = doc.dropAction as dropActionType; + DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY); + return true; + } + return false; + }, returnFalse, emptyFunction); }; tab.buttonDisposer = reaction(() => ((view: Opt<DocumentView>) => view ? [view] : [])(DocumentManager.Instance.getDocumentView(doc)), diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index cc7a9f5ac..e0b53e762 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -43,6 +43,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @observable private heading: string = ""; @observable private color: string = "#f1efeb"; @observable private collapsed: boolean = false; + @observable private _paletteOn = false; private set _heading(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.heading = this.heading = value)); } private set _color(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.color = this.color = value)); } private set _collapsed(value: boolean) { runInAction(() => this.props.headingObject && (this.props.headingObject.collapsed = this.collapsed = value)); } @@ -293,11 +294,10 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr {noChrome ? evContents : <EditableView {...headerEditableViewProps} />} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionColor"> - <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}> - <button className="collectionStackingView-sectionColorButton"> - <FontAwesomeIcon icon="palette" size="lg" /> - </button> - </ Flyout > + <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}> + <FontAwesomeIcon icon="palette" size="lg" /> + </button> + {this._paletteOn ? this.renderColorPicker() : (null)} </div> } {noChrome ? (null) : <button className="collectionStackingView-sectionDelete" onClick={noChrome ? undefined : this.collapseSection}> @@ -305,7 +305,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr </button>} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionOptions"> - <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> + <Flyout anchorPoint={anchorPoints.TOP_CENTER} content={this.renderMenu()}> <button className="collectionStackingView-sectionOptionButton"> <FontAwesomeIcon icon="ellipsis-v" size="lg" /> </button> diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 203c51163..3d8ec2fd5 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -240,11 +240,15 @@ } .collectionStackingView-sectionColorButton { - height: 35px; + height: 30px; + display: inherit; } .collectionStackingView-colorPicker { width: 78px; + z-index: 10; + position: relative; + background: white; .colorOptions { display: flex; @@ -278,7 +282,7 @@ } .collectionStackingView-sectionOptionButton { - height: 35px; + height: 30px; } .collectionStackingView-optionPicker { diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index bcd55f0fe..b60ed853b 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -50,6 +50,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC private dropDisposer?: DragManager.DragDropDisposer; private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); + @observable _paletteOn = false; @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; @@ -326,11 +327,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC <EditableView {...headerEditableViewProps} /> {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionColor"> - <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}> - <button className="collectionStackingView-sectionColorButton"> - <FontAwesomeIcon icon="palette" size="lg" /> - </button> - </ Flyout > + <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}> + <FontAwesomeIcon icon="palette" size="lg" /> + </button> + {this._paletteOn ? this.renderColorPicker() : (null)} </div> } {evContents === `NO ${key.toUpperCase()} VALUE` ? diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 00d6d59c8..3a13ac822 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -19,6 +19,7 @@ import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; import React = require("react"); +import * as rp from 'request-promise'; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc | Doc[]) => boolean; @@ -103,8 +104,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } docFilters = () => { return this.props.ignoreFields?.includes("_docFilters") ? [] : - this.props.docFilters !== returnEmptyFilter ? this.props.docFilters() : - Cast(this.props.Document._docFilters, listSpec("string"), []); + [...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])]; } @computed get childDocs() { const docFilters = this.docFilters(); @@ -400,9 +400,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const item = e.dataTransfer.items[i]; if (item.kind === "string" && item.type.includes("uri")) { const stringContents = await new Promise<string>(resolve => item.getAsString(resolve)); - const type = "html";// (await rp.head(Utils.CorsProxy(stringContents)))["content-type"]; + const type = (await rp.head(Utils.CorsProxy(stringContents)))["content-type"]; if (type) { - const doc = await DocUtils.DocumentFromType(type, stringContents, options); + const doc = await DocUtils.DocumentFromType(type, Utils.CorsProxy(stringContents), options); doc && generatedDocuments.push(doc); } } diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 2aac81146..50f0534bd 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -82,8 +82,6 @@ text-overflow: ellipsis; white-space: pre-wrap; min-width: 10px; - // width:100%;//width: max-content; - } .treeViewItem-openRight { @@ -100,6 +98,7 @@ border-left: dashed 1px #00000042; } +.treeViewItem-header-editing, .treeViewItem-header { border: transparent 1px solid; display: flex; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 747eb36a1..d54f4d6e6 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -13,7 +13,7 @@ import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from '../../util/DocumentManager'; import { SnappingManager } from '../../util/SnappingManager'; -import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; +import { DragManager, dropActionType } from "../../util/DragManager"; import { Scripting } from '../../util/Scripting'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; @@ -55,7 +55,7 @@ export interface TreeViewProps { ScreenToLocalTransform: () => Transform; backgroundColor?: (doc: Doc) => string | undefined; outerXf: () => { translateX: number, translateY: number }; - treeViewId: Doc; + treeViewDoc: Doc; parentKey: string; active: (outsideReaction?: boolean) => boolean; treeViewHideHeaderFields: () => boolean; @@ -76,31 +76,32 @@ export interface TreeViewProps { * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree */ class TreeView extends React.Component<TreeViewProps> { - static _editTitleScript: ScriptField | undefined; + private _editTitleScript: ScriptField | undefined; private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); private _tref = React.createRef<HTMLDivElement>(); private _docRef = React.createRef<DocumentView>(); + private _uniqueId = Utils.GenerateGuid(); + private _editMaxWidth: number | string = 0; + get doc() { return this.props.document; } get noviceMode() { return BoolCast(Doc.UserDoc().noviceMode, false); } - get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive - get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, this.noviceMode ? "layout" : "fields"); } + get displayName() { return "TreeView(" + this.doc.title + ")"; } // this makes mobx trace() statements more descriptive + get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.doc.defaultExpandedView, this.noviceMode ? "layout" : "fields"); } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state - 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 && !this.props.document.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } - @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); } + set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; } + @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.doc.treeViewPreventOpen && BoolCast(this.doc.treeViewOpen)) || this._overrideTreeViewOpen; } + @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.defaultExpandedView); } @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } - @computed get dataDoc() { return this.props.document[DataSym]; } - @computed get fieldKey() { - const splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\'"); - return splits.length > 1 ? splits[1].split("\'")[0] : "data"; - } + @computed get dataDoc() { return this.doc[DataSym]; } + @computed get layoutDoc() { return Doc.Layout(this.doc); } + @computed get fieldKey() { const splits = StrCast(Doc.LayoutField(this.doc)).split("fieldKey={\'"); return splits.length > 1 ? splits[1].split("\'")[0] : "data"; } childDocList(field: string) { - const layout = Doc.LayoutField(this.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined; + const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined; return ((this.props.dataDoc ? DocListCast(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field (layout ? Cast(layout[field], listSpec(Doc)) : undefined) || // else if there's a layout doc, display it's fields - Cast(this.props.document[field], listSpec(Doc))) as Doc[]; // otherwise use the document's data field + Cast(this.doc[field], listSpec(Doc))) as Doc[]; // otherwise use the document's data field } @computed get childDocs() { return this.childDocList(this.fieldKey); } @computed get childLinks() { return this.childDocList("links"); } @@ -109,22 +110,27 @@ class TreeView extends React.Component<TreeViewProps> { Doc.ComputeContentBounds(DocListCast(this.props.document[this.fieldKey])); } - @undoBatch openRight = () => this.props.addDocTab(this.props.document, "onRight", this.props.libraryPath); + @undoBatch openRight = () => this.props.addDocTab(this.doc, "onRight", this.props.libraryPath); @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { - return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc); + return this.doc !== target && this.props.deleteDoc(doc) && addDoc(doc); } @undoBatch @action remove = (doc: Doc | Doc[], key: string) => { - return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => - flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); + return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); } @undoBatch @action removeDoc = (doc: Doc | Doc[]) => { return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.props.containingCollection, Doc.LayoutFieldKey(this.props.containingCollection), doc), true); } + constructor(props: any) { + super(props); + this._editTitleScript = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); selectDoc(self);} `); + if (Doc.GetT(this.doc, "editTitle", "string", true) === "*") Doc.SetInPlace(this.doc, "editTitle", this._uniqueId, false); + } + protected createTreeDropTarget = (ele: HTMLDivElement) => { this._treedropDisposer?.(); - ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this)), this.props.document); + ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this)), this.doc); } onPointerEnter = (e: React.PointerEvent): void => { @@ -136,7 +142,9 @@ class TreeView extends React.Component<TreeViewProps> { } onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.dataDoc); - this._header!.current!.className = "treeViewItem-header"; + if (this._header?.current?.className !== "treeViewItem-header-editing") { + this._header!.current!.className = "treeViewItem-header"; + } document.removeEventListener("pointermove", this.onDragMove, true); } onDragMove = (e: PointerEvent): void => { @@ -144,7 +152,7 @@ class TreeView extends React.Component<TreeViewProps> { const pt = [e.clientX, e.clientY]; const rect = this._header!.current!.getBoundingClientRect(); const before = pt[1] < rect.top + rect.height / 2; - const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length); + const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length); this._header!.current!.className = "treeViewItem-header"; if (inside) this._header!.current!.className += " treeViewItem-header-inside"; else if (before) this._header!.current!.className += " treeViewItem-header-above"; @@ -155,42 +163,38 @@ class TreeView extends React.Component<TreeViewProps> { editableView = (key: string, style?: string) => (<EditableView oneLine={true} display={"inline-block"} - editing={true /*this.dataDoc[Id] === EditableView.loadId*/} - contents={StrCast(this.props.document[key])} + editing={true} + contents={StrCast(this.doc[key])} height={12} + sizeToContent={true} fontStyle={style} fontSize={12} - GetValue={() => StrCast(this.props.document[key])} + GetValue={() => StrCast(this.doc[key])} SetValue={undoBatch((value: string) => { - Doc.SetInPlace(this.props.document, key, value, false) || true; - Doc.SetInPlace(this.props.document, "editTitle", undefined, false); + Doc.SetInPlace(this.doc, key, value, false) || true; + Doc.SetInPlace(this.doc, "editTitle", undefined, false); })} OnFillDown={undoBatch((value: string) => { - Doc.SetInPlace(this.props.document, key, value, false); + Doc.SetInPlace(this.doc, key, value, false); const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, _LODdisable: true, templates: new List<string>([Templates.Title.Layout]) }); - Doc.SetInPlace(this.props.document, "editTitle", undefined, false); - Doc.SetInPlace(doc, "editTitle", true, false); + Doc.SetInPlace(this.doc, "editTitle", undefined, false); + Doc.SetInPlace(doc, "editTitle", "*", false); return this.props.addDocument(doc); })} onClick={() => { SelectionManager.DeselectAll(); - Doc.UserDoc().activeSelection = new List([this.props.document]); + Doc.UserDoc().activeSelection = new List([this.doc]); return false; }} OnTab={undoBatch((shift?: boolean) => { - EditableView.loadId = this.dataDoc[Id]; shift ? this.props.outdentDocument?.() : this.props.indentDocument?.(); - setTimeout(() => { // unsetting/setting brushing for this doc will recreate & refocus this editableView after all other treeview changes have been made to the Dom (which may remove focus from this document). - Doc.UnBrushDoc(this.props.document); - Doc.BrushDoc(this.props.document); - EditableView.loadId = ""; - }, 0); + setTimeout(() => Doc.SetInPlace(this.doc, "editTitle", "*", false), 0); })} />) preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { const dragData = de.complete.docDragData; - dragData && (dragData.dropAction = this.props.treeViewId[Id] === dragData.treeViewId ? "same" : dragData.dropAction); + dragData && (dragData.dropAction = this.props.treeViewDoc === dragData.treeViewDoc ? "same" : dragData.dropAction); } @undoBatch @@ -198,18 +202,18 @@ class TreeView extends React.Component<TreeViewProps> { const pt = [de.x, de.y]; const rect = this._header!.current!.getBoundingClientRect(); const before = pt[1] < rect.top + rect.height / 2; - const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length); + const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length); const complete = de.complete; if (complete.linkDragData) { const sourceDoc = complete.linkDragData.linkSourceDocument; - const destDoc = this.props.document; + const destDoc = this.doc; DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link"); e.stopPropagation(); } const docDragData = complete.docDragData; if (docDragData) { e.stopPropagation(); - if (docDragData.draggedDocuments[0] === this.props.document) return true; + if (docDragData.draggedDocuments[0] === this.doc) return true; const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); let addDoc = parentAddDoc; if (inside) { @@ -222,35 +226,28 @@ class TreeView extends React.Component<TreeViewProps> { return false; } - 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]); - - return finalXf; - } - getTransform = () => { - const { scale, translateX, translateY } = Utils.GetScreenTransform(this._tref.current!); + refTransform = (ref: HTMLDivElement) => { + const { scale, translateX, translateY } = Utils.GetScreenTransform(ref); 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]); - return finalXf; + return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); } + docTransform = () => this.refTransform(this._dref.current!); + getTransform = () => this.refTransform(this._tref.current!); docWidth = () => { - const layoutDoc = Doc.Layout(this.props.document); + const layoutDoc = this.layoutDoc; 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; } docHeight = () => { - const layoutDoc = Doc.Layout(this.props.document); + const layoutDoc = this.layoutDoc; const bounds = this.boundsOfCollectionDocument; return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => { 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) : + return layoutDoc._fitWidth ? (!this.doc._nativeHeight ? NumCast(this.props.containingCollection._height) : Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, NumCast(this.props.containingCollection._height)))) : NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; @@ -259,7 +256,7 @@ class TreeView extends React.Component<TreeViewProps> { @computed get expandedField() { const ids: { [key: string]: string } = {}; - const doc = this.props.document; + const doc = this.doc; doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); const rows: JSX.Element[] = []; @@ -273,13 +270,12 @@ class TreeView extends React.Component<TreeViewProps> { const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce( (flg, doc) => flg && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true), true); contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : - DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, + DocListCast(contents), this.props.treeViewDoc, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields); } else { - contentElement = <EditableView - key="editableView" + contentElement = <EditableView key="editableView" contents={contents !== undefined ? Field.toString(contents as Field) : "null"} height={13} fontSize={12} @@ -307,45 +303,45 @@ class TreeView extends React.Component<TreeViewProps> { return rows; } - rtfWidth = () => Math.min(Doc.Layout(this.props.document)?.[WidthSym](), this.props.panelWidth() - 20); - rtfHeight = () => this.rtfWidth() < Doc.Layout(this.props.document)?.[WidthSym]() ? Math.min(Doc.Layout(this.props.document)?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; + rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.panelWidth() - 20); + rtfHeight = () => this.rtfWidth() < this.layoutDoc?.[WidthSym]() ? Math.min(this.layoutDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; @computed get renderContent() { TraceMobx(); - const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined; - if (expandKey !== undefined) { + const expandKey = this.treeViewExpandedView; + if (["links", this.fieldKey].includes(expandKey)) { const remDoc = (doc: Doc | Doc[]) => this.remove(doc, expandKey); const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true), true); const docs = expandKey === "links" ? this.childLinks : this.childDocs; const sortKey = `${this.fieldKey}-sortAscending`; return <ul key={expandKey + "more"} onClick={(e) => { - this.props.document[sortKey] = (this.props.document[sortKey] ? false : (this.props.document[sortKey] === false ? undefined : true)); + this.doc[sortKey] = (this.doc[sortKey] ? false : (this.doc[sortKey] === false ? undefined : true)); e.stopPropagation(); }}> {!docs ? (null) : - TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document), + TreeView.GetChildElements(docs, this.props.treeViewDoc, this.layoutDoc, this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - StrCast(this.props.document.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, + StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)} + [...this.props.renderedIds, this.doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)} </ul >; } else if (this.treeViewExpandedView === "fields") { - return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}> + return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.doc[Id] + this.doc.title}> {this.expandedField} </div></ul>; } else { - const layoutDoc = Doc.Layout(this.props.document); + const layoutDoc = this.layoutDoc; const panelHeight = layoutDoc.type === DocumentType.RTF ? this.rtfHeight : this.docHeight; const panelWidth = layoutDoc.type === DocumentType.RTF ? this.rtfWidth : this.docWidth; - return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.props.document[Id] + this.props.document.title}> + return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.doc[Id]}> <ContentFittingDocumentView Document={layoutDoc} DataDoc={this.dataDoc} LibraryPath={emptyPath} renderDepth={this.props.renderDepth + 1} rootSelected={returnTrue} - treeViewId={this.props.treeViewId[Id]} + treeViewDoc={undefined} backgroundColor={this.props.backgroundColor} fitToBox={this.boundsOfCollectionDocument !== undefined} FreezeDimensions={true} @@ -372,17 +368,17 @@ class TreeView extends React.Component<TreeViewProps> { } } - get onCheckedClick() { return this.props.onCheckedClick || ScriptCast(this.props.document.onCheckedClick); } + get onCheckedClick() { return this.props.onCheckedClick || ScriptCast(this.doc.onCheckedClick); } @action bulletClick = (e: React.MouseEvent) => { - if (this.onCheckedClick && this.props.document.type !== DocumentType.COL) { + if (this.onCheckedClick && this.doc.type !== DocumentType.COL) { // this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check"; this.onCheckedClick.script.run({ - this: this.props.document.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.props.document, + this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc, heading: this.props.containingCollection.title, - checked: this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check", - containingTreeView: this.props.treeViewId, + checked: this.doc.treeViewChecked === "check" ? "x" : this.doc.treeViewChecked === "x" ? undefined : "check", + containingTreeView: this.props.treeViewDoc, }, console.log); } else { this.treeViewOpen = !this.treeViewOpen; @@ -392,104 +388,110 @@ class TreeView extends React.Component<TreeViewProps> { @computed get renderBullet() { - const checked = this.props.document.type === DocumentType.COL ? undefined : this.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined; + const checked = this.doc.type === DocumentType.COL ? undefined : this.onCheckedClick ? (this.doc.treeViewChecked ?? "unchecked") : undefined; return <div className="bullet" title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : "view fields"} onClick={this.bulletClick} - style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}> + style={{ color: StrCast(this.doc.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}> {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />} </div>; } showContextMenu = (e: React.MouseEvent) => { this._docRef.current?.ContentDiv && simulateMouseClick(this._docRef.current.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); - e.stopPropagation(); } focusOnDoc = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(doc)?.props.focus(doc, true); - contextMenuItems = () => { - const focusScript = ScriptField.MakeFunction(`DocFocus(self)`); - return [{ script: focusScript!, label: "Focus" }]; - } + contextMenuItems = () => [{ script: ScriptField.MakeFunction(`DocFocus(self)`)!, label: "Focus" }]; + truncateTitleWidth = () => NumCast(this.props.treeViewDoc.treeViewTruncateTitleWidth, 0); + showTitleEdit = () => ["*", this._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || ""); /** * Renders the EditableView title element for placement into the tree. */ @computed get renderTitle() { TraceMobx(); - (!TreeView._editTitleScript) && (TreeView._editTitleScript = ScriptField.MakeFunction("setInPlace(self, 'editTitle', true)")); - const headerElements = ( + const headerElements = this.props.treeViewHideHeaderFields() ? (null) : <> - <FontAwesomeIcon icon="cog" size="sm" onClick={e => this.showContextMenu(e)}></FontAwesomeIcon> + <FontAwesomeIcon icon="cog" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} /> <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} onPointerDown={action(() => { if (this.treeViewOpen) { - this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode ? "layout" : "fields") : - this.treeViewExpandedView === "fields" && Doc.Layout(this.props.document) ? "layout" : - this.treeViewExpandedView === "layout" && this.props.document.links ? "links" : + this.doc.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode ? "layout" : "fields") : + this.treeViewExpandedView === "fields" && this.layoutDoc ? "layout" : + this.treeViewExpandedView === "layout" && DocListCast(this.doc.links).length ? "links" : this.childDocs ? this.fieldKey : (Doc.UserDoc().noviceMode ? "layout" : "fields"); } this.treeViewOpen = true; })}> {this.treeViewExpandedView} </span> - </>); - const openRight = (<div className="treeViewItem-openRight" onClick={this.openRight}> - <FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" /> - </div>); + </>; + const view = this.showTitleEdit() ? this.editableView("title") : + <DocumentView + ref={this._docRef} + Document={this.doc} + DataDoc={undefined} + treeViewDoc={this.props.treeViewDoc} + LibraryPath={this.props.libraryPath || emptyPath} + addDocument={undefined} + addDocTab={this.props.addDocTab} + rootSelected={returnTrue} + pinToPres={emptyFunction} + onClick={this.props.onChildClick || this._editTitleScript} + dropAction={this.props.dropAction} + moveDocument={this.move} + removeDocument={this.removeDoc} + ScreenToLocalTransform={this.getTransform} + ContentScaling={returnOne} + PanelWidth={this.truncateTitleWidth} + PanelHeight={returnZero} + NativeHeight={returnZero} + NativeWidth={returnZero} + contextMenuItems={this.contextMenuItems} + renderDepth={1} + focus={returnTrue} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + dontRegisterView={BoolCast(this.props.treeViewDoc.dontRegisterChildViews)} + docFilters={returnEmptyFilter} + ContainingCollectionView={undefined} + ContainingCollectionDoc={this.props.containingCollection} + />; return <> <div className="docContainer" ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`} style={{ - fontWeight: this.props.document.searchMatch ? "bold" : undefined, - textDecoration: Doc.GetT(this.props.document, "title", "string", true) ? "underline" : undefined, - outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, + fontWeight: this.doc.searchMatch ? "bold" : undefined, + textDecoration: Doc.GetT(this.doc, "title", "string", true) ? "underline" : undefined, + outline: BoolCast(this.doc.workspaceBrush) ? "dashed 1px #06123232" : undefined, pointerEvents: this.props.active() || SnappingManager.GetIsDragging() ? undefined : "none" }} > - {Doc.GetT(this.props.document, "editTitle", "boolean", true) ? - this.editableView("title") : - <DocumentView - ref={this._docRef} - Document={this.props.document} - DataDoc={undefined} - treeViewId={this.props.treeViewId[Id]} - LibraryPath={this.props.libraryPath || emptyPath} - addDocument={undefined} - addDocTab={this.props.addDocTab} - rootSelected={returnTrue} - pinToPres={emptyFunction} - onClick={this.props.onChildClick || TreeView._editTitleScript} - dropAction={this.props.dropAction} - moveDocument={this.move} - removeDocument={this.removeDoc} - ScreenToLocalTransform={this.getTransform} - ContentScaling={returnOne} - PanelWidth={returnZero} - PanelHeight={returnZero} - NativeHeight={returnZero} - NativeWidth={returnZero} - contextMenuItems={this.contextMenuItems} - renderDepth={1} - focus={returnTrue} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildViews)} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={this.props.containingCollection} - />} + {view} </div > - {this.props.treeViewHideHeaderFields() ? (null) : headerElements} - {openRight} + {headerElements} + <div className="treeViewItem-openRight" onClick={this.openRight}> + <FontAwesomeIcon title="open in pane on right" icon="external-link-alt" size="sm" /> + </div> </>; } render() { TraceMobx(); - const sorting = this.props.document[`${this.fieldKey}-sortAscending`]; - //setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0); + const sorting = this.doc[`${this.fieldKey}-sortAscending`]; + if (this.showTitleEdit()) { // find containing CollectionTreeView and set our maximum width so the containing tree view won't have to scroll + let par: any = this._header?.current; + if (par) { + while (par && par.className !== "collectionTreeView-dropTarget") par = par.parentNode; + if (par) { + const par_rect = (par as HTMLElement).getBoundingClientRect(); + const my_recct = this._docRef.current?.ContentDiv?.getBoundingClientRect(); + this._editMaxWidth = Math.max(100, par_rect.right - (my_recct?.left || 0)); + } + } + } else this._editMaxWidth = ""; return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onPointerDown={e => this.props.active() && SelectionManager.DeselectAll()}> <li className="collection-child"> - <div className="treeViewItem-header" ref={this._header} onClick={e => { + <div className={`treeViewItem-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ maxWidth: this._editMaxWidth }} onClick={e => { if (this.props.active(true)) { e.stopPropagation(); e.preventDefault(); @@ -507,14 +509,14 @@ class TreeView extends React.Component<TreeViewProps> { {this.renderTitle} </div> <div className="treeViewItem-border" style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> - {!this.treeViewOpen || this.props.renderedIds.indexOf(this.props.document[Id]) !== -1 ? (null) : this.renderContent} + {!this.treeViewOpen || this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? (null) : this.renderContent} </div> </li> </div>; } public static GetChildElements( childDocs: Doc[], - treeViewId: Doc, + treeViewDoc: Doc, containingCollection: Doc, dataDoc: Doc | undefined, key: string, @@ -624,7 +626,7 @@ class TreeView extends React.Component<TreeViewProps> { libraryPath={libraryPath ? [...libraryPath, containingCollection] : undefined} containingCollection={containingCollection} prevSibling={docs[i]} - treeViewId={treeViewId} + treeViewDoc={treeViewDoc} key={child[Id]} indentDocument={indent} outdentDocument={outdent} @@ -665,21 +667,22 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll private treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; - @computed get dataDoc() { return this.props.DataDoc || this.props.Document; } + @computed get doc() { return this.props.Document; } + @computed get dataDoc() { return this.props.DataDoc || this.doc; } protected createTreeDropTarget = (ele: HTMLDivElement) => { this.treedropDisposer?.(); if (this._mainEle = ele) { - this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.props.Document, this.onInternalPreDrop.bind(this)); + this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this)); } } protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { const dragData = de.complete.docDragData; if (dragData) { - if (targetAction && !dragData.draggedDocuments.some(d => d.context === this.props.Document && this.childDocs.includes(d))) { + if (targetAction && !dragData.draggedDocuments.some(d => d.context === this.doc && this.childDocs.includes(d))) { dragData.dropAction = targetAction; - } else dragData.dropAction = this.props.Document[Id] === dragData?.treeViewId ? "same" : dragData.dropAction; + } else dragData.dropAction = this.doc === dragData?.treeViewDoc ? "same" : dragData.dropAction; } } @@ -691,7 +694,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll @action remove = (doc: Doc | Doc[]): boolean => { const docs = doc instanceof Doc ? [doc] : doc; - const targetDataDoc = this.props.Document[DataSym]; + const targetDataDoc = this.doc[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const result = value.filter(v => !docs.includes(v)); if (result.length !== value.length) { @@ -704,9 +707,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll addDoc = (doc: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => { const doAddDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => - flg && Doc.AddDocToList(this.props.Document[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false), true); - if (this.props.Document.resolvedDataDoc instanceof Promise) { - this.props.Document.resolvedDataDoc.then((resolved: any) => doAddDoc(doc)); + flg && Doc.AddDocToList(this.doc[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false), true); + if (this.doc.resolvedDataDoc instanceof Promise) { + this.doc.resolvedDataDoc.then((resolved: any) => doAddDoc(doc)); } else { doAddDoc(doc); } @@ -714,25 +717,26 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped() && this.props.Document === Doc.UserDoc().myWorkspaces) { + if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myWorkspaces) { ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); - ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" }); + ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.doc), icon: "minus" }); e.stopPropagation(); e.preventDefault(); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - } else if (!e.isPropagationStopped() && this.props.Document === Doc.UserDoc().myRecentlyClosed) { + } else if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myRecentlyClosed) { ContextMenu.Instance.addItem({ description: "Clear All", event: () => Doc.UserDoc().myRecentlyClosed = new List<Doc>(), icon: "plus" }); e.stopPropagation(); e.preventDefault(); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } else { const layoutItems: ContextMenuProps[] = []; - layoutItems.push({ description: (this.props.Document.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.treeViewPreventOpen = !this.props.Document.treeViewPreventOpen, icon: "paint-brush" }); - layoutItems.push({ description: (this.props.Document.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.treeViewHideHeaderFields = !this.props.Document.treeViewHideHeaderFields, icon: "paint-brush" }); - layoutItems.push({ description: (this.props.Document.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.props.Document.treeViewHideTitle = !this.props.Document.treeViewHideTitle, icon: "paint-brush" }); + layoutItems.push({ description: (this.doc.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.doc.treeViewPreventOpen = !this.doc.treeViewPreventOpen, icon: "paint-brush" }); + layoutItems.push({ description: (this.doc.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.doc.treeViewHideHeaderFields = !this.doc.treeViewHideHeaderFields, icon: "paint-brush" }); + layoutItems.push({ description: (this.doc.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.doc.treeViewHideTitle = !this.doc.treeViewHideTitle, icon: "paint-brush" }); + layoutItems.push({ description: (this.doc.treeViewHideLinkLines ? "Show" : "Hide") + " Link Lines", event: () => this.doc.treeViewHideLinkLines = !this.doc.treeViewHideLinkLines, icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" }); } - ContextMenu.Instance.addItem({ + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: "Buxton Layout", icon: "eye", event: () => { const { ImageDocument, PdfDocument } = Docs.Create; const { Document } = this.props; @@ -745,7 +749,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll const fallback = ImageDocument("http://cs.brown.edu/~bcz/face.gif", { _width: 400 }); // replace with desired double click target let pdfContent: string; - DocListCast(this.dataDoc[this.props.fieldKey]).map(d => { + this.childDocs?.map(d => { DocListCast(d.data).map((img, i) => { const caption = (d.captions as any)[i]; if (caption) { @@ -774,7 +778,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ - description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" + description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" }); !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } @@ -784,7 +788,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll @computed get renderClearButton() { return <div id="toolbar" key="toolbar"> <button className="toolbar-button round-button" title="Empty" - onClick={undoBatch(action(() => Doc.GetProto(this.props.Document)[this.props.fieldKey] = undefined))}> + onClick={undoBatch(action(() => Doc.GetProto(this.doc)[this.props.fieldKey] = undefined))}> <FontAwesomeIcon icon={"trash"} size="sm" /> </button> </div >; @@ -794,51 +798,52 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll console.log(e); } render() { - if (!(this.props.Document instanceof Doc)) return (null); - const dropAction = StrCast(this.props.Document.childDropAction) as dropActionType; + TraceMobx(); + if (!(this.doc instanceof Doc)) return (null); + const dropAction = StrCast(this.doc.childDropAction) as dropActionType; const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(d, target, addDoc); const childDocs = this.props.overrideDocuments ? this.props.overrideDocuments : this.childDocs; return !childDocs ? (null) : ( - <div className="collectionTreeView-dropTarget" id="body" - style={{ - background: this.props.backgroundColor?.(this.props.Document), - paddingLeft: `${NumCast(this.props.Document._xPadding, 10)}px`, - paddingRight: `${NumCast(this.props.Document._xPadding, 10)}px`, - paddingTop: `${NumCast(this.props.Document._yPadding, 20)}px`, - pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined - }} - onKeyPress={this.onKeyPress} - onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} - onDrop={this.onTreeDrop} - ref={this.createTreeDropTarget}> - {(this.props.treeViewHideTitle || this.props.Document.treeViewHideTitle ? (null) : <EditableView - contents={this.dataDoc.title} - editing={false} - display={"block"} - maxHeight={72} - height={"auto"} - GetValue={() => StrCast(this.dataDoc.title)} - SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)} - OnFillDown={undoBatch((value: string) => { - Doc.SetInPlace(this.dataDoc, "title", value, false); - const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, _LODdisable: true, templates: new List<string>([Templates.Title.Layout]) }); - EditableView.loadId = doc[Id]; - Doc.SetInPlace(doc, "editTitle", true, false); - this.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true); - })} />)} - {this.props.Document.allowClear ? this.renderClearButton : (null)} - <ul className="no-indent" style={{ width: "max-content" }} > - { - TreeView.GetChildElements(childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, - moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, - this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.props.Document.treeViewHideHeaderFields), - BoolCast(this.props.Document.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick, - this.props.onChildClick || ScriptCast(this.props.Document.onChildClick), this.props.ignoreFields) - } - </ul> - </div > + <div className="collectionTreeView-container" onContextMenu={this.onContextMenu}> + <div className="collectionTreeView-dropTarget" id="body" + style={{ + background: this.props.backgroundColor?.(this.doc), + paddingLeft: `${NumCast(this.doc._xPadding, 10)}px`, + paddingRight: `${NumCast(this.doc._xPadding, 10)}px`, + paddingTop: `${NumCast(this.doc._yPadding, 20)}px`, + pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined + }} + onKeyPress={this.onKeyPress} + onWheel={(e) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} + onDrop={this.onTreeDrop} + ref={this.createTreeDropTarget}> + {this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : <EditableView + contents={this.dataDoc.title} + editing={false} + display={"block"} + maxHeight={72} + height={"auto"} + GetValue={() => StrCast(this.dataDoc.title)} + SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)} + OnFillDown={undoBatch((value: string) => { + Doc.SetInPlace(this.dataDoc, "title", value, false); + const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, _LODdisable: true, templates: new List<string>([Templates.Title.Layout]) }); + Doc.SetInPlace(doc, "editTitle", "*", false); + this.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true); + })} />} + {this.doc.allowClear ? this.renderClearButton : (null)} + <ul className="no-indent" style={{ width: "max-content" }} > + { + TreeView.GetChildElements(childDocs, this.doc, this.doc, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, + moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, + this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields), + BoolCast(this.doc.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick, + this.props.onChildClick || ScriptCast(this.doc.onChildClick), this.props.ignoreFields) + } + </ul> + </div > + </div> ); } } diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss index 7877fe155..b630f9cf8 100644 --- a/src/client/views/collections/CollectionView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -11,16 +11,18 @@ height: 100%; overflow: hidden; // bcz: used to be 'auto' which would create scrollbars when there's a floating doc that's not visible. not sure if that's better, but the scrollbars are annoying... - .collectionTimeView-dragger { - background-color: lightgray; + .collectionView-filterDragger { + background-color: rgb(140, 139, 139); height: 40px; - width: 20px; + width: 10px; position: absolute; - border-radius: 10px; top: 55%; border: 1px black solid; + border-radius: 0; + border-top-left-radius: 20px; + border-bottom-left-radius: 20px; + border-right: unset; z-index: 2; - right: -10px; } .collectionTimeView-treeView { display: flex; @@ -30,6 +32,8 @@ position: absolute; right: 0; top: 0; + border-left: solid 1px; + z-index: 1; .collectionTimeView-addfacet { display: inline-block; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 78b70b5fa..5c87bc483 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -108,7 +108,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus get collectionViewType(): CollectionViewType | undefined { const viewField = StrCast(this.props.Document._viewType); if (CollectionView._safeMode) { - if (viewField === CollectionViewType.Freeform) { + if (viewField === CollectionViewType.Freeform || viewField === CollectionViewType.Schema) { return CollectionViewType.Tree; } if (viewField === CollectionViewType.Invalid) { @@ -193,7 +193,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus // return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />; } - screenToLocalTransform = () => this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth()); + screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth()); private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { const props: SubCollectionViewProps = { ...this.props, ...renderProps, ScreenToLocalTransform: this.screenToLocalTransform, CollectionView: this, annotationsKey: "" }; switch (type) { @@ -436,7 +436,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { this._facetWidth = this.props.PanelWidth() - Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0); return false; - }), returnFalse, action(() => this._facetWidth = this.facetWidth() < 15 ? Math.min(this.props.PanelWidth() - 25, 200) : 0)); + }), returnFalse, action(() => this._facetWidth = this.facetWidth() < 15 ? Math.min(this.props.PanelWidth() - 25, 200) : 0), false); } filterBackground = () => "rgba(105, 105, 105, 0.432)"; get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves) @@ -535,7 +535,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus : ""))} {!this.props.isSelected() || this.props.PanelHeight() < 100 || this.props.Document.hideFilterView ? (null) : - <div className="collectionTimeView-dragger" title="library View Dragger" onPointerDown={this.onPointerDown} style={{ right: this.facetWidth() - 10 }} /> + <div className="collectionView-filterDragger" title="library View Dragger" onPointerDown={this.onPointerDown} + style={{ right: this.facetWidth() - 1, top: this.props.Document._viewType === CollectionViewType.Docking ? "25%" : "55%" }} /> } {this.facetWidth() < 10 ? (null) : this.filterView} </div>); diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 31a086d93..276bccede 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -40,28 +40,41 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro _templateCommand = { params: ["target", "source"], title: "=> item view", script: "this.target.childLayout = getDocTemplate(this.source?.[0])", - immediate: (source: Doc[]) => this.target.childLayout = Doc.getDocTemplate(source?.[0]), + immediate: undoBatch((source: Doc[]) => source.length && (this.target.childLayout = Doc.getDocTemplate(source?.[0]))), initialize: emptyFunction, }; _narrativeCommand = { params: ["target", "source"], title: "=> child click view", script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])", - immediate: (source: Doc[]) => this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]), + immediate: undoBatch((source: Doc[]) => source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]))), initialize: emptyFunction, }; _contentCommand = { - params: ["target", "source"], title: "=> content", + params: ["target", "source"], title: "=> clear content", script: "getProto(this.target).data = copyField(this.source);", - immediate: (source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source), // Doc.aliasDocs(source), + immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), // Doc.aliasDocs(source), initialize: emptyFunction, }; _viewCommand = { - params: ["target"], title: "=> saved view", + params: ["target"], title: "=> reset view", script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", - immediate: (source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target.scale = 1; }, + immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target.scale = 1; }), initialize: (button: Doc) => { button.restoredPanX = this.target._panX; button.restoredPanY = this.target._panY; button.restoredScale = this.target.scale; }, }; - _freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand]; + _clusterCommand = { + params: ["target"], title: "=> fit content", + script: "this.target._fitToBox = !this.target._fitToBox;", + immediate: undoBatch((source: Doc[]) => this.target._fitToBox = !this.target._fitToBox), + initialize: emptyFunction + }; + _fitContentCommand = { + params: ["target"], title: "=> toggle clusters", + script: "this.target.useClusters = !this.target.useClusters;", + immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters), + initialize: emptyFunction + }; + + _freeform_commands = [this._viewCommand, this._fitContentCommand, this._clusterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; _stacking_commands = [this._contentCommand, this._templateCommand]; _masonry_commands = [this._contentCommand, this._templateCommand]; _schema_commands = [this._templateCommand, this._narrativeCommand]; @@ -85,6 +98,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @observable private _currentKey: string = ""; componentDidMount = action(() => { + this._currentKey = this._currentKey || (this._buttonizableCommands.length ? this._buttonizableCommands[0]?.title : ""); // chrome status is one of disabled, collapsed, or visible. this determines initial state from document switch (this.props.CollectionView.props.Document._chromeStatus) { case "disabled": @@ -250,7 +264,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY)); return true; - }, emptyFunction, emptyFunction); + }, emptyFunction, () => { + this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate([])); + }); } @computed get templateChrome() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 6cac39f77..a24693c30 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -56,27 +56,27 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const targetAhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(AanchorId)); const targetBhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(BanchorId)); if (!targetBhyperlink) { - this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; - this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; + this.props.A.rootDoc[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; + this.props.A.rootDoc[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; } else { setTimeout(() => { - (this.props.A.props.Document[(this.props.A.props as any).fieldKey] as Doc); + (this.props.A.rootDoc[(this.props.A.props as any).fieldKey] as Doc); const m = targetBhyperlink.getBoundingClientRect(); const mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - this.props.A.props.Document[afield + "_x"] = mp[0] / this.props.A.props.PanelWidth() * 100; - this.props.A.props.Document[afield + "_y"] = mp[1] / this.props.A.props.PanelHeight() * 100; + this.props.A.rootDoc[afield + "_x"] = Math.min(1, mp[0] / this.props.A.props.PanelWidth()) * 100; + this.props.A.rootDoc[afield + "_y"] = Math.min(1, mp[1] / this.props.A.props.PanelHeight()) * 100; }, 0); } if (!targetAhyperlink) { - this.props.A.props.Document[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100; - this.props.A.props.Document[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100; + this.props.A.rootDoc[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100; + this.props.A.rootDoc[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100; } else { setTimeout(() => { - (this.props.B.props.Document[(this.props.B.props as any).fieldKey] as Doc); + (this.props.B.rootDoc[(this.props.B.props as any).fieldKey] as Doc); const m = targetAhyperlink.getBoundingClientRect(); const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - this.props.B.props.Document[bfield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100; - this.props.B.props.Document[bfield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100; + this.props.B.rootDoc[bfield + "_x"] = Math.min(1, mp[0] / this.props.B.props.PanelWidth()) * 100; + this.props.B.rootDoc[bfield + "_y"] = Math.min(1, mp[1] / this.props.B.props.PanelHeight()) * 100; }, 0); } }) diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index ae81b4b36..1a2421bfd 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -10,6 +10,7 @@ import React = require("react"); import { Utils, emptyFunction } from "../../../../Utils"; import { DocumentType } from "../../../documents/DocumentTypes"; import { SnappingManager } from "../../../util/SnappingManager"; +import { Cast } from "../../../../fields/Types"; @observer export class CollectionFreeFormLinksView extends React.Component { @@ -30,8 +31,8 @@ export class CollectionFreeFormLinksView extends React.Component { return drawnPairs; }, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]); return connections.filter(c => - c.a.props.Document.type === DocumentType.LINK && - c.a.props.pinToPres !== emptyFunction && c.b.props.pinToPres !== emptyFunction // bcz: this prevents links to be drawn to anchors in CollectionTree views -- this is a hack that should be fixed + c.a.props.Document.type === DocumentType.LINK + && !c.a.props.treeViewDoc?.treeViewHideLinkLines && !c.b.props.treeViewDoc?.treeViewHideLinkLines ).map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index d9011c9d3..92aee3776 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,7 +1,6 @@ @import "../../globalCssVariables"; -.collectionfreeformview-none, -.collectionfreeformview-ease { +.collectionfreeformview-none { position: inherit; top: 0; left: 0; @@ -22,10 +21,6 @@ } } -.collectionfreeformview-ease { - transition: transform 500ms; -} - .collectionfreeformview-none { touch-action: none; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 00260745d..5135c4ae4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -45,7 +45,9 @@ import React = require("react"); import { CollectionViewType } from "../CollectionView"; import { Timeline } from "../../animationtimeline/Timeline"; import { SnappingManager } from "../../../util/SnappingManager"; -import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../../InkingStroke"; +import { InkingStroke, ActiveArrowStart, ActiveArrowEnd, ActiveInkColor, ActiveFillColor, ActiveInkWidth, ActiveInkBezierApprox, ActiveDash } from "../../InkingStroke"; +import { DocumentType } from "../../../documents/DocumentTypes"; +import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -61,7 +63,7 @@ export const panZoomSchema = createSchema({ fitToBox: "boolean", _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set - panTransformType: "string", + _viewTransition: "string", scrollHeight: "number", fitX: "number", fitY: "number", @@ -108,9 +110,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @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 scaleFieldKey() { return this.props.scaleField || "scale"; } + private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private easing = () => this.props.Document.panTransformType === "Ease"; private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; private zoomScaling = () => (this.fitToContentScaling / this.parentScaling) * (this.fitToContent ? @@ -239,12 +240,19 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @undoBatch @action internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData, xp: number, yp: number) { - if (linkDragData.linkSourceDocument === this.props.Document) return false; - const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); - this.props.addDocument(source); - linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation"); // TODODO this is where in text links get passed - e.stopPropagation(); - return true; + if (linkDragData.linkSourceDocument === this.props.Document || this.props.Document.annotationOn) return false; + if (!linkDragData.linkSourceDocument.context || StrCast(Cast(linkDragData.linkSourceDocument.context, Doc, null)?.type) === DocumentType.COL) { + // const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); + // this.props.addDocument(source); + // linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation"); // TODODO this is where in text links get passed + return false; + } else { + const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); + this.props.addDocument(source); + linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation"); // TODODO this is where in text links get passed + e.stopPropagation(); + return true; + } } @action @@ -454,7 +462,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P case GestureUtils.Gestures.Stroke: const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); - const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), points, + const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), points, { title: "ink stroke", x: B.x - Number(ActiveInkWidth()) / 2, y: B.y - Number(ActiveInkWidth()) / 2, _width: B.width + Number(ActiveInkWidth()), _height: B.height + Number(ActiveInkWidth()) }); this.addDocument(inkDoc); e.stopPropagation(); @@ -581,6 +589,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onClick = (e: React.MouseEvent) => { if (this.layoutDoc.targetScale && (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { if (Date.now() - this._lastTap < 300) { + runInAction(() => DocumentLinksButton.StartLink = undefined); const docpt = this.getTransform().transformPoint(e.clientX, e.clientY); this.scaleAtPt(docpt, 1); e.stopPropagation(); @@ -822,7 +831,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } if (!this.layoutDoc._lockedTransform || this.Document.inOverlay) { - this.Document.panTransformType = panType; + this.Document._viewTransition = panType; const scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); @@ -850,7 +859,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P scaleAtPt(docpt: number[], scale: number) { const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); - this.Document.panTransformType = "Ease"; + this.Document._viewTransition = "transform 500ms"; this.layoutDoc[this.scaleFieldKey] = scale; const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] }; @@ -895,14 +904,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); - const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document.panTransformType }; + const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition }; // if (!willZoom && DocumentView._focusHack.length) { // Doc.BrushDoc(this.props.Document); // !doc.z && NumCast(this.layoutDoc.scale) < 1 && this.scaleAtPt(DocumentView._focusHack, 1); // [NumCast(doc.x), NumCast(doc.y)], 1); // } else { if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { - if (!doc.z) this.setPan(newPanX, newPanY, "Ease", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow + if (!doc.z) this.setPan(newPanX, newPanY, "transform 500ms", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow } Doc.BrushDoc(this.props.Document); this.props.focus(this.props.Document); @@ -915,7 +924,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.Document._panX = savedState.px; this.Document._panY = savedState.py; this.Document[this.scaleFieldKey] = savedState.s; - this.Document.panTransformType = savedState.pt; + this.Document._viewTransition = savedState.pt; } }, 500); } @@ -1001,7 +1010,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const { z, color, zIndex } = params.pair.layout; return { x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"), - transition: StrCast(layoutDoc.transition), opacity: this.Document.editing ? 1 : Cast(opacity, "number", null), + transition: StrCast(layoutDoc.dataTransition), opacity: this.Document.editing ? 1 : Cast(opacity, "number", null), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number"), pair: params.pair, replica: "" }; } @@ -1128,6 +1137,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); } + this.Document.useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; } @@ -1197,57 +1207,58 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onContextMenu = (e: React.MouseEvent) => { if (this.props.annotationsKey) return; - ContextMenu.Instance.addItem({ - description: (this._timelineVisible ? "Close" : "Open") + " Animation Timeline", event: action(() => { - this._timelineVisible = !this._timelineVisible; - }), icon: this._timelineVisible ? faEyeSlash : faEye - }); + const appearance = ContextMenu.Instance.findByDescription("Appearance..."); + const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; + appearanceItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); + appearanceItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); + appearanceItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); + appearanceItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); + !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); const options = ContextMenu.Instance.findByDescription("Options..."); - const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - - optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); - optionItems.push({ description: "toggle snap line display", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); - optionItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); - optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); - optionItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); - optionItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); - this.props.ContainingCollectionView && optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" }); + const optionItems = options && "subitems" in options ? options.subitems : []; + !this.props.isAnnotationOverlay && + optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: faEye }); + this.props.ContainingCollectionView && + optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" }); + optionItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " snap lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" }); optionItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); - // layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); - optionItems.push({ - description: "Import document", icon: "upload", event: ({ x, y }) => { - const input = document.createElement("input"); - input.type = "file"; - input.accept = ".zip"; - input.onchange = async _e => { - const upload = Utils.prepend("/uploadDoc"); - const formData = new FormData(); - const file = input.files && input.files[0]; - if (file) { - formData.append('file', file); - formData.append('remap', "true"); - const response = await fetch(upload, { method: "POST", body: formData }); - const json = await response.json(); - if (json !== "error") { - const doc = await DocServer.GetRefField(json); - if (doc instanceof Doc) { - const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); - doc.x = xx, doc.y = yy; - this.props.addDocument?.(doc); + if (!Doc.UserDoc().noviceMode) { + optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); + optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); + optionItems.push({ + description: "Import document", icon: "upload", event: ({ x, y }) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".zip"; + input.onchange = async _e => { + const upload = Utils.prepend("/uploadDoc"); + const formData = new FormData(); + const file = input.files && input.files[0]; + if (file) { + formData.append('file', file); + formData.append('remap', "true"); + const response = await fetch(upload, { method: "POST", body: formData }); + const json = await response.json(); + if (json !== "error") { + const doc = await DocServer.GetRefField(json); + if (doc instanceof Doc) { + const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); + doc.x = xx, doc.y = yy; + this.props.addDocument?.(doc); + } } } - } - }; - input.click(); - } - }); - optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); + }; + input.click(); + } + }); + } + !options && ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); } - @observable _timelineVisible = false; + @observable showTimeline = false; intersectRect(r1: { left: number, top: number, width: number, height: number }, r2: { left: number, top: number, width: number, height: number }) { @@ -1321,9 +1332,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P nudge = action((x: number, y: number) => { if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform) { // bcz: this isn't ideal, but want to try it out... this.setPan(NumCast(this.layoutDoc._panX) + this.props.PanelWidth() / 2 * x / this.zoomScaling(), - NumCast(this.layoutDoc._panY) + this.props.PanelHeight() / 2 * (-y) / this.zoomScaling(), "Ease", true); + NumCast(this.layoutDoc._panY) + this.props.PanelHeight() / 2 * (-y) / this.zoomScaling(), "transform 500ms", true); this._nudgeTime = Date.now(); - setTimeout(() => (Date.now() - this._nudgeTime >= 500) && (this.Document.panTransformType = undefined), 500); + setTimeout(() => (Date.now() - this._nudgeTime >= 500) && (this.Document._viewTransition = undefined), 500); return true; } return false; @@ -1342,13 +1353,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} - easing={this.easing} - transition={Cast(this.layoutDoc.transition, "string", null)} + transition={Cast(this.layoutDoc._viewTransition, "string", null)} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> {this.children} </CollectionFreeFormViewPannableContents> - {this._timelineVisible ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} + {this.showTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} </MarqueeView>; } @@ -1371,9 +1381,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onExternalDrop.bind(this)} - onDragOver={e => { - e.preventDefault(); - }} + onDragOver={e => e.preventDefault()} onContextMenu={this.onContextMenu} style={{ pointerEvents: this.backgroundEvents ? "all" : undefined, @@ -1424,7 +1432,6 @@ interface CollectionFreeFormViewPannableContentsProps { panX: () => number; panY: () => number; zoomScaling: () => number; - easing: () => boolean; viewDefDivClick?: ScriptField; children: () => JSX.Element[]; transition?: string; @@ -1433,7 +1440,7 @@ interface CollectionFreeFormViewPannableContentsProps { @observer class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{ render() { - const freeformclass = "collectionfreeformview" + (this.props.viewDefDivClick ? "-viewDef" : (this.props.easing() ? "-ease" : "-none")); + const freeformclass = "collectionfreeformview" + (this.props.viewDefDivClick ? "-viewDef" : "-none"); const cenx = this.props.centeringShiftX(); const ceny = this.props.centeringShiftY(); const panx = -this.props.panX(); diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss index a7f4d4e53..a9fab4c1e 100644 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss +++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss @@ -1,5 +1,10 @@ .antimodeMenu-button { - .color-preview { + .color-previewI { + width: 100%; + height: 40%; + } + + .color-previewII { width: 100%; height: 100%; } diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx index ae82c6a65..f1032adaa 100644 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx @@ -3,25 +3,44 @@ import AntimodeMenu from "../../AntimodeMenu"; import { observer } from "mobx-react"; import { observable, action, computed } from "mobx"; import "./InkOptionsMenu.scss"; -import { ActiveInkColor, ActiveInkBezierApprox, SetActiveInkWidth, SetActiveInkColor, SetActiveBezierApprox } from "../../InkingStroke"; +import { ActiveInkColor, ActiveInkBezierApprox, ActiveFillColor, ActiveArrowStart, ActiveArrowEnd, SetActiveInkWidth, SetActiveInkColor, SetActiveBezierApprox, SetActiveFillColor, SetActiveArrowStart, SetActiveArrowEnd, ActiveDash, SetActiveDash } from "../../InkingStroke"; import { Scripting } from "../../../util/Scripting"; import { InkTool } from "../../../../fields/InkField"; import { ColorState } from "react-color"; import { Utils } from "../../../../Utils"; import GestureOverlay from "../../GestureOverlay"; import { Doc } from "../../../../fields/Doc"; +import { SelectionManager } from "../../../util/SelectionManager"; +import { DocumentView } from "../../../views/nodes/DocumentView"; +import { Document } from "../../../../fields/documentSchemas"; +import { DocumentType } from "../../../documents/DocumentTypes"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faArrowsAlt, faHighlighter, faLink, faPaintRoller, faSleigh, faBars, faFillDrip, faBrush, faPenNib, faShapes, faArrowLeft, faEllipsisH, faBezierCurve, } from "@fortawesome/free-solid-svg-icons"; + +library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faArrowsAlt, faHighlighter, faLink, faPaintRoller, faBars, faFillDrip, faBrush, faPenNib, faShapes, faArrowLeft, faEllipsisH, faBezierCurve); @observer export default class InkOptionsMenu extends AntimodeMenu { static Instance: InkOptionsMenu; - private _palette = ["D0021B", "F5A623", "F8E71C", "8B572A", "7ED321", "417505", "9013FE", "4A90E2", "50E3C2", "B8E986", "000000", "4A4A4A", "9B9B9B", "FFFFFF"]; - private _width = ["1", "5", "10", "100", "200", "300"]; - private _buttons = ["circle", "triangle", "rectangle", "arrow", "line"]; - private _icons = ["O", "∆", "ãƒ", "âžœ", "-"]; + private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", "none"]; + private _width = ["1", "5", "10", "100"]; + // private _buttons = ["circle", "triangle", "rectangle", "arrow", "line"]; + // private _icons = ["O", "∆", "ãƒ", "âžœ", "-"]; + private _buttons = ["circle", "triangle", "rectangle", "line", "noRec", "",]; + private _icons = ["O", "∆", "ãƒ", "⎯", "✖︎", " "]; + //arrowStart and arrowEnd must match and defs must exist in Inking Stroke + private _arrowStart = ["arrowHead", "arrowHead", "dot", "dot", "none"]; + private _arrowEnd = ["none", "arrowEnd", "none", "dot", "none"]; + private _arrowIcons = ["→", "↔︎", "•", "••", " "]; @observable _colorBtn = false; @observable _widthBtn = false; + @observable _fillBtn = false; + @observable _arrowBtn = false; + @observable _dashBtn = false; + @observable _shapeBtn = false; constructor(props: Readonly<{}>) { super(props); @@ -29,18 +48,106 @@ export default class InkOptionsMenu extends AntimodeMenu { this._canFade = false; // don't let the inking menu fade away } + getColors = () => { + return this._palette; + } + @action - changeColor = (color: string) => { + changeArrow = (arrowStart: string, arrowEnd: string) => { + SetActiveArrowStart(arrowStart); + SetActiveArrowEnd(arrowEnd); + } + + @action + changeColor = (color: string, type: string) => { const col: ColorState = { hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" }, rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "", }; - SetActiveInkColor(Utils.colorString(col)); + if (type === "color") { + SetActiveInkColor(Utils.colorString(col)); + } else if (type === "fill") { + SetActiveFillColor(Utils.colorString(col)); + } } @action + editProperties = (value: any, field: string) => { + SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { + const doc = Document(element.rootDoc); + if (doc.type === DocumentType.INK) { + switch (field) { + case "width": + doc.strokeWidth = Number(value); + break; + case "color": + doc.color = String(value); + break; + case "fill": + doc.fillColor = String(value); + break; + case "bezier": + // doc.strokeBezier === 300 ? doc.strokeBezier = 0 : doc.strokeBezier = 300; + break; + case "arrowStart": + doc.arrowStart = String(value); + break; + case "arrowEnd": + doc.arrowEnd = String(value); + break; + case "dash": + doc.dash = Number(value); + default: + break; + } + } + })); + } + + + @action changeBezier = (e: React.PointerEvent): void => { SetActiveBezierApprox(!ActiveInkBezierApprox() ? "300" : ""); + this.editProperties(0, "bezier"); + } + @action + changeDash = (e: React.PointerEvent): void => { + SetActiveDash(ActiveDash() === "0" ? "2" : "0"); + this.editProperties(ActiveDash(), "dash"); + } + + @computed get arrowPicker() { + var currIcon; + for (var i = 0; i < this._arrowStart.length; i++) { + if (this._arrowStart[i] === ActiveArrowStart() && this._arrowEnd[i] === ActiveArrowEnd()) { + currIcon = this._arrowIcons[i]; + if (this._arrowIcons[i] === " ") { + currIcon = "➤"; + } + } + } + var arrowPicker = <button + className="antimodeMenu-button" + key="arrow" + onPointerDown={action(e => this._arrowBtn = !this._arrowBtn)} + style={{ backgroundColor: this._arrowBtn ? "121212" : "" }}> + {currIcon} + </button>; + if (this._arrowBtn) { + arrowPicker = <div className="btn2-group" key="arrows"> + {arrowPicker} + {this._arrowStart.map((arrowStart, i) => { + return <button + className="antimodeMenu-button" + key={arrowStart} + onPointerDown={action(() => { SetActiveArrowStart(arrowStart); SetActiveArrowEnd(this._arrowEnd[i]); this.editProperties(arrowStart, "arrowStart"), this.editProperties(this._arrowEnd[i], "arrowEnd"); this._arrowBtn = false; })} + style={{ backgroundColor: this._arrowBtn ? "121212" : "" }}> + {this._arrowIcons[i]} + </button>; + })} + </div>; + } + return arrowPicker; } @computed get widthPicker() { @@ -49,7 +156,7 @@ export default class InkOptionsMenu extends AntimodeMenu { key="width" onPointerDown={action(e => this._widthBtn = !this._widthBtn)} style={{ backgroundColor: this._widthBtn ? "121212" : "" }}> - W + <FontAwesomeIcon icon="bars" size="lg" /> </button>; if (this._widthBtn) { widthPicker = <div className="btn2-group" key="width"> @@ -58,7 +165,7 @@ export default class InkOptionsMenu extends AntimodeMenu { return <button className="antimodeMenu-button" key={wid} - onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; })} + onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })} style={{ backgroundColor: this._widthBtn ? "121212" : "" }}> {wid} </button>; @@ -68,6 +175,8 @@ export default class InkOptionsMenu extends AntimodeMenu { return widthPicker; } + + @computed get colorPicker() { var colorPicker = <button className="antimodeMenu-button" @@ -75,7 +184,9 @@ export default class InkOptionsMenu extends AntimodeMenu { title="colorChanger" onPointerDown={action(e => this._colorBtn = !this._colorBtn)} style={{ backgroundColor: this._colorBtn ? "121212" : "" }}> - <div className="color-preview" style={{ backgroundColor: ActiveInkColor() ?? "121212" }}></div> + <FontAwesomeIcon icon="pen-nib" size="lg" /> + <div className="color-previewI" style={{ backgroundColor: ActiveInkColor() ?? "121212" }}></div> + </button>; if (this._colorBtn) { colorPicker = <div className="btn-group" key="color"> @@ -84,9 +195,10 @@ export default class InkOptionsMenu extends AntimodeMenu { return <button className="antimodeMenu-button" key={color} - onPointerDown={action(() => { this.changeColor(color); this._colorBtn = false; })} + onPointerDown={action(() => { this.changeColor(color, "color"); this._colorBtn = false; this.editProperties(color, "color"); })} style={{ backgroundColor: this._colorBtn ? "121212" : "" }}> - <div className="color-preview" style={{ backgroundColor: color }}></div> + {/* <FontAwesomeIcon icon="pen-nib" size="lg" /> */} + <div className="color-previewII" style={{ backgroundColor: color }}></div> </button>; })} </div>; @@ -94,15 +206,75 @@ export default class InkOptionsMenu extends AntimodeMenu { return colorPicker; } - @computed get shapeButtons() { - return this._buttons.map((btn, i) => <button + @computed get fillPicker() { + var fillPicker = <button className="antimodeMenu-button" - title={`Draw ${btn}`} - key={i} - onPointerDown={action(e => GestureOverlay.Instance.InkShape = btn)} - style={{ backgroundColor: btn === GestureOverlay.Instance.InkShape ? "121212" : "" }}> - {this._icons[i]} - </button>); + key="fill" + title="fillChanger" + onPointerDown={action(e => this._fillBtn = !this._fillBtn)} + style={{ backgroundColor: this._fillBtn ? "121212" : "" }}> + <FontAwesomeIcon icon="fill-drip" size="lg" /> + <div className="color-previewI" style={{ backgroundColor: ActiveFillColor() ?? "121212" }}></div> + </button>; + if (this._fillBtn) { + fillPicker = <div className="btn-group" key="fill"> + {fillPicker} + {this._palette.map(color => { + return <button + className="antimodeMenu-button" + key={color} + onPointerDown={action(() => { this.changeColor(color, "fill"); this._fillBtn = false; this.editProperties(color, "fill"); })} + style={{ backgroundColor: this._fillBtn ? "121212" : "" }}> + <div className="color-previewII" style={{ backgroundColor: color }}></div> + </button>; + })} + + </div>; + } + return fillPicker; + } + + @computed get shapePicker() { + var currIcon; + if (GestureOverlay.Instance.InkShape === "") { + currIcon = <FontAwesomeIcon icon="shapes" size="lg" />; + } else { + for (var i = 0; i < this._icons.length; i++) { + if (GestureOverlay.Instance.InkShape === this._buttons[i]) { + currIcon = this._icons[i]; + } + } + } + var shapePicker = <button + className="antimodeMenu-button" + key="shape" + onPointerDown={action(e => this._shapeBtn = !this._shapeBtn)} + style={{ backgroundColor: this._shapeBtn ? "121212" : "" }}> + {currIcon} + </button>; + if (this._shapeBtn) { + shapePicker = <div className="btn2-group" key="shape"> + {shapePicker} + {this._buttons.map((btn, i) => { + var ttl = btn; + if (btn === "") { + ttl = "no shape"; + } + if (btn === "noRec") { + ttl = "disable shape recognition"; + } + return <button + className="antimodeMenu-button" + title={`Draw ${btn}`} + key={ttl} + onPointerDown={action((e) => { GestureOverlay.Instance.InkShape = btn; this._shapeBtn = false; })} + style={{ backgroundColor: this._shapeBtn ? "121212" : "" }}> + {this._icons[i]} + </button>; + })} + </div>; + } + return shapePicker; } @computed get bezierButton() { @@ -112,17 +284,35 @@ export default class InkOptionsMenu extends AntimodeMenu { key="bezier" onPointerDown={e => this.changeBezier(e)} style={{ backgroundColor: ActiveInkBezierApprox() ? "121212" : "" }}> - B + <FontAwesomeIcon icon="bezier-curve" size="lg" /> + + </button>; + } + + @computed get dashButton() { + return <button + className="antimodeMenu-button" + title="dash changer" + key="dash" + onPointerDown={e => this.changeDash(e)} + style={{ backgroundColor: ActiveDash() !== "0" ? "121212" : "" }}> + <FontAwesomeIcon icon="ellipsis-h" size="lg" /> + </button>; } render() { const buttons = [ - <button className="antimodeMenu-button" title="Drag" key="drag" onPointerDown={e => this.dragStart(e)}> ✜ </button>, - ...this.shapeButtons, + // <button className="antimodeMenu-button" title="Drag" key="drag" onPointerDown={e => this.dragStart(e)}> + // <FontAwesomeIcon icon="arrows-alt" size="lg" /> + // </button>, + this.shapePicker, this.bezierButton, this.widthPicker, this.colorPicker, + this.fillPicker, + this.arrowPicker, + this.dashButton, ]; return this.getElement(buttons); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 5f09fa0ee..099859109 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,6 +1,6 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt } from "../../../../fields/Doc"; +import { Doc, Opt, DocListCast, DataSym } from "../../../../fields/Doc"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; @@ -25,7 +25,7 @@ interface MarqueeViewProps { getContainerTransform: () => Transform; getTransform: () => Transform; activeDocuments: () => Doc[]; - selectDocuments: (docs: Doc[], ink: { Document: Doc, Ink: Map<any, any> }[]) => void; + selectDocuments: (docs: Doc[]) => void; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; nudge: (x: number, y: number) => boolean; @@ -80,11 +80,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }); ContextMenu.Instance.displayMenu(this._downX, this._downY); + e.stopPropagation(); } else if (e.key === ":") { DocUtils.addDocumentCreatorMenuItems(this.props.addLiveTextDocument, this.props.addDocument, x, y); ContextMenu.Instance.displayMenu(this._downX, this._downY); + e.stopPropagation(); + } else if (e.key === "a" && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.props.selectDocuments(this.props.activeDocuments()); + e.stopPropagation(); } else if (e.key === "q" && e.ctrlKey) { e.preventDefault(); (async () => { @@ -108,6 +114,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque y += 40 * this.props.getTransform().Scale; }); })(); + e.stopPropagation(); } else if (e.key === "b" && e.ctrlKey) { e.preventDefault(); navigator.clipboard.readText().then(text => { @@ -118,7 +125,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.pasteTable(ns, x, y); } }); - } else if (!e.ctrlKey) { + e.stopPropagation(); + } else if (!e.ctrlKey && !e.metaKey) { FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : ""; const tbox = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: NumCast(Doc.UserDoc().fontSize), @@ -132,8 +140,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template; } this.props.addLiveTextDocument(tbox); + e.stopPropagation(); } - e.stopPropagation(); } //heuristically converts pasted text into a table. // assumes each entry is separated by a tab @@ -225,7 +233,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // let inkselect = this.ink ? this.marqueeInkSelect(this.ink.inkData) : new Map(); // let inks = inkselect.size ? [{ Document: this.inkDoc, Ink: inkselect }] : []; const docs = mselect.length ? mselect : [this.props.Document]; - this.props.selectDocuments(docs, []); + this.props.selectDocuments(docs); } const hideMarquee = () => { this.hideMarquee(); @@ -250,6 +258,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque e.preventDefault(); } } + clearSelection() { + if (window.getSelection) { window.getSelection()?.removeAllRanges(); } + else if (document.getSelection()) { document.getSelection()?.empty(); } + } setPreviewCursor = action((x: number, y: number, drag: boolean) => { if (drag) { @@ -265,6 +277,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._downX = x; this._downY = y; PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); + this.clearSelection(); } }); @@ -354,7 +367,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque selected.forEach(d => this.props.removeDocument(d)); const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2); this.props.addDocument(newCollection!); - this.props.selectDocuments([newCollection!], []); + this.props.selectDocuments([newCollection!]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } @@ -379,7 +392,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t" ? Docs.Create.StackingDocument : undefined); this.props.addDocument(newCollection); - this.props.selectDocuments([newCollection], []); + this.props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } @@ -492,7 +505,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.props.addDocument(newCollection); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); - setTimeout(() => this.props.selectDocuments([newCollection], []), 0); + setTimeout(() => this.props.selectDocuments([newCollection]), 0); } @undoBatch diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index 9d3d2e592..3e54d001b 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -12,6 +12,8 @@ $lighter-alt-accent: rgb(207, 220, 240); $darker-alt-accent: rgb(178, 206, 248); $intermediate-color: #9c9396; $dark-color: #121721; +$link-color: lightBlue; +$antimodemenu-height: 35px; // fonts $sans-serif: "Noto Sans", sans-serif; @@ -40,4 +42,5 @@ $MAX_ROW_HEIGHT: 44px; MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE; MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT; SEARCH_THUMBNAIL_SIZE: $search-thumnail-size; + ANTIMODEMENU_HEIGHT: $antimodemenu-height; }
\ No newline at end of file diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/globalCssVariables.scss.d.ts index d95cec9d8..a7ca4b300 100644 --- a/src/client/views/globalCssVariables.scss.d.ts +++ b/src/client/views/globalCssVariables.scss.d.ts @@ -5,6 +5,7 @@ interface IGlobalScss { MINIMIZED_ICON_SIZE: string; MAX_ROW_HEIGHT: string; SEARCH_THUMBNAIL_SIZE: string; + ANTIMODEMENU_HEIGHT: string; } declare const globalCssVariables: IGlobalScss; diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 7dee22f66..c372e7098 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -8,6 +8,10 @@ .linkMenu-list { max-height: 200px; overflow-y: scroll; + position: absolute; + z-index: 10; + background: $link-color; + min-width: 150px } .linkMenu-group { diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 786d6be47..0fcc0f0b9 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -4,12 +4,12 @@ import { DocumentView } from "../nodes/DocumentView"; import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); -import { Doc } from "../../../fields/Doc"; +import { Doc, Opt } from "../../../fields/Doc"; import { LinkManager } from "../../util/LinkManager"; import { LinkMenuGroup } from "./LinkMenuGroup"; import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { library } from "@fortawesome/fontawesome-svg-core"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { DocumentLinksButton } from "../nodes/DocumentLinksButton"; library.add(faTrash); @@ -17,16 +17,29 @@ interface Props { docView: DocumentView; changeFlyout: () => void; addDocTab: (document: Doc, where: string) => boolean; + location: number[]; } @observer export class LinkMenu extends React.Component<Props> { @observable private _editingLink?: Doc; + @observable private _linkMenuRef: Opt<HTMLDivElement | null>; @action + onClick = (e: PointerEvent) => { + if (!Array.from(this._linkMenuRef?.getElementsByTagName((e.target as HTMLElement).tagName) || []).includes(e.target as any)) { + DocumentLinksButton.EditLink = undefined; + } + } + @action componentDidMount() { this._editingLink = undefined; + document.addEventListener("pointerdown", this.onClick); + } + + componentWillUnmount() { + document.removeEventListener("pointerdown", this.onClick); } clearAllLinks = () => { @@ -57,20 +70,11 @@ export class LinkMenu extends React.Component<Props> { render() { const sourceDoc = this.props.docView.props.Document; const groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc); - if (this._editingLink === undefined) { - return ( - <div className="linkMenu"> - {/* <button className="linkEditor-button linkEditor-clearButton" onClick={() => this.clearAllLinks()} title="Clear all links"><FontAwesomeIcon icon="trash" size="sm" /></button> */} - {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */} - <div className="linkMenu-list"> - {this.renderAllGroups(groups)} - </div> - </div> - ); - } else { - return ( - <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor> - ); - } + return <div className="linkMenu-list" ref={(r) => this._linkMenuRef = r} style={{ left: this.props.location[0], top: this.props.location[1] }}> + {!this._editingLink ? + this.renderAllGroups(groups) : + <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)} /> + } + </div>; } }
\ No newline at end of file diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 17cd33241..edc18b6a9 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -77,7 +77,8 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { } editMoved = (e: PointerEvent) => { - DragManager.StartDocumentDrag([this._editRef.current!], new DragManager.DocumentDragData([this.props.linkDoc]), e.x, e.y); + const dragData = new DragManager.DocumentDragData([this.props.linkDoc]); + DragManager.StartDocumentDrag([this._editRef.current!], dragData, e.x, e.y); return true; } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index f934945a6..a3020f912 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -22,7 +22,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { zIndex?: number; highlight?: boolean; jitterRotation: number; - transition?: string; + dataTransition?: string; fitToBox?: boolean; replica: string; } @@ -60,10 +60,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const someView = Cast(this.props.Document.someView, Doc); const minimap = Cast(this.props.Document.minimap, Doc); if (someView instanceof Doc && minimap instanceof Doc) { - const x = (NumCast(someView._panX) - NumCast(someView._width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap._width) - NumCast(minimap._width) / 2; - const y = (NumCast(someView._panY) - NumCast(someView._height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap._height) - NumCast(minimap._height) / 2; - const w = NumCast(someView._width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width); - const h = NumCast(someView._height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height); + const x = (NumCast(someView._panX) - NumCast(someView._width) / 2 / NumCast(someView._viewScale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap._width) - NumCast(minimap._width) / 2; + const y = (NumCast(someView._panY) - NumCast(someView._height) / 2 / NumCast(someView._viewScale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap._height) - NumCast(minimap._height) / 2; + const w = NumCast(someView._width) / NumCast(someView._viewScale) / NumCast(minimap.fitW) * NumCast(minimap.width); + const h = NumCast(someView._height) / NumCast(someView._viewScale) / NumCast(minimap.fitH) * NumCast(minimap.height); return { x: x, y: y, width: w, height: h }; } } @@ -81,9 +81,15 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF public static setValues(time: number, d: Doc, x?: number, y?: number, opacity?: number) { const timecode = Math.round(time); - Cast(d["x-indexed"], listSpec("number"), [])[timecode] = x as any as number; - Cast(d["y-indexed"], listSpec("number"), [])[timecode] = y as any as number; - Cast(d["opacity-indexed"], listSpec("number"), null)[timecode] = opacity as any as number; + const xindexed = Cast(d["x-indexed"], listSpec("number"), []).slice(); + const yindexed = Cast(d["y-indexed"], listSpec("number"), []).slice(); + const oindexed = Cast(d["opacity-indexed"], listSpec("number"), []).slice(); + xindexed[timecode] = x as any as number; + yindexed[timecode] = y as any as number; + oindexed[timecode] = opacity as any as number; + d["x-indexed"] = new List<number>(xindexed); + d["y-indexed"] = new List<number>(yindexed); + d["opacity-indexed"] = new List<number>(oindexed); } public static updateKeyframe(docs: Doc[], time: number) { const timecode = Math.round(time); @@ -94,14 +100,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF xindexed?.length <= timecode + 1 && xindexed.push(undefined as any as number); yindexed?.length <= timecode + 1 && yindexed.push(undefined as any as number); opacityindexed?.length <= timecode + 1 && opacityindexed.push(undefined as any as number); - doc.transition = "all 1s"; + doc.dataTransition = "all 1s"; }); - setTimeout(() => docs.forEach(doc => doc.transition = "inherit"), 1010); + setTimeout(() => docs.forEach(doc => doc.dataTransition = "inherit"), 1010); } public static gotoKeyframe(docs: Doc[]) { - docs.forEach(doc => doc.transition = "all 1s"); - setTimeout(() => docs.forEach(doc => doc.transition = "inherit"), 1010); + docs.forEach(doc => doc.dataTransition = "all 1s"); + setTimeout(() => docs.forEach(doc => doc.dataTransition = "inherit"), 1010); } public static setupKeyframes(docs: Doc[], timecode: number, progressivize: boolean = false) { @@ -119,7 +125,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF doc.x = ComputedField.MakeInterpolated("x", "activeFrame"); doc.y = ComputedField.MakeInterpolated("y", "activeFrame"); doc.opacity = ComputedField.MakeInterpolated("opacity", "activeFrame"); - doc.transition = "inherit"; + doc.dataTransition = "inherit"; }); } @@ -144,13 +150,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF boxShadow: this.Opacity === 0 ? undefined : // if it's not visible, then no shadow this.layoutDoc.z ? `#9c9396 ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow - this.props.backgroundHalo?.() ? (`${this.props.backgroundColor?.(this.props.Document)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent + this.props.backgroundHalo?.() && this.props.Document.type !== DocumentType.INK ? (`${this.props.backgroundColor?.(this.props.Document)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent this.layoutDoc.isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big StrCast(this.layoutDoc.boxShadow, ""), borderRadius: StrCast(Doc.Layout(this.layoutDoc).borderRounding), outline: this.Highlight ? "orange solid 2px" : "", transform: this.transform, - transition: this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition), + transition: this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition), width: this.props.Document.isInkMask ? 5000 : this.width, height: this.props.Document.isInkMask ? 5000 : this.height, zIndex: this.ZInd, diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index d04da8f5b..137b387c0 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -36,8 +36,11 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document); if (targetDoc) { - if (StrCast(Doc.Layout(view.props.Document).layout).indexOf("FormattedTextBox") !== -1 && FormattedTextBox.HadSelection) { - Doc.Layout(view.props.Document).color = Doc.UserDoc().bacgroundColor; + if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { // this situation typically occurs when you have a link dot + targetDoc.backgroundColor = Doc.UserDoc().backgroundColor; // bcz: don't know how to change the color of an inline template... + } + else if (StrCast(Doc.Layout(view.props.Document).layout).includes("FormattedTextBox") && window.getSelection()?.toString() !== "") { + Doc.Layout(view.props.Document)[Doc.LayoutFieldKey(view.props.Document) + "-color"] = Doc.UserDoc().backgroundColor; } else { Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment } diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss new file mode 100644 index 000000000..484f8c469 --- /dev/null +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -0,0 +1,38 @@ +@import "../globalCssVariables.scss"; + + +.documentLinksButton, +.documentLinksButton-endLink, +.documentLinksButton-startLink { + height: 20px; + width: 20px; + position: absolute; + border-radius: 50%; + opacity: 0.9; + pointer-events: auto; + color: black; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + background: deepskyblue; + transform: scale(1.05); + cursor: default; + } +} +.documentLinksButton { + background-color: $link-color; +} +.documentLinksButton-endLink { + border: red solid 2px; +} +.documentLinksButton-startLink { + border: red solid 2px; + background-color: rgba(255, 192, 203, 0.5); +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx new file mode 100644 index 000000000..4f4f12521 --- /dev/null +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -0,0 +1,104 @@ +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, DocListCast } from "../../../fields/Doc"; +import { emptyFunction, setupMoveUpEvents, returnFalse } from "../../../Utils"; +import { DragManager } from "../../util/DragManager"; +import { UndoManager } from "../../util/UndoManager"; +import './DocumentLinksButton.scss'; +import { DocumentView } from "./DocumentView"; +import React = require("react"); +import { DocUtils } from "../../documents/Documents"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { LinkDocPreview } from "./LinkDocPreview"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +interface DocumentLinksButtonProps { + View: DocumentView; + Offset?: number[]; + AlwaysOn?: boolean; +} +@observer +export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { + private _linkButton = React.createRef<HTMLDivElement>(); + + @action + onLinkButtonMoved = (e: PointerEvent) => { + if (this._linkButton.current !== null) { + const linkDrag = UndoManager.StartBatch("Drag Link"); + this.props.View && DragManager.StartLinkDrag(this._linkButton.current, this.props.View.props.Document, e.pageX, e.pageY, { + dragComplete: dropEv => { + const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop + if (this.props.View && linkDoc) { + !linkDoc.linkRelationship && (Doc.GetProto(linkDoc).linkRelationship = "hyperlink"); + + // we want to allow specific views to handle the link creation in their own way (e.g., rich text makes text hyperlinks) + // the dragged view can regiser a linkDropCallback to be notified that the link was made and to update their data structures + // however, the dropped document isn't so accessible. What we do is set the newly created link document on the documentView + // The documentView passes a function prop returning this link doc to its descendants who can react to changes to it. + dropEv.linkDragData?.linkDropCallback?.(dropEv.linkDragData); + runInAction(() => this.props.View._link = linkDoc); + setTimeout(action(() => this.props.View._link = undefined), 0); + } + linkDrag?.end(); + }, + hideSource: false + }); + return true; + } + return false; + } + + @observable static StartLink: DocumentView | undefined; + onLinkButtonDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { + if (doubleTap) { + DocumentLinksButton.StartLink = this.props.View; + } else { + DocumentLinksButton.EditLink = this.props.View; + DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY]; + } + })); + } + completeLink = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e, doubleTap) => { + if (doubleTap) { + if (DocumentLinksButton.StartLink === this.props.View) { + DocumentLinksButton.StartLink = undefined; + } else { + DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View && + DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); + } + } + })); + } + + @observable + public static EditLink: DocumentView | undefined; + public static EditLinkLoc: number[] = [0, 0]; + + @computed + get linkButton() { + const links = DocListCast(this.props.View.props.Document.links); + return (!links.length || links[0].hidden) && !this.props.AlwaysOn ? (null) : + <div title="Drag(create link) Tap(view links)" ref={this._linkButton} style={{ minWidth: 20, minHeight: 20, position: "absolute", left: this.props.Offset?.[0] }}> + <div className={"documentLinksButton"} style={{ backgroundColor: DocumentLinksButton.StartLink ? "transparent" : "" }} + onPointerDown={this.onLinkButtonDown} + onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} + onPointerEnter={action(e => links.length && (LinkDocPreview.LinkInfo = { + addDocTab: this.props.View.props.addDocTab, + linkSrc: this.props.View.props.Document, + linkDoc: links[0], + Location: [e.clientX, e.clientY + 20] + }))} > + {links.length ? links.length : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />} + </div> + {DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"} onPointerDown={this.completeLink} /> : (null)} + {DocumentLinksButton.StartLink === this.props.View ? <div className={"documentLinksButton-startLink"} /> : (null)} + </div>; + } + render() { + return this.linkButton; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index b7726f7ba..b978f6245 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -42,6 +42,17 @@ width:10px !important; } } + .documentView-treeView { + max-height: 1.5em; + text-overflow: ellipsis; + display: inline-block; + white-space: pre; + width: 100%; + overflow: hidden; + > .documentView-node { + position: absolute; + } + } .documentView-lock { width: 20; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 3205e3050..3a3bef2e0 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -41,6 +41,7 @@ import "./DocumentView.scss"; import { LinkAnchorBox } from './LinkAnchorBox'; import { RadialMenu } from './RadialMenu'; import React = require("react"); +import { DocumentLinksButton } from './DocumentLinksButton'; library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight, fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, @@ -68,7 +69,7 @@ export interface DocumentViewProps { onDoubleClick?: ScriptField; onPointerDown?: ScriptField; onPointerUp?: ScriptField; - treeViewId?: string; + treeViewDoc?: Doc; dropAction?: dropActionType; dragDivName?: string; nudge?: (x: number, y: number) => void; @@ -90,6 +91,7 @@ export interface DocumentViewProps { pinToPres: (document: Doc) => void; backgroundHalo?: () => boolean; backgroundColor?: (doc: Doc) => string | undefined; + forcedBackgroundColor?: (doc: Doc) => string | undefined; opacity?: () => number | undefined; ChromeHeight?: () => number; dontRegisterView?: boolean; @@ -121,7 +123,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @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 || Cast(this.layoutDoc.onClick, ScriptField, null) || this.Document.onClick; } + @computed get onClickHandler() { return this.props.onClick || Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } @computed get onDoubleClickHandler() { return this.props.onDoubleClick || Cast(this.layoutDoc.onDoubleClick, ScriptField, null) || this.Document.onDoubleClick; } @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; } @@ -233,10 +235,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; dragData.removeDocument = this.props.removeDocument; - dragData.moveDocument = this.props.moveDocument;// this.Document.onDragStart ? undefined : this.props.moveDocument; + dragData.moveDocument = this.props.moveDocument;// this.layoutDoc.onDragStart ? undefined : this.props.moveDocument; dragData.dragDivName = this.props.dragDivName; - dragData.treeViewId = this.props.treeViewId; - DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.Document.onDragStart }); + dragData.treeViewDoc = this.props.treeViewDoc; + DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.layoutDoc.onDragStart }); } } @@ -323,20 +325,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const alias = Doc.MakeAlias(this.props.Document); DocUtils.makeCustomViewClicked(alias, undefined, "onClick"); this.props.addDocTab(alias, "onRight"); - // UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"); - //ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click"); } else if (this.props.Document.links && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { DocListCast(this.props.Document.links).length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey); } else { - if ((this.props.Document.onDragStart || (this.props.Document.rootDocument)) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTEmplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part + if ((this.layoutDoc.onDragStart || (this.props.Document.rootDocument)) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTEmplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template } else { - // if (this.props.Document.type === DocumentType.RTF) { - // DocumentView._focusHack = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY) || [0, 0]; - // DocumentView._focusHack = [DocumentView._focusHack[0] + NumCast(this.props.Document.x), DocumentView._focusHack[1] + NumCast(this.props.Document.y)]; - - // this.props.focus(this.props.Document, false); - // } SelectionManager.SelectDoc(this, e.ctrlKey || e.shiftKey); } preventDefault = false; @@ -380,7 +374,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._downX = touch.clientX; this._downY = touch.clientY; if (!e.nativeEvent.cancelBubble) { - if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); + if ((this.active || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc.lockedPosition && !this.layoutDoc.inOverlay) e.stopPropagation(); this.removeMoveListeners(); this.addMoveListeners(); this.removeEndListeners(); @@ -395,11 +389,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (e.cancelBubble && this.active) { this.removeMoveListeners(); } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc.lockedPosition && !this.layoutDoc.inOverlay) { const touch = me.touchEvent.changedTouches.item(0); if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) { - if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) { + if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) { this.cleanUpInteractions(); this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined); } @@ -512,15 +506,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } this._downX = e.clientX; this._downY = e.clientY; - if ((!e.nativeEvent.cancelBubble || this.onClickHandler || this.Document.onDragStart) && + if ((!e.nativeEvent.cancelBubble || this.onClickHandler || this.layoutDoc.onDragStart) && // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking !((this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0))) { - if ((this.active || this.Document.onDragStart) && + if ((this.active || this.layoutDoc.onDragStart) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && - !this.Document.inOverlay) { + !this.layoutDoc.inOverlay) { e.stopPropagation(); - if (SelectionManager.IsSelected(this, true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it + if (SelectionManager.IsSelected(this, true) && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it } document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -538,9 +532,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (e.cancelBubble && this.active) { document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart) && !this.Document.lockedPosition && !this.Document.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.layoutDoc.onDragStart) && !this.layoutDoc.lockedPosition && !this.layoutDoc.inOverlay) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { + if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && "alias") || (this.props.dropAction || this.Document.dropAction || undefined) as dropActionType); @@ -576,17 +570,28 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @undoBatch - deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument?.(this.props.Document); } + deleteClicked = (): void => { + if (Doc.UserDoc().activeWorkspace === this.props.Document) { + alert("Can't delete the active workspace"); + } else { + SelectionManager.DeselectAll(); + this.props.removeDocument?.(this.props.Document); + } + } @undoBatch toggleLinkButtonBehavior = (): void => { - if (this.Document.isLinkButton || this.Document.onClick || this.Document.ignoreClick) { + if (this.Document.isLinkButton || this.onClickHandler || this.Document.ignoreClick) { this.Document.isLinkButton = false; + const first = DocListCast(this.Document.links).find(d => d instanceof Doc); + first && (first.hidden = false); this.Document.ignoreClick = false; - this.Document.onClick = undefined; + this.Document.onClick = this.layoutDoc.onClick = undefined; } else { this.Document.isLinkButton = true; + const first = DocListCast(this.Document.links).find(d => d instanceof Doc); + first && (first.hidden = true); this.Document.followLinkZoom = false; this.Document.followLinkLocation = undefined; } @@ -596,8 +601,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu toggleFollowInPlace = (): void => { if (this.Document.isLinkButton) { this.Document.isLinkButton = false; + const first = DocListCast(this.Document.links).find(d => d instanceof Doc); + first && (first.hidden = false); } else { this.Document.isLinkButton = true; + const first = DocListCast(this.Document.links).find(d => d instanceof Doc); + first && (first.hidden = true); this.Document.followLinkZoom = true; this.Document.followLinkLocation = "inPlace"; } @@ -607,6 +616,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu toggleFollowOnRight = (): void => { if (this.Document.isLinkButton) { this.Document.isLinkButton = false; + const first = DocListCast(this.Document.links).find(d => d instanceof Doc); + first && (first.hidden = false); } else { this.Document.isLinkButton = true; this.Document.followLinkZoom = false; @@ -619,6 +630,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { + if (this.props.Document === Doc.UserDoc().activeWorkspace) { + alert("linking to document tabs not yet supported. Drop link on document content."); + return; + } if (de.complete.annoDragData) { /// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner e.stopPropagation(); @@ -727,31 +742,29 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.props.contextMenuItems?.().forEach(item => cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); - let options = cm.findByDescription("Options..."); + const options = cm.findByDescription("Options..."); const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); templateDoc && optionItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); - if (!options) { - options = { description: "Options...", subitems: optionItems, icon: "compass" }; - cm.addItem(options); - } + optionItems.push({ description: "Toggle Show Each Link Dot", event: () => this.layoutDoc.showLinks = !this.layoutDoc.showLinks, icon: "eye" }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); const existingOnClick = cm.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.props.Document.layoutKey}")`), icon: "window-restore" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.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.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: this.toggleFollowInPlace, icon: "concierge-bell" }); onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link on Right", event: this.toggleFollowOnRight, icon: "concierge-bell" }); - onClicks.push({ description: this.Document.isLinkButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" }); + onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" }); onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" }); !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); const funcs: ContextMenuProps[] = []; - if (this.Document.onDragStart) { - funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); - funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); - funcs.push({ description: "Drag Document", icon: "edit", event: () => this.Document.onDragStart = undefined }); + if (this.layoutDoc.onDragStart) { + funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); + funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); + funcs.push({ description: "Drag Document", icon: "edit", event: () => this.layoutDoc.onDragStart = undefined }); cm.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" }); } @@ -995,7 +1008,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); @computed get contents() { TraceMobx(); - return (<> + return (<div style={{ position: "absolute", width: "100%", height: "100%" }}> <DocumentContentsView key={1} docFilters={this.props.docFilters} ContainingCollectionView={this.props.ContainingCollectionView} @@ -1008,6 +1021,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu LayoutTemplate={this.props.LayoutTemplate} makeLink={this.makeLink} rootSelected={this.rootSelected} + backgroundHalo={this.props.backgroundHalo} dontRegisterView={this.props.dontRegisterView} fitToBox={this.props.fitToBox} LibraryPath={this.props.LibraryPath} @@ -1032,8 +1046,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu select={this.select} onClick={this.onClickHandler} layoutKey={this.finalLayoutKey} /> - {this.anchors} - </> + {this.layoutDoc.showLinks ? this.anchors : (null)} + {this.props.forcedBackgroundColor?.(this.Document) === "transparent" || this.props.dontRegisterView ? (null) : <DocumentLinksButton View={this} Offset={[-15, 0]} />} + </div> ); } @@ -1056,29 +1071,34 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu anchorPanelHeight = () => this.props.PanelHeight() || 1; @computed get anchors() { TraceMobx(); - return this.layoutDoc.presBox ? (null) : DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => - <DocumentView {...this.props} key={i + 1} - Document={d} - ContainingCollectionView={this.props.ContainingCollectionView} - ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox - PanelWidth={this.anchorPanelWidth} - PanelHeight={this.anchorPanelHeight} - ContentScaling={returnOne} - backgroundColor={returnTransparent} - removeDocument={this.hideLinkAnchor} - pointerEvents={false} - LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} - />); + return (this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots + this.layoutDoc.presBox || // presentationbox nodes + this.props.dontRegisterView ? (null) : // view that are not registered + DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => + <DocumentView {...this.props} key={i + 1} + Document={d} + ContainingCollectionView={this.props.ContainingCollectionView} + ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox + PanelWidth={this.anchorPanelWidth} + PanelHeight={this.anchorPanelHeight} + ContentScaling={returnOne} + dontRegisterView={false} + forcedBackgroundColor={returnTransparent} + removeDocument={this.hideLinkAnchor} + pointerEvents={false} + LayoutTemplate={undefined} + LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} + />); } @computed get innards() { TraceMobx(); - if (!this.props.PanelWidth()) { // this happens when the document is a tree view label - return <div className="documentView-linkAnchorBoxAnchor" > + if (this.props.treeViewDoc && !this.props.LayoutTemplateString) { // this happens when the document is a tree view label (but not an anchor dot) + return <div className="documentView-treeView" style={{ maxWidth: this.props.PanelWidth() || undefined }}> {StrCast(this.props.Document.title)} {this.anchors} </div>; } + const showTitle = StrCast(this.layoutDoc._showTitle); const showTitleHover = StrCast(this.layoutDoc._showTitleHover); const showCaption = StrCast(this.layoutDoc._showCaption); @@ -1140,7 +1160,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu renderLock() { return (this.Document.isBackground !== undefined || this.isSelected(false)) && ((this.Document.type === DocumentType.COL && this.Document._viewType !== CollectionViewType.Pile) || this.Document.type === DocumentType.IMG) && - this.props.renderDepth > 0 && this.props.PanelWidth() > 0 ? + this.props.renderDepth > 0 && !this.props.treeViewDoc ? <div className="documentView-lock" onClick={() => this.toggleBackground(true)}> <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" /> </div> @@ -1150,7 +1170,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu render() { if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null); if (!(this.props.Document instanceof Doc)) return (null); - const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); + const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : this.props.forcedBackgroundColor?.(this.Document) || StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null))); const finalOpacity = this.props.opacity ? this.props.opacity() : opacity; const finalColor = this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor; @@ -1182,12 +1202,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu style={{ transformOrigin: this._animateScalingTo ? "center center" : undefined, transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, - transition: !this._animateScalingTo ? StrCast(this.Document.transition) : this._animateScalingTo < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out", + transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : this._animateScalingTo < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out", pointerEvents: this.ignorePointerEvents ? "none" : undefined, color: StrCast(this.layoutDoc.color, "inherit"), outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, - boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined, + boxShadow: this.Document.isLinkButton && !this.props.dontRegisterView && this.props.forcedBackgroundColor?.(this.Document) !== "transparent" ? + StrCast(this.props.Document._linkButtonShadow, "lightblue 0em 0em 1em") : + this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : + undefined, background: finalColor, opacity: finalOpacity, fontFamily: StrCast(this.Document._fontFamily, "inherit"), diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 305c04a90..c57738361 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -27,6 +27,7 @@ export interface FieldViewProps { LibraryPath: Doc[]; onClick?: ScriptField; dropAction: dropActionType; + backgroundHalo?: () => boolean; docFilters: () => string[]; isSelected: (outsideReaction?: boolean) => boolean; select: (isCtrlPressed: boolean) => void; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 84d49681c..c1c6f6baf 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -396,12 +396,23 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD const aspect = (rotation % 180) ? nativeHeight / nativeWidth : 1; const shift = (rotation % 180) ? (nativeHeight - nativeWidth) * (1 - 1 / aspect) : 0; this.resize(srcpath); + let transformOrigin = "center center"; + let transform = `translate(0%, 0%) rotate(${rotation}deg) scale(${aspect})`; + if (rotation === 90 || rotation === -270) { + transformOrigin = "top left"; + transform = `translate(100%, 0%) rotate(${rotation}deg) scale(${aspect})`; + } else if (rotation === 180) { + transform = `rotate(${rotation}deg) scale(${aspect})`; + } else if (rotation === 270 || rotation === -90) { + transformOrigin = "right top"; + transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`; + } return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget}> <div className="imageBox-fader" > <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={srcpath} - style={{ transform: `scale(${aspect}) translate(0px, ${shift}px) rotate(${rotation}deg)` }} + style={{ transform, transformOrigin }} width={nativeWidth} ref={this._imgRef} onError={this.onError} /> @@ -409,7 +420,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD <img className="imageBox-fadeaway" key={"fadeaway" + this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={fadepath} - style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})`, }} + style={{ transform, transformOrigin }} width={nativeWidth} ref={this._imgRef} onError={this.onError} /></div>} @@ -447,7 +458,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD TraceMobx(); return (<div className={`imageBox`} onContextMenu={this.specificContextMenu} style={{ - transform: this.props.PanelWidth() ? `translate(0px, ${this.ycenter}px)` : `scale(${this.props.ContentScaling()})`, + transform: this.props.PanelWidth() ? undefined : `scale(${this.props.ContentScaling()})`, width: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, height: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, pointerEvents: this.layoutDoc.isBackground ? "none" : undefined, diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 4442ee2eb..d375466c9 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -147,7 +147,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { <td className="keyValueBox-td-value" onClick={(e) => { this._valInput.current!.select(); e.stopPropagation(); }} style={{ width: `${this.splitPercentage}%` }}> <input style={{ width: "100%" }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} /> </td> - </tr> + </tr>; } @action diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index ad9e49369..360ead18e 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -77,7 +77,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument paddingBottom: NumCast(this.layoutDoc._yPadding), whiteSpace: this.layoutDoc._singleLine ? "pre" : "pre-wrap" }} > - {StrCast(this.rootDoc.text, StrCast(this.rootDoc.title))} + {StrCast(this.rootDoc[this.fieldKey], StrCast(this.rootDoc.title))} </div> <div className="labelBox-fieldKeyParams" > {!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)} diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 3c232eff8..d4ab70200 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -17,6 +17,8 @@ import { LinkEditor } from "../linking/LinkEditor"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SelectionManager } from "../../util/SelectionManager"; import { TraceMobx } from "../../../fields/util"; +import { Id } from "../../../fields/FieldSymbols"; +import { LinkDocPreview } from "./LinkDocPreview"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -55,8 +57,8 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch DragManager.StartDocumentDrag([this._ref.current!], dragData, down[0], down[1]); return true; } else if (dragdist > separation) { - this.layoutDoc[this.fieldKey + "_x"] = (pt[0] - bounds.left) / bounds.width * 100; - this.layoutDoc[this.fieldKey + "_y"] = (pt[1] - bounds.top) / bounds.height * 100; + this.rootDoc[this.fieldKey + "_x"] = (pt[0] - bounds.left) / bounds.width * 100; + this.rootDoc[this.fieldKey + "_y"] = (pt[1] - bounds.top) / bounds.height * 100; } } return false; @@ -113,9 +115,10 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch render() { TraceMobx(); - const x = this.props.PanelWidth() > 1 ? NumCast(this.layoutDoc[this.fieldKey + "_x"], 100) : 0; - const y = this.props.PanelWidth() > 1 ? NumCast(this.layoutDoc[this.fieldKey + "_y"], 100) : 0; - const c = StrCast(this.layoutDoc.backgroundColor, "lightblue"); + const small = this.props.PanelWidth() <= 1; // this happens when rendered in a treeView + const x = NumCast(this.rootDoc[this.fieldKey + "_x"], 100); + const y = NumCast(this.rootDoc[this.fieldKey + "_y"], 100); + const c = StrCast(this.layoutDoc._backgroundColor, StrCast(this.layoutDoc.backgroundColor, StrCast(this.dataDoc.backgroundColor, "lightBlue"))); // note this is not where the typical lightBlue default color comes from. See Documents.Create.LinkDocument() const anchor = this.fieldKey === "anchor1" ? "anchor2" : "anchor1"; const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25; @@ -129,12 +132,19 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch </div>} </div> ); - const small = this.props.PanelWidth() <= 1; - return <div className={`linkAnchorBox-cont${small ? "-small" : ""}`} onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu} + return <div className={`linkAnchorBox-cont${small ? "-small" : ""} ${this.rootDoc[Id]}`} + onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} + onPointerEnter={action(e => LinkDocPreview.LinkInfo = { + addDocTab: this.props.addDocTab, + linkSrc: this.props.ContainingCollectionDoc!, + linkDoc: this.rootDoc, + Location: [e.clientX, e.clientY + 20] + })} + onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu} ref={this._ref} style={{ background: c, - left: !small ? `calc(${x}% - 7.5px)` : undefined, - top: !small ? `calc(${y}% - 7.5px)` : undefined, + left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`, + top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`, transform: `scale(${anchorScale / this.props.ContentScaling()})` }} > {!this._editing && !this._forceOpen ? (null) : diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx new file mode 100644 index 000000000..92b443d3b --- /dev/null +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -0,0 +1,107 @@ +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from "mobx-react"; +import wiki from "wikijs"; +import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { Cast, FieldValue, NumCast } from "../../../fields/Types"; +import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { DocumentManager } from "../../util/DocumentManager"; +import { Transform } from "../../util/Transform"; +import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; +import React = require("react"); +import { DocumentView } from './DocumentView'; + +interface Props { + linkDoc?: Doc; + linkSrc?: Doc; + href?: string; + backgroundColor: (doc: Doc) => string; + addDocTab: (document: Doc, where: string) => boolean; + location: number[]; +} +@observer +export class LinkDocPreview extends React.Component<Props> { + @observable public static LinkInfo: Opt<{ linkDoc?: Doc; addDocTab: (document: Doc, where: string) => boolean, linkSrc: Doc; href?: string; Location: number[] }>; + @observable _targetDoc: Opt<Doc>; + @observable _toolTipText = ""; + + componentDidUpdate() { this.updatePreview(); } + componentDidMount() { this.updatePreview(); } + async updatePreview() { + const linkDoc = this.props.linkDoc; + const linkSrc = this.props.linkSrc; + if (this.props.href) { + if (this.props.href.startsWith("https://en.wikipedia.org/wiki/")) { + wiki().page(this.props.href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(action(summary => this._toolTipText = summary.substring(0, 500)))); + } else { + runInAction(() => this._toolTipText = "external => " + this.props.href); + } + } else if (linkDoc && linkSrc) { + const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), linkSrc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc); + const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; + runInAction(() => { + this._toolTipText = ""; + this._targetDoc = target; + if (anchor !== this._targetDoc && anchor && this._targetDoc) { + this._targetDoc._scrollY = NumCast(anchor?.y); + } + }); + } + } + pointerDown = (e: React.PointerEvent) => { + if (this.props.linkDoc && this.props.linkSrc) { + DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.linkSrc, + (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + } else if (this.props.href) { + this.props.addDocTab(Docs.Create.WebDocument(this.props.href, { title: this.props.href, _width: 200, _height: 400, UseCors: true }), "onRight"); + } + } + width = () => Math.min(350, NumCast(this._targetDoc?.[WidthSym](), 350)); + height = () => Math.min(350, NumCast(this._targetDoc?.[HeightSym](), 350)); + @computed get targetDocView() { + return !this._targetDoc ? + <div style={{ pointerEvents: "all", maxWidth: 350, maxHeight: 250, width: "100%", height: "100%", overflow: "hidden" }}> + <div style={{ width: "100%", height: "100%", textOverflow: "ellipsis", }} onPointerDown={this.pointerDown}> + {this._toolTipText} + </div> + </div> : + <ContentFittingDocumentView + Document={this._targetDoc} + LibraryPath={emptyPath} + fitToBox={true} + backgroundColor={this.props.backgroundColor} + moveDocument={returnFalse} + rootSelected={returnFalse} + ScreenToLocalTransform={Transform.Identity} + parentActive={returnFalse} + addDocument={returnFalse} + removeDocument={returnFalse} + addDocTab={returnFalse} + pinToPres={returnFalse} + dontRegisterView={true} + docFilters={returnEmptyFilter} + ContainingCollectionDoc={undefined} + ContainingCollectionView={undefined} + renderDepth={0} + PanelWidth={this.width} + PanelHeight={this.height} + focus={emptyFunction} + whenActiveChanged={returnFalse} + bringToFront={returnFalse} + ContentScaling={returnOne} + NativeWidth={returnZero} + NativeHeight={returnZero} + />; + } + + render() { + return <div className="linkDocPreview" + style={{ + position: "absolute", left: this.props.location[0], + top: this.props.location[1], width: this.width(), height: this.height(), + boxShadow: "black 2px 2px 1em" + }}> + {this.targetDocView} + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 6f18b1321..f55c4f7d6 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -1,7 +1,7 @@ .pdfBox, .pdfBox-interactive { display: inline-block; - position: absolute; + position: relative; height: 100%; width: 100%; overflow: hidden; @@ -15,6 +15,27 @@ z-index: 1; pointer-events: none; + .pdfBox-pageNums { + display: flex; + flex-direction: row; + height: 25px; + position: absolute; + left: 5px; + top: 5px; + .pdfBox-overlayButton-fwd, + .pdfBox-overlayButton-back { + background: #121721; + height: 25px; + width: 25px; + display: flex; + position: relative; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + } + } + .pdfBox-overlayButton { border-bottom-left-radius: 50%; display: flex; @@ -47,26 +68,6 @@ pointer-events: all; } } - .pdfBox-overlayButton-fwd, - .pdfBox-overlayButton-back { - background: #121721; - height: 25px; - width: 25px; - display: flex; - position: relative; - align-items: center; - justify-content: center; - border-radius: 3px; - pointer-events: all; - position: absolute; - top: 5; - } - .pdfBox-overlayButton-fwd { - left: 45; - } - .pdfBox-overlayButton-back { - left: 25; - } .pdfBox-nextIcon, .pdfBox-prevIcon { diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index aabaac39e..6b1c9fcde 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -146,11 +146,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" /> </button> </>; + const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`; + const curPage = this.Document.curPage || 1; return !this.active() ? (null) : (<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true} onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none" }}> <div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> - <button className="pdfBox-overlayButton" title="Open Search Bar" /> + <button className="pdfBox-overlayButton" title={searchTitle} /> <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> <button title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" color="white" /></button> @@ -161,16 +163,22 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="lg" /> </button> </div> - <button className="pdfBox-overlayButton" key="search" onClick={action(() => this._searching = !this._searching)} title="Open Search Bar" style={{ bottom: 0, right: 0 }}> + <button className="pdfBox-overlayButton" key="search" onClick={action(() => { + this._searching = !this._searching; + this.search("mxytzlaf", true); + })} title={searchTitle} style={{ bottom: 0, right: 0 }}> <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="lg" /></div> </button> - <input value={`${(this.Document.curPage || 1)}`} - onChange={e => this.gotoPage(Number(e.currentTarget.value))} - style={{ left: 5, top: 5, height: "20px", width: "20px", position: "absolute", pointerEvents: "all" }} - onClick={action(() => this._pageControls = !this._pageControls)} /> - {this._pageControls ? pageBtns : (null)} + + <div className="pdfBox-pageNums"> + <input value={curPage} + onChange={e => this.gotoPage(Number(e.currentTarget.value))} + style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: "all" }} + onClick={action(() => this._pageControls = !this._pageControls)} /> + {this._pageControls ? pageBtns : (null)} + </div> <div className="pdfBox-settingsCont" key="settings" onPointerDown={(e) => e.stopPropagation()}> <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings" > <div className="pdfBox-settingsButton-arrow" style={{ transform: `scaleX(${this._flyout ? -1 : 1})` }} /> @@ -248,7 +256,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum _pdfjsRequested = false; render() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null); - if (this.props.isSelected() || this.props.renderDepth <= 1 || this.props.Document._scrollY !== undefined) this._everActive = true; + if (this.props.isSelected() || this.props.renderDepth === 0 || this.props.Document._scrollY !== undefined) this._everActive = true; if (pdfUrl && (this._everActive || this.props.Document._scrollTop || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) { if (pdfUrl instanceof PdfField && this._pdf) { return this.renderPdfView; diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index dbc879920..8818d375e 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -62,8 +62,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const lastFrame = Cast(presTargetDoc.lastFrame, "number", null); const curFrame = NumCast(presTargetDoc.currentFrame); if (lastFrame !== undefined && curFrame < lastFrame) { - presTargetDoc.transition = "all 1s"; - setTimeout(() => presTargetDoc.transition = undefined, 1010); + presTargetDoc._viewTransition = "all 1s"; + setTimeout(() => presTargetDoc._viewTransition = undefined, 1010); presTargetDoc.currentFrame = curFrame + 1; } else if (this.childDocs[this.itemIndex + 1] !== undefined) { diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 8912b113c..04ac34cc2 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -62,6 +62,10 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, Sc @observable private _scriptSuggestedParams: any = ""; @observable private _scriptParamsText: any = ""; + constructor(props: any) { + super(props); + } + // vars included in fields that store parameters types and names and the script itself @computed({ keepAlive: true }) get paramsNames() { return this.compileParams.map(p => p.split(":")[0].trim()); } @computed({ keepAlive: true }) get paramsTypes() { return this.compileParams.map(p => p.split(":")[1].trim()); } @@ -135,9 +139,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, Sc @action onFinish = () => { this.rootDoc.layoutKey = "layout"; - this.rootDoc._height = 50; - this.rootDoc._width = 100; - this.dataDoc.documentText = this.rawScript; } // displays error message @@ -158,8 +159,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, Sc params, typecheck: false }); - this.dataDoc.documentText = this.rawScript; - this.dataDoc.data = result.compiled ? new ScriptField(result) : undefined; + this.dataDoc[this.fieldKey] = result.compiled ? new ScriptField(result) : undefined; this.onError(result.compiled ? undefined : result.errors); return result.compiled; } @@ -171,7 +171,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, Sc const bindings: { [name: string]: any } = {}; this.paramsNames.forEach(key => bindings[key] = this.dataDoc[key]); // binds vars so user doesnt have to refer to everything as self.<var> - ScriptCast(this.dataDoc.data, null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, this.onError); + ScriptCast(this.dataDoc[this.fieldKey], null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, this.onError); } } @@ -589,7 +589,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, Sc } // inputs for scripting div (script box, params box, and params column) - @computed({ keepAlive: true }) get renderScriptingInputs() { + @computed get renderScriptingInputs() { + TraceMobx(); // should there be a border? style={{ borderStyle: "groove", borderBlockWidth: "1px" }} // params box on bottom @@ -673,6 +674,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, Sc // renders script UI if _applied = false and params UI if _applied = true render() { + TraceMobx(); return ( <div className={`scriptingBox`} onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined}> diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index b726a6df9..05355caba 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -41,7 +41,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @observable private _url: string = "hello"; @observable private _pressX: number = 0; @observable private _pressY: number = 0; - + private _keyInput = React.createRef<HTMLInputElement>(); private _longPressSecondsHack?: NodeJS.Timeout; private _outerRef = React.createRef<HTMLDivElement>(); private _iframeRef = React.createRef<HTMLIFrameElement>(); @@ -195,9 +195,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum onValueKeyDown = async (e: React.KeyboardEvent) => { if (e.key === "Enter") { - e.stopPropagation(); this.submitURL(); } + e.stopPropagation(); } toggleAnnotationMode = () => { @@ -237,6 +237,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum onDragOver={this.onUrlDragover} onChange={this.onURLChange} onKeyDown={this.onValueKeyDown} + onClick={(e) => { + this._keyInput.current!.select(); + e.stopPropagation(); + }} + ref={this._keyInput} /> <div style={{ display: "flex", diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index df4a05dd7..678494b27 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -92,10 +92,13 @@ left: 10%; } -.formattedTextBox-inner-rounded, -.formattedTextBox-inner { +.formattedTextBox-inner-rounded, .formattedTextBox-inner-rounded-selected, +.formattedTextBox-inner, .formattedTextBox-inner-selected { height: 100%; white-space: pre-wrap; + .ProseMirror:hover { + background: rgba(200,200,200,0.8); + } hr { display: block; unicode-bidi: isolate; @@ -248,22 +251,23 @@ footnote::after { .prosemirror-links { display: none; position: absolute; - background-color: gray; - padding-bottom: 10px; - margin-top: 1em; + background-color: dimgray; + margin-top: 1.5em; z-index: 1; + padding: 5; + border-radius: 2px; } .prosemirror-hrefoptions{ width:0px; border:unset; padding:0px; - } .prosemirror-links a { float: left; color: white; text-decoration: none; + border-radius: 3px; } .prosemirror-links a:hover { @@ -276,11 +280,22 @@ footnote::after { } .ProseMirror { + padding: 0px; + height: max-content; touch-action: none; span { font-family: inherit; } + blockquote { + padding: 10px 10px; + font-size: smaller; + margin: 0; + font-style: italic; + background: lightgray; + border-left: solid 2px dimgray; + } + ol, ul { counter-reset: deci1 0 multi1 0; padding-left: 1em; @@ -290,30 +305,45 @@ footnote::after { margin-left: 1em; font-family: inherit; } - - .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; } - .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;} - .decimal3-ol { counter-reset: deci3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} - .decimal4-ol { counter-reset: deci4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3em;} - .decimal5-ol { counter-reset: deci5; p {display: inline; font-family: inherit} font-size: smaller; } - .decimal6-ol { counter-reset: deci6; p {display: inline; font-family: inherit} font-size: smaller; } - .decimal7-ol { counter-reset: deci7; p {display: inline; font-family: inherit} font-size: smaller; } - - .multi1-ol { counter-reset: multi1; p {display: inline; font-family: inherit} margin-left: 0; padding-left: 1.2em } - .multi2-ol { counter-reset: multi2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1.4em;} - .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} - .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;} - - .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; } - .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; } - .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; } - .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; } - .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; } - .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; } - .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; } + .bullet { p {display: inline-block; font-family: inherit} margin-left: 0; } + .bullet1 { p {display: inline-block; font-family: inherit} } + .bullet2,.bullet3,.bullet4,.bullet5,.bullet6 { p {display: inline-block; font-family: inherit} font-size: smaller; } + + .decimal1-ol { counter-reset: deci1; p {display: inline-block; font-family: inherit} margin-left: 0; } + .decimal2-ol { counter-reset: deci2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 1em;} + .decimal3-ol { counter-reset: deci3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;} + .decimal4-ol { counter-reset: deci4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3em;} + .decimal5-ol { counter-reset: deci5; p {display: inline-block; font-family: inherit} font-size: smaller; } + .decimal6-ol { counter-reset: deci6; p {display: inline-block; font-family: inherit} font-size: smaller; } + .decimal7-ol { counter-reset: deci7; p {display: inline-block; font-family: inherit} font-size: smaller; } + + .multi1-ol { counter-reset: multi1; p {display: inline-block; font-family: inherit} margin-left: 0; padding-left: 1.2em } + .multi2-ol { counter-reset: multi2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 1.4em;} + .multi3-ol { counter-reset: multi3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;} + .multi4-ol { counter-reset: multi4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.4em;} - .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; } - .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; } - .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; } - .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } + .bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " } + + .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content: counter(deci1) ". "; } + .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; vertical-align: top; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; } + .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; vertical-align: top; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; } + .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; vertical-align: top; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; } + .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; vertical-align: top; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; } + .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; vertical-align: top; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; } + .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; vertical-align: top; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; } + + .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; vertical-align: top; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; } + .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; vertical-align: top; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; } + .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; vertical-align: top; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; } + .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; vertical-align: top; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } +} + +.formattedTextBox-inner-rounded-selected, +.formattedTextBox-inner-selected { + .ProseMirror { + padding:10px; + } + .ProseMirror:hover { + background: unset; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 96c5ca58f..90f379525 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { isEqual } from "lodash"; import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { baseKeymap } from "prosemirror-commands"; +import { baseKeymap, selectAll } from "prosemirror-commands"; import { history } from "prosemirror-history"; import { inputRules } from 'prosemirror-inputrules'; import { keymap } from "prosemirror-keymap"; @@ -15,6 +15,7 @@ import { EditorView } from "prosemirror-view"; import { DateField } from '../../../../fields/DateField'; import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclSym } from "../../../../fields/Doc"; import { documentSchema } from '../../../../fields/documentSchemas'; +import applyDevTools = require("prosemirror-dev-tools"); import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { PrefetchProxy } from '../../../../fields/Proxy'; @@ -57,7 +58,6 @@ import { FieldView, FieldViewProps } from "../FieldView"; import "./FormattedTextBox.scss"; import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment'; import React = require("react"); -import { InkingStroke } from '../../InkingStroke'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -102,6 +102,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp public static FocusedBox: FormattedTextBox | undefined; public static SelectOnLoad = ""; + public static PasteOnLoad: ClipboardEvent | undefined; public static SelectOnLoadChar = ""; public static IsFragment(html: string) { return html.indexOf("data-pm-slice") !== -1; @@ -230,7 +231,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp updateTitle = () => { if ((this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.rootDoc.customTitle) { - const str = this._editorView.state.doc.textContent; + let node = this._editorView.state.doc; + while (node.firstChild) node = node.firstChild; + const str = node.textContent; const titlestr = str.substr(0, Math.min(40, str.length)); this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : ""); } @@ -273,6 +276,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const end = this._editorView.state.doc.nodeSize - 2; this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); } + if (FormattedTextBox.PasteOnLoad) { + const pdfDocId = FormattedTextBox.PasteOnLoad.clipboardData?.getData("dash/pdfOrigin"); + const pdfRegionId = FormattedTextBox.PasteOnLoad.clipboardData?.getData("dash/pdfRegion"); + FormattedTextBox.PasteOnLoad = undefined; + setTimeout(() => pdfDocId && pdfRegionId && this.addPdfReference(pdfDocId, pdfRegionId, undefined), 10); + } } adoptAnnotation = (start: number, end: number, mark: Mark) => { const view = this._editorView!; @@ -422,42 +431,66 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; - const funcs: ContextMenuProps[] = []; - this.rootDoc.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); - !this.layoutDoc.isTemplateDoc && funcs.push({ - description: "Convert to use as a style", event: () => { - this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc); - Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc); - }, icon: "eye" + const appearance = ContextMenu.Instance.findByDescription("Appearance..."); + const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; + + const changeItems: ContextMenuProps[] = []; + const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null); + DocListCast(noteTypesDoc?.data).forEach(note => { + changeItems.push({ + description: StrCast(note.title), event: undoBatch(() => { + Doc.setNativeView(this.rootDoc); + DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); + }), icon: "eye" + }); }); - this.layoutDoc.isTemplateDoc && funcs.push({ - description: "Make New Template", event: () => { - const title = this.rootDoc.title as string; - this.rootDoc.layout = (this.layoutDoc as Doc).layout as string; - this.rootDoc.title = this.layoutDoc.isTemplateForField as string; - this.rootDoc.isTemplateDoc = false; - this.rootDoc.isTemplateForField = ""; - this.rootDoc.layoutKey = "layout"; - this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title); - setTimeout(() => { - this.rootDoc._width = this.layoutDoc._width || 300; // the width and height are stored on the template, since we're getting rid of the old template - this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields - this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, "string", null); - }, 10); + changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); + appearanceItems.push({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" }); + const uicontrols: ContextMenuProps[] = []; + uicontrols.push({ description: "Toggle Sidebar", event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" }); + uicontrols.push({ description: "Toggle Dictation Icon", event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); + uicontrols.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" }); + !Doc.UserDoc().noviceMode && uicontrols.push({ + description: "Broadcast Message", event: () => DocServer.GetRefField("rtfProto").then(proto => + proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)), icon: "expand-arrows-alt" + }); + + appearanceItems.push({ description: "UI Controls...", subitems: uicontrols, icon: "asterisk" }); + this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); + Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); + appearanceItems.push({ + description: "Convert to be a template style", event: () => { + if (!this.layoutDoc.isTemplateDoc) { + const title = StrCast(this.rootDoc.title); + this.rootDoc.title = "text"; + this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title); + } else { + const title = StrCast(this.rootDoc.title); + this.rootDoc.title = "text"; + this.rootDoc.layout = (this.layoutDoc as Doc).layout as string; + this.rootDoc.title = this.layoutDoc.isTemplateForField as string; + this.rootDoc.isTemplateDoc = false; + this.rootDoc.isTemplateForField = ""; + this.rootDoc.layoutKey = "layout"; + this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title); + setTimeout(() => { + this.rootDoc._autoHeight = this.layoutDoc._autoHeight; // autoHeight, width and height + this.rootDoc._width = this.layoutDoc._width || 300; // are stored on the template, since we're getting rid of the old template + this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields + this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, "string", null); + }, 10); + } Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc); }, icon: "eye" }); + !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); + + const funcs: ContextMenuProps[] = []; + //funcs.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); funcs.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); funcs.push({ description: "Toggle Single Line", event: () => this.layoutDoc._singleLine = !this.layoutDoc._singleLine, icon: "expand-arrows-alt" }); - const uicontrols: ContextMenuProps[] = []; - uicontrols.push({ description: "Toggle Sidebar", event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" }); - uicontrols.push({ description: "Toggle Dictation Icon", event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); - uicontrols.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" }); - - funcs.push({ description: "UI Controls...", subitems: uicontrols, icon: "asterisk" }); - const highlighting: ContextMenuProps[] = []; ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => highlighting.push({ @@ -474,21 +507,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp funcs.push({ description: "highlighting...", subitems: highlighting, icon: "hand-point-right" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); - - const change = cm.findByDescription("Change Perspective..."); - const changeItems: ContextMenuProps[] = change && "subitems" in change ? change.subitems : []; - - const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null); - DocListCast(noteTypesDoc?.data).forEach(note => { - changeItems.push({ - description: StrCast(note.title), event: undoBatch(() => { - Doc.setNativeView(this.rootDoc); - DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); - }), icon: "eye" - }); - }); - changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); - !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" }); + this._downX = this._downY = Number.NaN; } recordDictation = () => { @@ -589,10 +608,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }; } - makeLinkToSelection(linkId: string, title: string, location: string, targetId: string) { + makeLinkToSelection(linkId: string, title: string, location: string, targetId: string, targetHref?: string) { const state = this._editorView?.state; if (state) { - const href = Utils.prepend("/doc/" + linkId); + const href = targetHref ?? Utils.prepend("/doc/" + linkId); const sel = state.selection; const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); let tr = state.tr.addMark(sel.from, sel.to, splitter); @@ -600,7 +619,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { const allHrefs = [{ href, title, targetId, linkId }]; allHrefs.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.link.name)?.attrs.allHrefs ?? [])); - const link = state.schema.marks.link.create({ href, allHrefs, title, location, linkId, targetId }); + const link = state.schema.marks.link.create({ allHrefs, title, location, linkId }); tr = tr.addMark(pos, pos + node.nodeSize, link); } }); @@ -714,7 +733,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link); - return linkIndex !== -1 && marks[linkIndex].attrs.allRefs.find((item: { href: string }) => scrollToLinkID === item.href.replace(/.*\/doc\//, "")) ? node : undefined; + return linkIndex !== -1 && marks[linkIndex].attrs.allHrefs.find((item: { href: string }) => scrollToLinkID === item.href.replace(/.*\/doc\//, "")) ? node : undefined; }; let start = 0; @@ -845,8 +864,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => { const cbe = event as ClipboardEvent; - const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin"); - const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion"); + const pdfDocId = cbe.clipboardData?.getData("dash/pdfOrigin"); + const pdfRegionId = cbe.clipboardData?.getData("dash/pdfRegion"); + return pdfDocId && pdfRegionId && this.addPdfReference(pdfDocId, pdfRegionId, slice) ? true : false; + } + + addPdfReference = (pdfDocId: string, pdfRegionId: string, slice?: Slice) => { + const view = this._editorView!; if (pdfDocId && pdfRegionId) { DocServer.GetRefField(pdfDocId).then(pdfDoc => { DocServer.GetRefField(pdfRegionId).then(pdfRegion => { @@ -854,17 +878,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp setTimeout(async () => { const targetField = Doc.LayoutFieldKey(pdfDoc); const targetAnnotations = await DocListCastAsync(pdfDoc[DataSym][targetField + "-annotations"]);// bcz: better to have the PDF's view handle updating its own annotations - targetAnnotations?.push(pdfRegion); + if (targetAnnotations) targetAnnotations.push(pdfRegion); + else Doc.AddDocToList(pdfDoc[DataSym], targetField + "-annotations", pdfRegion); }); const link = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: pdfRegion }, "PDF pasted"); if (link) { - cbe.clipboardData!.setData("dash/linkDoc", link[Id]); const linkId = link[Id]; - const frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)); - slice = new Slice(frag, slice.openStart, slice.openEnd); - const tr = view.state.tr.replaceSelection(slice); - view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")); + const quote = view.state.schema.nodes.blockquote.create(); + quote.content = addMarkToFrag(slice?.content || view.state.doc.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)); + const newSlice = new Slice(Fragment.from(quote), slice?.openStart || 0, slice?.openEnd || 0); + if (slice) { + view.dispatch(view.state.tr.replaceSelection(newSlice).scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")); + } else { + selectAll(view.state, (tx: Transaction) => view.dispatch(tx.replaceSelection(newSlice).scrollIntoView())); + + } } } }); @@ -886,7 +915,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type.name === "link"); - const link = view.state.schema.mark(view.state.schema.marks.link, { href: Utils.prepend(`/doc/${linkId}`), location: "onRight", title: title, docref: true }); + const allHrefs = [{ href: Utils.prepend(`/doc/${linkId}`), title, linkId }]; + const link = view.state.schema.mark(view.state.schema.marks.link, { allHrefs, location: "onRight", title, docref: true }); marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link); return node.mark(marks); } @@ -921,11 +951,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); + !Doc.UserDoc().noviceMode && applyDevTools.applyDevTools(this._editorView); const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field); if (startupText) { const { state: { tr }, dispatch } = this._editorView; dispatch(tr.insertText(startupText)); } + (this._editorView as any).TextView = this; } const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad; @@ -996,15 +1028,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (!FormattedTextBox._downEvent) return; FormattedTextBox._downEvent = false; if (!(e.nativeEvent as any).formattedHandled) { + const editor = this._editorView!; FormattedTextBoxComment.textBox = this; - FormattedTextBoxComment.update(this._editorView!, undefined, (e.target as any)?.className === "prosemirror-dropdownlink" ? (e.target as any).href : ""); + const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); + !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); + FormattedTextBoxComment.update(editor, undefined, (e.target as any)?.className === "prosemirror-dropdownlink" ? (e.target as any).href : ""); } (e.nativeEvent as any).formattedHandled = true; if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) { e.stopPropagation(); } - this._downX = this._downY = Number.NaN; } @action @@ -1028,7 +1062,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (coords && coords.left > x && coords.left < x + RichTextMenu.Instance.width && coords.top > y && coords.top < y + RichTextMenu.Instance.height + 50) { y = Math.min(bounds.bottom, window.innerHeight - RichTextMenu.Instance.height); } - RichTextMenu.Instance.jumpTo(x, y); + setTimeout(() => window.document.activeElement === this.ProseRef?.children[0] && RichTextMenu.Instance.jumpTo(x, y), 250); } } onPointerWheel = (e: React.WheelEvent): void => { @@ -1059,48 +1093,42 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } (e.nativeEvent as any).formattedHandled = true; - if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientX - this._downX) < 4) { - this.props.select(e.ctrlKey); - this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false); - } if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events e.stopPropagation(); + this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false); } } // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. - hitBulletTargets(x: number, y: number, select: boolean, highlightOnly: boolean) { + hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean) { clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); - const pos = this._editorView!.posAtCoords({ left: x, top: y }); - if (pos && this.props.isSelected(true)) { - // let beforeEle = document.querySelector("." + hit.className) as Element; // const before = hit ? window.getComputedStyle(hit, ':before') : undefined; - //const node = this._editorView!.state.doc.nodeAt(pos.pos); - const $pos = this._editorView!.state.doc.resolve(pos.pos); - let list_node = $pos.node().type === schema.nodes.list_item ? $pos.node() : undefined; - if ($pos.node().type === schema.nodes.ordered_list) { - for (let off = 1; off < 100; off++) { - const pos = this._editorView!.posAtCoords({ left: x + off, top: y }); - const node = pos && this._editorView!.state.doc.nodeAt(pos.pos); - if (node?.type === schema.nodes.list_item) { - list_node = node; - break; - } + const clickPos = this._editorView!.posAtCoords({ left: x, top: y }); + let olistPos = clickPos?.pos; + if (clickPos && olistPos && this.props.isSelected(true)) { + const clickNode = this._editorView?.state.doc.nodeAt(olistPos); + const nodeBef = this._editorView?.state.doc.nodeAt(Math.max(0, olistPos - 1)); + olistPos = nodeBef?.type === this._editorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos; + let $olistPos = this._editorView?.state.doc.resolve(olistPos); + let olistNode = (nodeBef !== null || clickNode?.type === this._editorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef; + if (olistNode?.type === this._editorView?.state.schema.nodes.list_item) { + if ($olistPos && ($olistPos as any).path.length > 3) { + olistNode = $olistPos.parent; + $olistPos = this._editorView?.state.doc.resolve(($olistPos as any).path[($olistPos as any).path.length - 4]); } } - if (list_node && pos.inside >= 0 && this._editorView!.state.doc.nodeAt(pos.inside)!.attrs.bulletStyle === list_node.attrs.bulletStyle) { - if (select) { - const $olist_pos = this._editorView!.state.doc.resolve($pos.pos - $pos.parentOffset - 1); + const listNode = this._editorView?.state.doc.nodeAt(clickPos.pos); + if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list) { + if (!collapse) { if (!highlightOnly) { - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection($olist_pos))); + this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection($olistPos!))); } - addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); - } else if (Math.abs(pos.pos - pos.inside) < 2) { + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); + } else if (listNode && listNode.type === this._editorView.state.schema.nodes.list_item) { if (!highlightOnly) { - const offset = this._editorView!.state.doc.nodeAt(pos.inside)?.type === schema.nodes.ordered_list ? 1 : 0; - this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.inside + offset, list_node.type, { ...list_node.attrs, visibility: !list_node.attrs.visibility })); - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pos.inside + offset))); + this._editorView.dispatch(this._editorView.state.tr.setNodeMarkup(clickPos.pos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility })); + this._editorView.dispatch(this._editorView.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, clickPos.pos))); } - addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); } } } @@ -1181,6 +1209,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } + ondrop = (eve: React.DragEvent) => { + eve.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash. + } onscrolled = (ev: React.UIEvent) => { this.layoutDoc._scrollTop = this._scrollRef.current!.scrollTop; } @@ -1188,27 +1219,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp tryUpdateHeight(limitHeight?: number) { let scrollHeight = this._ref.current?.scrollHeight; if (this.props.renderDepth && this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight && scrollHeight) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - scrollHeight = scrollHeight * NumCast(this.layoutDoc.scale, 1); + scrollHeight = scrollHeight * NumCast(this.layoutDoc._viewScale, 1); if (limitHeight && scrollHeight > limitHeight) { scrollHeight = limitHeight; this.layoutDoc.limitHeight = undefined; this.layoutDoc._autoHeight = false; } - const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.dataDoc._nativeHeight, 0); + const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight, 0); const dh = NumCast(this.rootDoc._height, 0); const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); - if (Math.abs(newHeight - dh) > 1) { // bcz: Argh! without this, we get into a React crash if the same document is opened in a freeform view and in the treeview. no idea why, but after dragging the freeform document, selecting it, and selecting text, it will compute to 1 pixel higher than the treeview which causes a cycle - if (this.rootDoc !== this.layoutDoc.doc && !this.layoutDoc.resolvedDataDoc) { - // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... - console.log("Delayed height adjustment..."); - setTimeout(() => { - this.rootDoc._height = newHeight; - this.dataDoc._nativeHeight = nh ? scrollHeight : undefined; - }, 10); - } else { + if (this.rootDoc !== this.layoutDoc.doc && !this.layoutDoc.resolvedDataDoc) { + // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... + console.log("Delayed height adjustment..."); + setTimeout(() => { this.rootDoc._height = newHeight; - this.dataDoc._nativeHeight = nh ? scrollHeight : undefined; - } + this.layoutDoc._nativeHeight = nh ? scrollHeight : undefined; + }, 10); + } else { + this.layoutDoc._height = newHeight; + this.layoutDoc._nativeHeight = nh ? scrollHeight : undefined; } } } @@ -1219,7 +1248,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); } render() { TraceMobx(); - const scale = this.props.ContentScaling() * NumCast(this.layoutDoc.scale, 1); + const scale = this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground; if (this.props.isSelected()) { @@ -1227,16 +1256,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } else if (FormattedTextBoxComment.textBox === this) { setTimeout(() => FormattedTextBoxComment.Hide(), 0); } + const selPad = this.props.isSelected() ? -10 : 0; + const selclass = this.props.isSelected() ? "-selected" : ""; return ( - <div className={"formattedTextBox-cont"} style={{ - transform: `scale(${scale})`, - transformOrigin: "top left", - width: `${100 / scale}%`, - height: `calc(${100 / scale}% - ${this.props.ChromeHeight?.() || 0}px)`, - ...this.styleFromLayoutString(scale) - }}> + <div className={"formattedTextBox-cont"} + style={{ + transform: `scale(${scale})`, + transformOrigin: "top left", + width: `${100 / scale}%`, + height: `calc(${100 / scale}% - ${this.props.ChromeHeight?.() || 0}px)`, + ...this.styleFromLayoutString(scale) + }}> <div className={`formattedTextBox-cont`} ref={this._ref} style={{ + overflow: this.layoutDoc._autoHeight ? "hidden" : undefined, width: "100%", height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : undefined, background: Doc.UserDoc().renderStyle === "comic" ? "transparent" : this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""), @@ -1267,12 +1300,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } })} > - <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} onScroll={this.onscrolled} ref={this._scrollRef}> - <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} + <div className={`formattedTextBox-outer`} ref={this._scrollRef} + style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !this.props.isSelected() ? "none" : undefined }} + onScroll={this.onscrolled} onDrop={this.ondrop} > + <div className={`formattedTextBox-inner${rounded}${selclass}`} ref={this.createDropTarget} style={{ - padding: `${NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0)}px`, - pointerEvents: ((this.layoutDoc.isLinkButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined - }} /> + padding: `${Math.max(0, NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0) + selPad)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0) + selPad}px`, + pointerEvents: !this.props.isSelected() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : "all") : undefined + }} + /> </div> {!this.layoutDoc._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ? <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> : diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 7c697033c..90f2c0aa6 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -177,11 +177,12 @@ export class FormattedTextBoxComment { // this checks if the selection is a hyperlink. If so, it displays the target doc's text for internal links, and the url of the target for external links. if (set === "none" && state.selection.$from) { nbef = findStartOfMark(state.selection.$from, view, findLinkMark); - const naft = findEndOfMark(state.selection.$from, view, findLinkMark); + const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef; let child: any = null; state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node)); - const mark = child && findLinkMark(child.marks); - const href = mark?.attrs.allHrefs.find((item: { href: string }) => item.href)?.href || forceUrl; + child = child || (nbef && state.selection.$from.nodeBefore); + const mark = child ? findLinkMark(child.marks) : undefined; + const href = (!mark?.attrs.docref || naft === nbef) && mark?.attrs.allHrefs.find((item: { href: string }) => item.href)?.href || forceUrl; if (forceUrl || (href && child && nbef && naft && mark?.attrs.showPreview)) { FormattedTextBoxComment.tooltipText.textContent = "external => " + href; (FormattedTextBoxComment.tooltipText as any).href = href; @@ -225,7 +226,7 @@ export class FormattedTextBoxComment { docFilters={returnEmptyFilter} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} - renderDepth={1} + renderDepth={0} PanelWidth={() => Math.min(350, NumCast(target._width, 350))} PanelHeight={() => Math.min(250, NumCast(target._height, 250))} focus={emptyFunction} diff --git a/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts index d80e64634..30da91710 100644 --- a/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts +++ b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts @@ -28,7 +28,7 @@ const ALIGN_PATTERN = /(left|right|center|justify)/; // https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js // :: NodeSpec A plain paragraph textblock. Represented in the DOM // as a `<p>` element. -const ParagraphNodeSpec: NodeSpec = { +export const ParagraphNodeSpec: NodeSpec = { attrs: { align: { default: null }, color: { default: null }, diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 0a4c52ef9..1bbcb9fa8 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -1,4 +1,5 @@ -import { chainCommands, exitCode, joinDown, joinUp, lift, selectParentNode, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from "prosemirror-commands"; +import { chainCommands, exitCode, joinDown, joinUp, lift, deleteSelection, joinBackward, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn, newlineInCode } from "prosemirror-commands"; +import { liftTarget } from "prosemirror-transform"; import { redo, undo } from "prosemirror-history"; import { undoInputRule } from "prosemirror-inputrules"; import { Schema } from "prosemirror-model"; @@ -11,21 +12,19 @@ import { Doc, DataSym } from "../../../../fields/Doc"; import { FormattedTextBox } from "./FormattedTextBox"; import { Id } from "../../../../fields/FieldSymbols"; import { Docs } from "../../../documents/Documents"; +import { update } from "lodash"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; export type KeyMap = { [key: string]: any }; -export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string) => { - let fontSize: number | undefined = undefined; +export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string, from?: number, to?: number) => { tx2.doc.descendants((node: any, offset: any, index: any) => { - if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) { + if ((from === undefined || to === undefined || (from <= offset + node.nodeSize && to >= offset)) && (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item)) { const path = (tx2.doc.resolve(offset) as any).path; let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0); if (node.type === schema.nodes.ordered_list) depth++; - fontSize = depth === 1 && node.attrs.setFontSize ? Number(node.attrs.setFontSize) : fontSize; - const fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined; - tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle ? mapStyle : node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks); + tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle || node.attrs.mapStyle, bulletStyle: depth, }, node.marks); } }); return tx2; @@ -45,7 +44,6 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any //History commands bind("Mod-z", undo); - bind("Backspace", undoInputRule); bind("Shift-Mod-z", redo); !mac && bind("Mod-y", redo); @@ -62,7 +60,6 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any bind("Mod-U", toggleMark(schema.marks.underline)); //Commands for lists - bind("Ctrl-.", wrapInList(schema.nodes.bullet_list)); bind("Ctrl-i", wrapInList(schema.nodes.ordered_list)); bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { @@ -178,18 +175,54 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any } }); + // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); + bind("Backspace", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { + if (!deleteSelection(state, (tx: Transaction<Schema<any, any>>) => { + dispatch(updateBullets(tx, schema)); + })) { + if (!joinBackward(state, (tx: Transaction<Schema<any, any>>) => { + dispatch(updateBullets(tx, schema)); + })) { + if (!selectNodeBackward(state, (tx: Transaction<Schema<any, any>>) => { + dispatch(updateBullets(tx, schema)); + })) { + return false; + } + } + } + return true; + }); + + //newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock //command to break line bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { if (addTextOnRight(false)) return true; + const trange = state.selection.$from.blockRange(state.selection.$to); + const path = (state.selection.$from as any).path; + const depth = trange ? liftTarget(trange) : undefined; + const split = path.length > 5 && !path[path.length - 3].textContent && path[path.length - 6].type !== schema.nodes.list_item; + if (split && trange && depth !== undefined && depth !== null) { + dispatch(state.tr.lift(trange, depth)); + return true; + } + const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); - if (!splitListItem(schema.nodes.list_item)(state, dispatch)) { - if (!splitBlockKeepMarks(state, (tx3: Transaction) => { - splitMetadata(marks, tx3); - if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) { - dispatch(tx3); - } + const cr = state.selection.$from.node().textContent.endsWith("\n"); + if (cr || !newlineInCode(state, dispatch)) { + if (!splitListItem(schema.nodes.list_item)(state, (tx2: Transaction) => { + const tx3 = updateBullets(tx2, schema); + marks && tx3.ensureMarks([...marks]); + marks && tx3.setStoredMarks([...marks]); + dispatch(tx3); })) { - return false; + if (!splitBlockKeepMarks(state, (tx3: Transaction) => { + splitMetadata(marks, tx3); + if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) { + dispatch(tx3); + } + })) { + return false; + } } } return true; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 03d393cde..839943aac 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,30 +1,33 @@ import React = require("react"); -import AntimodeMenu from "../../AntimodeMenu"; -import { observable, action, } from "mobx"; +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faBold, faCaretDown, faChevronLeft, faEyeDropper, faHighlighter, faIndent, faItalic, faLink, faPaintRoller, faPalette, faStrikethrough, faSubscript, faSuperscript, faUnderline } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model"; -import { schema } from "./schema_rts"; -import { EditorView } from "prosemirror-view"; +import { lift, wrapIn } from "prosemirror-commands"; +import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos } from "prosemirror-model"; +import { wrapInList } from "prosemirror-schema-list"; import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons"; -import { updateBullets } from "./ProsemirrorExampleTransfer"; -import { FieldViewProps } from "../FieldView"; -import { Cast, StrCast } from "../../../../fields/Types"; -import { FormattedTextBoxProps } from "./FormattedTextBox"; +import { EditorView } from "prosemirror-view"; +import { Doc } from "../../../../fields/Doc"; +import { DarkPastelSchemaPalette, PastelSchemaPalette } from '../../../../fields/SchemaHeaderField'; +import { Cast, StrCast, BoolCast } from "../../../../fields/Types"; import { unimplementedFunction, Utils } from "../../../../Utils"; -import { wrapInList } from "prosemirror-schema-list"; -import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../../../fields/SchemaHeaderField'; -import "./RichTextMenu.scss"; import { DocServer } from "../../../DocServer"; -import { Doc } from "../../../../fields/Doc"; -import { SelectionManager } from "../../../util/SelectionManager"; import { LinkManager } from "../../../util/LinkManager"; -const { toggleMark, setBlockType } = require("prosemirror-commands"); +import { SelectionManager } from "../../../util/SelectionManager"; +import AntimodeMenu from "../../AntimodeMenu"; +import { FieldViewProps } from "../FieldView"; +import { FormattedTextBox, FormattedTextBoxProps } from "./FormattedTextBox"; +import { updateBullets } from "./ProsemirrorExampleTransfer"; +import "./RichTextMenu.scss"; +import { schema } from "./schema_rts"; +import { TraceMobx } from "../../../../fields/util"; +const { toggleMark } = require("prosemirror-commands"); library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller); + @observer export default class RichTextMenu extends AntimodeMenu { static Instance: RichTextMenu; @@ -69,6 +72,7 @@ export default class RichTextMenu extends AntimodeMenu { super(props); RichTextMenu.Instance = this; this._canFade = false; + this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); this.fontSizeOptions = [ { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize }, @@ -104,7 +108,7 @@ export default class RichTextMenu extends AntimodeMenu { { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType }, { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType }, { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType }, - { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType }, + //{ node: undefined, title: "Set list type", label: "Remove", command: this.changeListType }, ]; this.fontColors = [ @@ -143,7 +147,6 @@ export default class RichTextMenu extends AntimodeMenu { this.updateFromDash(view, lastState, this.editorProps); } - public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => { if (this.view) { const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId }); @@ -161,11 +164,10 @@ export default class RichTextMenu extends AntimodeMenu { return; } this.view = view; - const state = view.state; props && (this.editorProps = props); // Don't do anything if the document/selection didn't change - if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return; + if (lastState?.doc.eq(view.state.doc) && lastState.selection.eq(view.state.selection)) return; // update active marks const activeMarks = this.getActiveMarksOnSelection(); @@ -173,35 +175,36 @@ export default class RichTextMenu extends AntimodeMenu { // update active font family and size const active = this.getActiveFontStylesOnSelection(); - const activeFamilies = active && active.get("families"); - const activeSizes = active && active.get("sizes"); + const activeFamilies = active?.get("families"); + const activeSizes = active?.get("sizes"); - this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; - this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various"; + this.activeFontFamily = !activeFamilies?.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; + this.activeFontSize = !activeSizes?.length ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) : "various"; // update link in current selection const targetTitle = await this.getTextLinkTargetTitle(); this.setCurrentLink(targetTitle); } - setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => { + setMark = (mark: Mark, state: EditorState<any>, dispatch: any, dontToggle: boolean = false) => { if (mark) { const node = (state.selection as NodeSelection).node; if (node?.type === schema.nodes.ordered_list) { let attrs = node.attrs; - if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family }; - if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize }; - if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color }; + if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family }; + if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: `${mark.attrs.fontSize}px` }; + if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color }; const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema); dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from)))); - } else { + } else if (dontToggle) { toggleMark(mark.type, mark.attrs)(state, (tx: any) => { const { from, $from, to, empty } = tx.selection; - // if (!tx.doc.rangeHasMark(from, to, mark.type)) { - // toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch); - // } else - dispatch(tx); + if (!tx.doc.rangeHasMark(from, to, mark.type)) { // hack -- should have just set the mark in the first place + toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch); + } else dispatch(tx); }); + } else { + toggleMark(mark.type, mark.attrs)(state, dispatch); } } } @@ -307,7 +310,6 @@ export default class RichTextMenu extends AntimodeMenu { function onClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.view && command && command(self.view.state, self.view.dispatch, self.view); self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view); self.setActiveMarkButtons(self.getActiveMarksOnSelection()); @@ -369,35 +371,33 @@ export default class RichTextMenu extends AntimodeMenu { } changeFontSize = (mark: Mark, view: EditorView) => { - this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }), view.state, view.dispatch); + this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }), view.state, view.dispatch, true); } changeFontFamily = (mark: Mark, view: EditorView) => { - this.setMark(view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }), view.state, view.dispatch); + this.setMark(view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }), view.state, view.dispatch, true); } // TODO: remove doesn't work //remove all node type and apply the passed-in one to the selected text - changeListType = (nodeType: NodeType | undefined) => { + changeListType = (nodeType: Node | undefined) => { if (!this.view) return; - if (nodeType === schema.nodes.bullet_list) { - wrapInList(nodeType)(this.view.state, this.view.dispatch); - } else { - const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); - if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => { - const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle); - marks && tx3.ensureMarks([...marks]); - marks && tx3.setStoredMarks([...marks]); - - this.view!.dispatch(tx2); - })) { - const tx2 = this.view.state.tr; - const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle); + const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); + if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => { + const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1); + marks && tx3.ensureMarks([...marks]); + marks && tx3.setStoredMarks([...marks]); + + this.view!.dispatch(tx2); + })) { + const tx2 = this.view.state.tr; + if (nodeType && this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list) { + const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view.state.selection.from, this.view.state.selection.to); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); - this.view.dispatch(tx3); + this.view.dispatch(tx3.setSelection(new NodeSelection(tx3.doc.resolve(this.view.state.selection.$from.pos)))); } } } @@ -409,7 +409,17 @@ export default class RichTextMenu extends AntimodeMenu { tr.addMark(state.selection.from, state.selection.to, mark); const content = tr.selection.content(); const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }); - dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); + dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); + return true; + } + + insertBlockquote(state: EditorState<any>, dispatch: any) { + const path = (state.selection.$from as any).path; + if (path.length > 6 && path[path.length - 6].type === schema.nodes.blockquote) { + lift(state, dispatch); + } else { + wrapIn(schema.nodes.blockquote)(state, dispatch); + } return true; } @@ -429,7 +439,6 @@ export default class RichTextMenu extends AntimodeMenu { function onBrushClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.view && self.fillBrush(self.view.state, self.view.dispatch); } @@ -503,13 +512,11 @@ export default class RichTextMenu extends AntimodeMenu { function onColorClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch); } function changeColor(e: React.PointerEvent, color: string) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.setActiveColor(color); self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch); } @@ -545,7 +552,7 @@ export default class RichTextMenu extends AntimodeMenu { dispatch(state.tr.addStoredMark(colorMark)); return false; } - this.setMark(colorMark, state, dispatch); + this.setMark(colorMark, state, dispatch, true); } @action toggleHighlightDropdown() { this.showHighlightDropdown = !this.showHighlightDropdown; } @@ -556,13 +563,11 @@ export default class RichTextMenu extends AntimodeMenu { function onHighlightClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch); } function changeHighlight(e: React.PointerEvent, color: string) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.setActiveHighlight(color); self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch); } @@ -661,15 +666,8 @@ export default class RichTextMenu extends AntimodeMenu { } // TODO: should check for valid URL - makeLinkToURL = (target: String, lcoation: string) => { - if (!this.view) return; - - let node = this.view.state.selection.$from.nodeAfter; - let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location }); - this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link)); - this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link)); - node = this.view.state.selection.$from.nodeAfter; - link = node && node.marks.find(m => m.type.name === "link"); + makeLinkToURL = (target: string, lcoation: string) => { + ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRight", "", target); } deleteLink = () => { @@ -751,7 +749,7 @@ export default class RichTextMenu extends AntimodeMenu { @action toggleMenuPin = (e: React.MouseEvent) => { - this.Pinned = !this.Pinned; + Doc.UserDoc()["menuRichText-pinned"] = this.Pinned = !this.Pinned; if (!this.Pinned) { this.fadeOut(true); } @@ -762,13 +760,14 @@ export default class RichTextMenu extends AntimodeMenu { this.collapsed = !this.collapsed; setTimeout(() => { const x = Math.min(this._left, window.innerWidth - RichTextMenu.Instance.width); - RichTextMenu.Instance.jumpTo(x, this._top); + RichTextMenu.Instance.jumpTo(x, this._top, true); }, 0); } render() { - + TraceMobx(); const row1 = <div className="antimodeMenu-row" key="row1" style={{ display: this.collapsed ? "none" : undefined }}>{[ + !this.collapsed ? this.getDragger() : (null), this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), @@ -779,30 +778,31 @@ export default class RichTextMenu extends AntimodeMenu { this.createHighlighterButton(), this.createLinkButton(), this.createBrushButton(), - this.createButton("indent", "Summarize", undefined, this.insertSummarizer), + this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), + this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), ]}</div>; const row2 = <div className="antimodeMenu-row row-2" key="antimodemenu row2"> + {this.collapsed ? this.getDragger() : (null)} <div key="row" style={{ display: this.collapsed ? "none" : undefined }}> {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"), this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"), this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes")]} </div> <div key="button"> - <div key="collapser"> + {/* <div key="collapser"> <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}> <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} /> </button> - </div> + </div> */} <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}> <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> </button> - {this.getDragger()} </div> </div>; return ( - <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} > {this.getElementWithRows([row1, row2], 2, false)} </div> ); @@ -842,7 +842,6 @@ class ButtonDropdown extends React.Component<ButtonDropdownProps> { onDropdownClick = (e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); - this.props.view && this.props.view.focus(); this.toggleDropdown(); } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 91187edf9..ba3230801 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -59,7 +59,16 @@ export class RichTextRules { ), // * + - create bullet list - wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list), + wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.ordered_list, + // match => { + () => { + return ({ mapStyle: "bullet" }); + // return ({ order: +match[1] }) + }, + (match: any, node: any) => { + return node.childCount + node.attrs.order === +match[1]; + }, + (type: any) => ({ type: type, attrs: { mapStyle: "bullet" } })), // ``` create code block textblockTypeInputRule(/^```$/, schema.nodes.code_block), diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 49d5c96a4..b09ac0678 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -38,7 +38,7 @@ export const marks: { [index: string]: MarkSpec } = { const targetids = node.attrs.allHrefs.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.targetId, ""); const linkids = node.attrs.allHrefs.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.linkId, ""); return node.attrs.docref && node.attrs.title ? - ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] : + ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, href: node.attrs.allHrefs[0].href, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] : node.attrs.allHrefs.length === 1 ? ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}`, href: node.attrs.allHrefs[0].href }, 0] : ["div", { class: "prosemirror-anchor" }, @@ -53,21 +53,45 @@ export const marks: { [index: string]: MarkSpec } = { } }, + /** FONT SIZES */ + pFontSize: { + attrs: { fontSize: { default: 10 } }, + parseDOM: [{ + tag: "span", getAttrs(dom: any) { + return { fontSize: dom.style.fontSize ? Number(dom.style.fontSize.replace("px", "")) : "" }; + } + }], + toDOM: (node) => node.attrs.fontSize ? ['span', { style: `font-size: ${node.attrs.fontSize}px;` }] : ['span', 0] + }, + /* FONTS */ + pFontFamily: { + attrs: { family: { default: "" } }, + parseDOM: [{ + tag: "span", getAttrs(dom: any) { + const cstyle = getComputedStyle(dom); + if (cstyle.font) { + if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" }; + if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" }; + if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" }; + if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" }; + if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" }; + if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" }; + } + } + }], + toDOM: (node) => node.attrs.family ? ['span', { style: `font-family: "${node.attrs.family}";` }] : ['span', 0] + }, // :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text. pFontColor: { - attrs: { - color: { default: "#000" } - }, + attrs: { color: { default: "" } }, inclusive: true, parseDOM: [{ tag: "span", getAttrs(dom: any) { return { color: dom.getAttribute("color") }; } }], - toDOM(node: any) { - return node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0]; - } + toDOM: (node) => node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0] }, marker: { @@ -277,38 +301,4 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [{ tag: "code" }], toDOM() { return codeDOM; } }, - - /* FONTS */ - pFontFamily: { - attrs: { - family: { default: "Crimson Text" }, - }, - parseDOM: [{ - tag: "span", getAttrs(dom: any) { - const cstyle = getComputedStyle(dom); - if (cstyle.font) { - if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" }; - if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" }; - if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" }; - if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" }; - if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" }; - if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" }; - } - } - }], - toDOM: (node) => ['span', { - style: `font-family: "${node.attrs.family}";` - }] - }, - - /** FONT SIZES */ - pFontSize: { - attrs: { - fontSize: { default: 10 } - }, - parseDOM: [{ style: 'font-size: 10px;' }], - toDOM: (node) => ['span', { - style: `font-size: ${node.attrs.fontSize}px;` - }] - }, }; diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index af39ef9c7..afb1f57b7 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -1,7 +1,7 @@ import React = require("react"); import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model"; import { bulletList, listItem, orderedList } from 'prosemirror-schema-list'; -import ParagraphNodeSpec from "./ParagraphNodeSpec"; +import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from "./ParagraphNodeSpec"; const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; @@ -32,13 +32,29 @@ export const nodes: { [index: string]: NodeSpec } = { // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. blockquote: { - content: "block+", + content: "block*", group: "block", defining: true, parseDOM: [{ tag: "blockquote" }], toDOM() { return blockquoteDOM; } }, + + // blockquote: { + // ...ParagraphNodeSpec, + // defining: true, + // parseDOM: [{ + // tag: "blockquote", getAttrs(dom: any) { + // return getParagraphNodeAttrs(dom); + // } + // }], + // toDOM(node: any) { + // const dom = toParagraphDOM(node); + // (dom as any)[0] = 'blockquote'; + // return dom; + // }, + // }, + // :: NodeSpec A horizontal rule (`<hr>`). horizontal_rule: { group: "block", @@ -67,8 +83,8 @@ export const nodes: { [index: string]: NodeSpec } = { // nodes by default. Represented as a `<pre>` element with a // `<code>` element inside of it. code_block: { - content: "text*", - marks: "", + content: "inline*", + marks: "_", group: "block", code: true, defining: true, @@ -218,48 +234,85 @@ export const nodes: { [index: string]: NodeSpec } = { group: 'block', attrs: { bulletStyle: { default: 0 }, - mapStyle: { default: "decimal" }, - setFontSize: { default: undefined }, - setFontFamily: { default: "inherit" }, - setFontColor: { default: "inherit" }, - inheritedFontSize: { default: undefined }, + mapStyle: { default: "decimal" },// "decimal", "multi", "bullet" + fontColor: { default: "inherit" }, + fontSize: { default: undefined }, + fontFamily: { default: undefined }, visibility: { default: true }, indent: { default: undefined } }, + parseDOM: [ + { + tag: "ul", getAttrs(dom: any) { + return { + bulletStyle: dom.getAttribute("data-bulletStyle"), + mapStyle: dom.getAttribute("data-mapStyle"), + fontColor: dom.style.color, + fontSize: dom.style["font-size"], + fontFamily: dom.style["font-family"], + indent: dom.style["margin-left"] + }; + } + }, + { + style: 'list-style-type=disc', getAttrs(dom: any) { + return { mapStyle: "bullet" }; + } + }, + { + tag: "ol", getAttrs(dom: any) { + return { + bulletStyle: dom.getAttribute("data-bulletStyle"), + mapStyle: dom.getAttribute("data-mapStyle"), + fontColor: dom.style.color, + fontSize: dom.style["font-size"], + fontFamily: dom.style["font-family"], + indent: dom.style["margin-left"] + }; + } + }], toDOM(node: Node<any>) { - if (node.attrs.mapStyle === "bullet") return ['ul', 0]; const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ""; - const fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize; - const ffam = node.attrs.setFontFamily; - const color = node.attrs.setFontColor; + const fsize = node.attrs.fontSize ? `font-size: ${node.attrs.fontSize};` : ""; + const ffam = node.attrs.fontFamily ? `font-family:${node.attrs.fontFamily};` : ""; + const fcol = node.attrs.fontColor ? `color: ${node.attrs.fontColor};` : ""; + const marg = node.attrs.indent ? `margin-left: ${node.attrs.indent};` : ""; + if (node.attrs.mapStyle === "bullet") { + return ['ul', { + "data-mapStyle": node.attrs.mapStyle, + "data-bulletStyle": node.attrs.bulletStyle, + style: `${fsize} ${ffam} ${fcol} ${marg}` + }, 0]; + } return node.attrs.visibility ? - ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}; color:${color}; margin-left: ${node.attrs.indent}` }, 0] : + ['ol', { + class: `${map}-ol`, + "data-mapStyle": node.attrs.mapStyle, + "data-bulletStyle": node.attrs.bulletStyle, + style: `list-style: none; ${fsize} ${ffam} ${fcol} ${marg}` + }, 0] : ['ol', { class: `${map}-ol`, style: `list-style: none;` }]; } }, - bullet_list: { - ...bulletList, - content: 'list_item+', - group: 'block', - // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }], - toDOM(node: Node<any>) { - return ['ul', 0]; - } - }, - list_item: { + ...listItem, attrs: { bulletStyle: { default: 0 }, - mapStyle: { default: "decimal" }, + mapStyle: { default: "decimal" }, // "decimal", "multi", "bullet" visibility: { default: true } }, - ...listItem, - content: 'paragraph block*', + content: 'paragraph+ | (paragraph ordered_list)', + parseDOM: [{ + tag: "li", getAttrs(dom: any) { + return { mapStyle: dom.getAttribute("data-mapStyle"), bulletStyle: dom.getAttribute("data-bulletStyle") }; + } + }], toDOM(node: any) { const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ""; - return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."]; - //return ["li", { class: `${map}` }, 0]; + return node.attrs.visibility ? + ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, 0] : + ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, "..."]; } }, };
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/prosemirrorPatches.js b/src/client/views/nodes/formattedText/prosemirrorPatches.js index 269423482..763961958 100644 --- a/src/client/views/nodes/formattedText/prosemirrorPatches.js +++ b/src/client/views/nodes/formattedText/prosemirrorPatches.js @@ -136,4 +136,6 @@ function wrappingInputRule(regexp, nodeType, getAttrs, joinPredicate, customWith (!joinPredicate || joinPredicate(match, before))) { tr.join(start - 1); } return tr }) -}
\ No newline at end of file +} + + diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index cb6a15f36..d29b638e6 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -8,7 +8,6 @@ import { Cast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; import { DocumentManager } from "../../util/DocumentManager"; import PDFMenu from "./PDFMenu"; import "./Annotation.scss"; -import { DocumentView } from "../nodes/DocumentView"; interface IAnnotationProps { anno: Doc; @@ -19,7 +18,9 @@ interface IAnnotationProps { fieldKey: string; } -export default class Annotation extends React.Component<IAnnotationProps> { +@observer +export default + class Annotation extends React.Component<IAnnotationProps> { render() { return DocListCast(this.props.anno.annotations).map(a => ( <RegionAnnotation {...this.props} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />)); diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index ff328068b..6dcf5cce6 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -26,11 +26,13 @@ export default class PDFMenu extends AntimodeMenu { public AddTag: (key: string, value: string) => boolean = returnFalse; public PinToPres: () => void = unimplementedFunction; public Marquee: { left: number; top: number; width: number; height: number; } | undefined; + public get Active() { return this._opacity ? true : false; } constructor(props: Readonly<{}>) { super(props); PDFMenu.Instance = this; + PDFMenu.Instance._canFade = false; } pointerDown = (e: React.PointerEvent) => { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 516774f44..64f150dd5 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -1,5 +1,6 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; +const pdfjs = require('pdfjs-dist/es5/build/pdf.js'); import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import { Dictionary } from "typescript-collections"; @@ -41,7 +42,10 @@ export const pageSchema = createSchema({ serachMatch: "boolean" }); -pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; +//pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; +// The workerSrc property shall be specified. +pdfjsLib.GlobalWorkerOptions.workerSrc = "https://unpkg.com/pdfjs-dist@2.4.456/build/pdf.worker.min.js"; + type PdfDocument = makeInterface<[typeof documentSchema, typeof pageSchema]>; const PdfDocument = makeInterface(documentSchema, pageSchema); @@ -80,7 +84,6 @@ interface IViewerProps { export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocument>(PdfDocument) { static _annotationStyle: any = addStyleSheet(); @observable private _pageSizes: { width: number, height: number }[] = []; - @observable private _annotations: Doc[] = []; @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); @observable private _script: CompiledScript = CompileScript("return true") as CompiledScript; @observable private Index: number = -1; @@ -111,17 +114,15 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu private _downX: number = 0; private _downY: number = 0; private _coverPath: any; + private _viewerIsSetup = false; + private _lastSearch: string = ""; @computed get allAnnotations() { - return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]).filter( - anno => this._script.run({ this: anno }, console.log, true).result); - } - - @computed get nonDocAnnotations() { - return this._annotations.filter(anno => this._script.run({ this: anno }, console.log, true).result); + return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]). + filter(anno => this._script.run({ this: anno }, console.log, true).result); } + @computed get nonDocAnnotations() { return this.allAnnotations.filter(a => a.annotations); } - _lastSearch: string = ""; componentDidMount = async () => { // change the address to be the file address of the PNG version of each page // file address of the pdf @@ -147,7 +148,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } runInAction(() => this._showWaiting = this._showCover = true); this.props.startupLive && this.setupPdfJsViewer(); - this._mainCont.current!.scrollTop = this.layoutDoc._scrollTop || 0; + this._mainCont.current && (this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0); this._searchReactionDisposer = reaction(() => this.Document.searchMatch, search => { if (search) { this.search(Doc.SearchQuery(), true); @@ -160,7 +161,14 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu }, { fireImmediately: true }); this._selectionReactionDisposer = reaction(() => this.props.isSelected(), - () => (SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(), + selected => { + if (!selected) { + this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); + PDFMenu.Instance.fadeOut(true); + } + (SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(); + }, { fireImmediately: true }); this._reactionDisposer = reaction( () => this.Document._scrollY, @@ -216,32 +224,26 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action setupPdfJsViewer = async () => { - this._selectionReactionDisposer && this._selectionReactionDisposer(); - this._selectionReactionDisposer = undefined; + if (this._viewerIsSetup) return; + else this._viewerIsSetup = true; this._showWaiting = true; this.props.setPdfViewer(this); await this.initialLoad(); this._scrollTopReactionDisposer = reaction(() => Cast(this.layoutDoc._scrollTop, "number", null), - (stop) => (stop !== undefined && this.layoutDoc._scrollY === undefined) && (this._mainCont.current!.scrollTop = stop), { fireImmediately: true }); - - this._annotationReactionDisposer = reaction( - () => DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]), - annotations => annotations?.length && (this._annotations = annotations), + (stop) => (stop !== undefined && this.layoutDoc._scrollY === undefined && this._mainCont.current) && (this._mainCont.current.scrollTop = stop), { fireImmediately: true }); this._filterReactionDisposer = reaction( - () => ({ scriptField: Cast(this.Document.filterScript, ScriptField), annos: this._annotations.slice() }), - action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => { + () => Cast(this.Document.filterScript, ScriptField), + action(scriptField => { const oldScript = this._script.originalScript; - this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript; + this._script = scriptField?.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript; if (this._script.originalScript !== oldScript) { this.Index = -1; } - annos.forEach(d => d.opacity = this._script.run({ this: d }, console.log, 1).result ? 1 : 0); }), - { fireImmediately: true } - ); + { fireImmediately: true }); this.createPdfViewer(); } @@ -262,16 +264,18 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } document.removeEventListener("copy", this.copy); document.addEventListener("copy", this.copy); - document.addEventListener("pagesinit", this.pagesinit); - document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false)); + const eventBus = new PDFJSViewer.EventBus(true); + eventBus._on("pagesinit", this.pagesinit); + eventBus._on("pagerendered", action(() => this._showCover = this._showWaiting = false)); const pdfLinkService = new PDFJSViewer.PDFLinkService(); - const pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService }); + const pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService, eventBus }); this._pdfViewer = new PDFJSViewer.PDFViewer({ container: this._mainCont.current, viewer: this._viewer.current, linkService: pdfLinkService, findController: pdfFindController, renderer: "canvas", + eventBus }); pdfLinkService.setViewer(this._pdfViewer); pdfLinkService.setDocument(this.props.pdf, null); @@ -395,7 +399,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu if (!searchString) { fwd ? this.nextAnnotation() : this.prevAnnotation(); } - else if (this._pdfViewer._pageViewsReady) { + else if (this._pdfViewer.pageViewsReady) { this._pdfViewer.findController.executeCommand('findagain', { caseSensitive: false, findPrevious: !fwd, @@ -429,7 +433,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this._downX = e.clientX; this._downY = e.clientY; addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" }); - if ((this.Document.scale || 1) !== 1) return; + if ((this.Document._viewScale || 1) !== 1) return; if ((e.button !== 0 || e.altKey) && this.active(true)) { this._setPreviewCursor?.(e.clientX, e.clientY, true); //e.stopPropagation(); @@ -495,7 +499,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu const annoBox = document.createElement("div"); annoBox.className = "pdfViewer-annotationBox"; // transforms the positions from screen onto the pdf div - annoBox.style.top = ((rect.top - boundingRect.top) * scaleY / this._zoomed + this._mainCont.current.scrollTop).toString(); + annoBox.style.top = ((rect.top - boundingRect.top) * scaleX / this._zoomed + this._mainCont.current.scrollTop).toString(); annoBox.style.left = ((rect.left - boundingRect.left) * scaleX / this._zoomed).toString(); annoBox.style.width = (rect.width * this._mainCont.current.offsetWidth / boundingRect.width / this._zoomed).toString(); annoBox.style.height = (rect.height * this._mainCont.current.offsetHeight / boundingRect.height / this._zoomed).toString(); @@ -637,19 +641,22 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action onZoomWheel = (e: React.WheelEvent) => { - e.stopPropagation(); - if (e.ctrlKey) { - const curScale = Number(this._pdfViewer.currentScaleValue); - this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale - curScale * e.deltaY / 1000)); - this._zoomed = Number(this._pdfViewer.currentScaleValue); + if (this.active()) { + e.stopPropagation(); + if (e.ctrlKey) { + const curScale = Number(this._pdfViewer.currentScaleValue); + this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale - curScale * e.deltaY / 1000)); + this._zoomed = Number(this._pdfViewer.currentScaleValue); + } } } @computed get annotationLayer() { TraceMobx(); return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.Document._nativeHeight), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> - {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) => - <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />)} + {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => + <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) + } </div>; } overlayTransform = () => this.scrollXf().scale(1 / this._zoomed); diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 74262d81a..2436bf418 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -85,7 +85,7 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> { const item = React.createRef<HTMLDivElement>(); return <div className="collection" key={doc.col[Id] + doc.target[Id]} ref={item}> <div className="collection-item" onPointerDown={ - SetupDrag(item, () => doc.col, undefined, undefined, undefined, undefined, () => SearchBox.Instance.closeSearch())}> + SetupDrag(item, () => doc.col, undefined, undefined, () => SearchBox.Instance.closeSearch())}> <FontAwesomeIcon icon={faStickyNote} /> </div> <a onClick={this.getOnClick(doc)}>{doc.col.title}</a> diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 8c8720179..dd7117594 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -973,7 +973,7 @@ export namespace Doc { export function toggleNativeDimensions(layoutDoc: Doc, contentScale: number, panelWidth: number, panelHeight: number) { runInAction(() => { if (layoutDoc._nativeWidth || layoutDoc._nativeHeight) { - layoutDoc.scale = NumCast(layoutDoc.scale, 1) * contentScale; + layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale; layoutDoc._nativeWidth = undefined; layoutDoc._nativeHeight = undefined; } @@ -1159,7 +1159,6 @@ Scripting.addGlobal(function activePresentationItem() { const curPres = Doc.UserDoc().activePresentation as Doc; return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)]; }); -Scripting.addGlobal(function selectDoc(doc: any) { Doc.UserDoc().activeSelection = new List([doc]); }); Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) { const docs = DocListCast(Doc.UserDoc().activeSelection). filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.DOCHOLDER && d.type !== DocumentType.KVP && diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 51a5768bf..7cfd74cc4 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -1,7 +1,7 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { serializable, custom, createSimpleSchema, list, object, map } from "serializr"; import { ObjectField } from "./ObjectField"; -import { Copy, ToScriptString, ToString } from "./FieldSymbols"; +import { Copy, ToScriptString, ToString, Update } from "./FieldSymbols"; export enum InkTool { None = "none", @@ -31,6 +31,8 @@ const strokeDataSchema = createSimpleSchema({ export class InkField extends ObjectField { @serializable(list(object(strokeDataSchema))) readonly inkData: InkData; + // inkData: InkData; + constructor(data: InkData) { super(); diff --git a/src/fields/List.ts b/src/fields/List.ts index fdabea365..a9da75abb 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -291,9 +291,10 @@ class ListImpl<T extends Field> extends ObjectField { this.___fields = value; for (const key in value) { const field = value[key]; - if (!(field instanceof ObjectField)) continue; - (field as ObjectField)[Parent] = this[Self]; - (field as ObjectField)[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]); + if (field instanceof ObjectField) { + field[Parent] = this[Self]; + field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]); + } } } diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts index 9aa1c9b04..92b2cfa60 100644 --- a/src/fields/ObjectField.ts +++ b/src/fields/ObjectField.ts @@ -3,8 +3,8 @@ import { OnUpdate, Parent, Copy, ToScriptString, ToString } from "./FieldSymbols import { Scripting } from "../client/util/Scripting"; export abstract class ObjectField { - protected [OnUpdate](diff?: any) { } - private [Parent]?: RefField | ObjectField; + public [OnUpdate](diff?: any) { } + public [Parent]?: RefField | ObjectField; abstract [Copy](): ObjectField; abstract [ToScriptString](): string; diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index 66959882d..7c7bf3e12 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -256,7 +256,7 @@ export namespace RichTextUtils { }; const list = (schema: any, items: Node[]): Node => { - return schema.node("bullet_list", null, items); + return schema.node("ordered_list", { mapStyle: "bullet" }, items); }; const paragraphNode = (schema: any, runs: docs_v1.Schema$TextRun[]): Node => { diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts index 72bce283d..98ef3e087 100644 --- a/src/fields/Schema.ts +++ b/src/fields/Schema.ts @@ -65,9 +65,8 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu return obj; }; return function (doc?: Doc | Doc[]) { - doc = doc || new Doc; - if (doc instanceof Doc) { - return fn(doc); + if (doc instanceof Doc || doc === undefined) { + return fn(doc || new Doc); } else { return doc.map(fn); } diff --git a/src/fields/util.ts b/src/fields/util.ts index ad7b6ea7a..c4affb2d7 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -10,6 +10,7 @@ import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; import { ScriptCast } from "./Types"; + function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); } @@ -110,7 +111,7 @@ const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHe "LODdisable", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"]; export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; - if (target[AclSym] && !_overrideAcl) return true; + if (target[AclSym] && !_overrideAcl && !DocServer.PlaygroundFields.includes(in_prop.toString())) return true; if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { console.log(prop + " is deprecated - switch to _" + prop); @@ -155,9 +156,6 @@ export function getter(target: any, in_prop: string | symbol | number, receiver: function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any { receiver = receiver || target[SelfProxy]; - if (target === undefined) { - console.log(""); - } let field = target.__fields[prop]; for (const plugin of getterPlugins) { const res = plugin(receiver, prop, field); diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts index d2a9e9cce..e028d628d 100644 --- a/src/server/ApiManagers/PDFManager.ts +++ b/src/server/ApiManagers/PDFManager.ts @@ -2,6 +2,7 @@ import ApiManager, { Registration } from "./ApiManager"; import { Method } from "../RouteManager"; import RouteSubscriber from "../RouteSubscriber"; import { existsSync, createReadStream, createWriteStream } from "fs"; +const pdfjs = require('pdfjs-dist/es5/build/pdf.js'); import * as Pdfjs from 'pdfjs-dist'; import { createCanvas } from "canvas"; const imageSize = require("probe-image-size"); @@ -51,11 +52,13 @@ async function getOrCreateThumbnail(coreFilename: string, pageNum: number, res: } async function CreateThumbnail(coreFilename: string, pageNum: number, res: express.Response, subtree?: string) { - const sourcePath = resolve(pathToDirectory(Directory.pdfs), `${subtree ?? ""}${coreFilename}.pdf`); + const part1 = subtree ?? ""; + const filename = `${part1}${coreFilename}.pdf`; + const sourcePath = resolve(pathToDirectory(Directory.pdfs), filename); const documentProxy = await Pdfjs.getDocument(sourcePath).promise; const factory = new NodeCanvasFactory(); const page = await documentProxy.getPage(pageNum); - const viewport = page.getViewport(1 as any); + const viewport = page.getViewport({ scale: 1, rotation: 0, dontFlip: false }); const { canvas, context } = factory.create(viewport.width, viewport.height); const renderContext = { canvasContext: context, |