diff options
author | Bob Zeleznik <zzzman@gmail.com> | 2020-06-16 19:37:50 -0400 |
---|---|---|
committer | Bob Zeleznik <zzzman@gmail.com> | 2020-06-16 19:37:50 -0400 |
commit | 7b3c348de46b07d3296fff1cafba13e568ef7494 (patch) | |
tree | 9c49bda8737224f41cb1db937d65625211861405 /src | |
parent | b45ed3ca06509318c810e3a51844eacc95cb62e7 (diff) | |
parent | 4a0332994b38c8910077e6cc711e9e78c2964b67 (diff) |
Merge branch 'master' into script_documents
Diffstat (limited to 'src')
43 files changed, 1059 insertions, 1287 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index c6b3fa61f..c7dfb0b23 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,6 +1,6 @@ import * as io from 'socket.io-client'; import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message"; -import { Opt, Doc } from '../fields/Doc'; +import { Opt, Doc, fetchProto } from '../fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../fields/RefField'; @@ -244,7 +244,10 @@ export namespace DocServer { return cached; } else { // CACHED => great, let's just return the cached field we have - return Promise.resolve(cached); + return Promise.resolve(cached).then(field => { + (field instanceof Doc) && fetchProto(field); + return field; + }); } }; const _GetCachedRefFieldImpl = (id: string): Opt<RefField> => { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 03355b487..56ed6febb 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -617,13 +617,15 @@ export namespace Docs { selection: { type: "text", anchor: 1, head: 1 }, storedMarks: [] }; - const field = text ? new RichTextField(JSON.stringify(rtf), text) : undefined; return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); } 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, removeDropProperties: new List(["isBackground", "isLinkButton"]), ...options }); + const doc = InstanceFromProto(Prototypes.get(DocumentType.LINK), undefined, { + isLinkButton: true, treeViewHideTitle: true, treeViewOpen: false, + removeDropProperties: new List(["isBackground", "isLinkButton"]), ...options + }, id); const linkDocProto = Doc.GetProto(doc); linkDocProto.anchor1 = source.doc; linkDocProto.anchor2 = target.doc; @@ -707,7 +709,7 @@ export namespace Docs { } export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, _viewType: CollectionViewType.Schema }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List(schemaColumns.length ? schemaColumns : [new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Schema }); } export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { @@ -839,8 +841,9 @@ export namespace DocUtils { if (sv && sv.props.ContainingCollectionDoc === target.doc) return; if (target.doc === Doc.UserDoc()) return undefined; - const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship }, id); - Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('self.anchor1.title +" (" + (self.linkRelationship||"to") +") " + self.anchor2.title'); + const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship, layoutKey: "layout_linkView" }, id); + linkDoc.layout_linkView = Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null); + Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('self.anchor1?.title +" (" + (self.linkRelationship||"to") +") " + self.anchor2?.title'); Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(self)"); Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(self)"); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 49af892c9..a7ced3965 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -86,6 +86,52 @@ export class CurrentUserUtils { }); } + if (doc["template-button-link"] === undefined) { + 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} 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"); + + const rtf2 = { + doc: { + type: "doc", content: [ + { + type: "paragraph", + content: [{ + type: "dashField", + attrs: { + fieldKey: "src", + hideKey: false + } + }] + }, + { type: "paragraph" }, + { + type: "paragraph", + content: [{ + type: "dashField", + attrs: { + fieldKey: "dst", + hideKey: false + } + }] + }] + }, + selection: { type: "text", anchor: 1, head: 1 }, + storedMarks: [] + }; + linkTemplate.header = new RichTextField(JSON.stringify(rtf2), ""); + + doc["template-button-link"] = CurrentUserUtils.ficon({ + onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory)'), + dragFactory: new PrefetchProxy(linkTemplate) as any as Doc, + removeDropProperties: new List<string>(["dropAction"]), title: "link view", icon: "window-maximize" + }); + } + if (doc["template-button-switch"] === undefined) { const { FreeformDocument, MulticolumnDocument, TextDocument } = Docs.Create; @@ -168,17 +214,21 @@ export class CurrentUserUtils { }); } + const requiredTypes = [ + doc["template-button-slides"] as Doc, + doc["template-button-description"] as Doc, + doc["template-button-query"] as Doc, + doc["template-button-detail"] as Doc, + doc["template-button-link"] as Doc, + doc["template-button-switch"] as Doc]; if (doc["template-buttons"] === undefined) { - doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument([doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc, - doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc, doc["template-button-switch"] as Doc], { + doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument(requiredTypes, { title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", _autoHeight: true, _width: 500, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), })); } else { const curButnTypes = Cast(doc["template-buttons"], Doc, null); - const requiredTypes = [doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc, - doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc, doc["template-button-switch"] as Doc]; DocListCastAsync(curButnTypes.data).then(async curBtns => { await Promise.all(curBtns!); requiredTypes.map(btype => Doc.AddDocToList(curButnTypes, "data", btype)); @@ -510,8 +560,9 @@ export class CurrentUserUtils { } static setupCatalog(doc: Doc) { if (doc.myCatalog === undefined) { - doc.myCatalog = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "CATALOG", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, lockedPosition: true, + doc.myCatalog = new PrefetchProxy(Docs.Create.SchemaDocument([], [], { + title: "CATALOG", _height: 1000, _fitWidth: true, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, lockedPosition: true, + childDropAction: "alias", targetDropAction: "same", treeViewExpandedView: "layout" })); } return doc.myCatalog as Doc; @@ -539,10 +590,11 @@ export class CurrentUserUtils { if (doc["tabs-button-library"] === undefined) { doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({ - _width: 50, _height: 25, title: "Library", _fontSize: 10, + _width: 50, _height: 25, title: "Library", _fontSize: 10, targetDropAction: "same", letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", sourcePanel: new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], { - title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true + title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", + lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" })) as any as Doc, targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;") diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index f1afaf734..597b72e0c 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -13,7 +13,7 @@ import * as globalCssVariables from "../views/globalCssVariables.scss"; import { UndoManager } from "./UndoManager"; import { SnappingManager } from "./SnappingManager"; -export type dropActionType = "alias" | "copy" | "move" | undefined; // undefined = move +export type dropActionType = "alias" | "copy" | "move" | "same" | undefined; // undefined = move export function SetupDrag( _reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc> | undefined, @@ -37,7 +37,7 @@ export function SetupDrag( dragData.treeViewId = treeViewId; dragData.dontHideOnDrop = dontHideOnDrop; DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y); - dragStarted && dragStarted(); + dragStarted?.(); } }; const onRowUp = (): void => { @@ -122,7 +122,7 @@ export namespace DragManager { export class DocumentDragData { constructor(dragDoc: Doc[]) { this.draggedDocuments = dragDoc; - this.droppedDocuments = dragDoc; + this.droppedDocuments = []; this.offset = [0, 0]; } draggedDocuments: Doc[]; @@ -209,15 +209,19 @@ export namespace DragManager { }; const batch = UndoManager.StartBatch("dragging"); const finishDrag = (e: DragCompleteEvent) => { - e.docDragData && (e.docDragData.droppedDocuments = - dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : - dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) : - dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeDelegate(d) : d) - ); - e.docDragData?.droppedDocuments.forEach((drop: Doc, i: number) => - (dragData?.removeDropProperties || []).concat(Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), [])).map(prop => drop[prop] = undefined) - ); - batch.end(); + const docDragData = e.docDragData; + if (docDragData && !docDragData.droppedDocuments.length) { + docDragData.dropAction = dragData.userDropAction || dragData.dropAction; + docDragData.droppedDocuments = + 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) + ); + batch.end(); + } + return e; }; dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded StartDrag(eles, dragData, downX, downY, options, finishDrag); @@ -231,6 +235,7 @@ export namespace DragManager { initialize?.(bd); Doc.GetProto(bd)["onClick-paramFieldKeys"] = new List<string>(params); e.docDragData && (e.docDragData.droppedDocuments = [bd]); + return e; }; StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag); } @@ -319,7 +324,7 @@ export namespace DragManager { dragDiv = document.createElement("div"); dragDiv.className = "dragManager-dragDiv"; dragDiv.style.pointerEvents = "none"; - dragLabel = document.createElement("div") as HTMLDivElement; + dragLabel = document.createElement("div"); dragLabel.className = "dragManager-dragLabel"; dragLabel.style.zIndex = "100001"; dragLabel.style.fontSize = "10"; @@ -406,31 +411,28 @@ export namespace DragManager { const yFromTop = downY - elesCont.top; const xFromRight = elesCont.right - downX; const yFromBottom = elesCont.bottom - downY; - let alias = "alias"; const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : undefined; } - if (e) - if (e.shiftKey && dragData.droppedDocuments.length === 1) { - !dragData.dropAction && (dragData.dropAction = alias); - if (dragData.dropAction === "move") { - dragData.removeDocument?.(dragData.draggedDocuments[0]); - } - AbortDrag(); - finishDrag?.(new DragCompleteEvent(true, dragData)); - DragManager.StartWindowDrag?.({ - pageX: e.pageX, - pageY: e.pageY, - preventDefault: emptyFunction, - button: 0 - }, dragData.droppedDocuments); + if (e?.shiftKey && dragData.draggedDocuments.length === 1) { + dragData.dropAction = dragData.userDropAction || "same"; + if (dragData.dropAction === "move") { + dragData.removeDocument?.(dragData.draggedDocuments[0]); } + AbortDrag(); + finishDrag?.(new DragCompleteEvent(true, dragData)); + DragManager.StartWindowDrag?.({ + pageX: e.pageX, + pageY: e.pageY, + preventDefault: emptyFunction, + button: 0 + }, dragData.droppedDocuments); + } const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom); - alias = "move"; const moveX = thisX - lastX; const moveY = thisY - lastY; lastX = thisX; diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 81f9b9362..aeb0f670d 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -89,42 +89,40 @@ export namespace InteractionUtils { return myTouches; } - export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean) { - var pts = ""; - if (shape) { - //if any of the shape are true - const shapePts = makePolygon(shape, points); - pts = shapePts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, ""); + 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) { + 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) - const newPoints: number[][] = []; - const newPts: { X: number; Y: number; }[] = []; - //convert to [][] for fitcurve module - for (var i = 0; i < points.length - 1; i++) { - newPoints.push([points[i].X, points[i].Y]); - } + points.pop(); + const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]); + const bezierCurves = fitCurve(newPoints, parseInt(bezier)); for (var i = 0; i < bezierCurves.length; i++) { for (var t = 0; t < 1; t += 0.01) { const point = beziercurve(t, bezierCurves[i]); - newPts.push({ X: point[0], Y: point[1] }); + pts.push({ X: point[0], Y: point[1] }); } } - pts = newPts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, ""); } else { - //in the middle of drawing - pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, ""); + pts = points; } + 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} `, ""); + return ( <polyline - points={pts} + points={strpts} style={{ filter: drawHalo ? "url(#dangerShine)" : undefined, fill: "none", + opacity: strokeWidth !== width ? 0.5 : undefined, pointerEvents: pevents as any, stroke: color ?? "rgb(0, 0, 0)", - strokeWidth: parseInt(width), + strokeWidth: strokeWidth, strokeLinejoin: "round", strokeLinecap: "round" }} diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index fafc30625..ee3ce1cf3 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -69,16 +69,17 @@ export class EditableView extends React.Component<EditableProps> { EditableView.loadId = ""; } - @action - componentDidUpdate(nextProps: EditableProps) { - // this is done because when autosuggest is turned on, the suggestions are passed in as a prop, - // so when the suggestions are passed in, and no editing prop is passed in, it used to set it - // to false. this will no longer do so -syip - if (nextProps.editing && nextProps.editing !== this._editing) { - this._editing = nextProps.editing; - EditableView.loadId = ""; - } - } + // @action + // componentDidUpdate(nextProps: EditableProps) { + // // this is done because when autosuggest is turned on, the suggestions are passed in as a prop, + // // so when the suggestions are passed in, and no editing prop is passed in, it used to set it + // // to false. this will no longer do so -syip + // console.log("props editing = " + nextProps.editing); + // if (nextProps.editing && nextProps.editing !== this._editing) { + // this._editing = nextProps.editing; + // EditableView.loadId = ""; + // } + // } @action componentDidMount() { @@ -119,7 +120,7 @@ export class EditableView extends React.Component<EditableProps> { if (this._ref.current && this.props.showMenuOnLoad) { this.props.menuCallback?.(this._ref.current.getBoundingClientRect().x, this._ref.current.getBoundingClientRect().y); } else { - if (!this.props.onClick || !this.props.onClick(e)) { + if (!this.props.onClick?.(e)) { this._editing = true; this.props.isEditingCallback?.(true); } diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 372e42468..79d407c7a 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -323,7 +323,7 @@ export default class GestureOverlay extends Touchable { this._thumbY = thumb.clientY; this._menuX = thumb.clientX + 50; this._menuY = thumb.clientY; - this._palette = <HorizontalPalette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />; + this._palette = <HorizontalPalette key="palette" x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />; }); } @@ -803,17 +803,18 @@ export default class GestureOverlay extends Touchable { @computed get elements() { const B = this.svgBounds; + const width = Number(ActiveInkWidth()); return [ this.props.children, this._palette, - [this._strokes.map(l => { + [this._strokes.map((l, i) => { const b = this.getBounds(l); - return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> - {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape)} + 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)} </svg>; }), - this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> - {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape)} + 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)} </svg>] ]; } diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index a7650163f..7e3bd1c17 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,6 +1,5 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faPaintBrush } from "@fortawesome/free-solid-svg-icons"; -import { observable, runInAction, action } from "mobx"; import { observer } from "mobx-react"; import { documentSchema } from "../../fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../fields/InkField"; @@ -16,7 +15,6 @@ import { FieldView, FieldViewProps } from "./nodes/FieldView"; import React = require("react"); import { Scripting } from "../util/Scripting"; import { Doc } from "../../fields/Doc"; -import { Id } from "../../fields/FieldSymbols"; library.add(faPaintBrush); @@ -43,25 +41,22 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume render() { TraceMobx(); const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; + const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth())); const xs = data.map(p => p.X); const ys = data.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - const right = Math.max(...xs); - const bottom = Math.max(...ys); + const left = Math.min(...xs) - strokeWidth / 2; + const top = Math.min(...ys) - strokeWidth / 2; + const right = Math.max(...xs) + strokeWidth / 2; + const bottom = Math.max(...ys) + strokeWidth / 2; const width = right - left; const height = bottom - top; - const scaleX = this.props.PanelWidth() / width; - const scaleY = this.props.PanelHeight() / height; - const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth())); + const scaleX = (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth); + const scaleY = (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); const strokeColor = StrCast(this.layoutDoc.color, ActiveInkColor()); - const points = InteractionUtils.CreatePolyline(data, left, top, - strokeColor, - strokeWidth.toString(), + const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5); const hpoints = InteractionUtils.CreatePolyline(data, left, top, - this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", - (strokeWidth + 15).toString(), + this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, (strokeWidth + 15), StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", this.props.active() ? "visiblestroke" : "none", false); return ( <svg className="inkingStroke" diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 62aed67ed..baf9d4156 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -1,5 +1,5 @@ import React = require("react"); -import { action, observable } from "mobx"; +import { action, observable, trace } from "mobx"; import { observer } from "mobx-react"; import { CellInfo } from "react-table"; import "react-table/react-table.css"; @@ -23,6 +23,7 @@ import { faExpand } from '@fortawesome/free-solid-svg-icons'; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { undoBatch } from "../../util/UndoManager"; import { SnappingManager } from "../../util/SnappingManager"; +import { ComputedField } from "../../../fields/ScriptField"; library.add(faExpand); @@ -57,7 +58,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { componentDidMount() { document.addEventListener("keydown", this.onKeyDown); - } componentWillUnmount() { @@ -70,7 +70,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { document.removeEventListener("keydown", this.onKeyDown); this._isEditing = true; this.props.setIsEditing(true); - } } @@ -217,7 +216,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // <div className="collectionSchemaView-cellContents-docExpander" onPointerDown={this.expandDoc} > // <FontAwesomeIcon icon="expand" size="sm" /> // </div> - // ); + // ); + trace(); return ( <div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "grab" : "auto" }} ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> @@ -231,23 +231,29 @@ export class CollectionSchemaCell extends React.Component<CellProps> { height={"auto"} maxHeight={Number(MAX_ROW_HEIGHT)} GetValue={() => { - const field = props.Document[props.fieldKey]; - if (Field.IsField(field)) { - return Field.toScriptString(field); - } - return ""; - } - } - SetValue={(value: string) => { + const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); + const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined; + const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1]; + const val = cscript !== undefined ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : + Field.IsField(cfield) ? Field.toScriptString(cfield) : ""; + return val; + }} + SetValue={action((value: string) => { + let retVal = false; if (value.startsWith(":=")) { - return this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col); + retVal = this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col); + } else { + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + if (script.compiled) { + retVal = this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); + } } - const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - if (!script.compiled) { - return false; + if (retVal) { + this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing' + this.props.setIsEditing(false); } - return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); - }} + return retVal; + })} OnFillDown={async (value: string) => { const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); if (script.compiled) { diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index 6f1e8ac1f..b206765e8 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -66,8 +66,9 @@ export class MovableColumn extends React.Component<MovableColumnProps> { const rect = this._header!.current!.getBoundingClientRect(); const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); const before = x[0] < bounds[0]; - if (de.complete.columnDragData) { - this.props.reorderColumns(de.complete.columnDragData.colKey, this.props.columnValue, before, this.props.allColumns); + const colDragData = de.complete.columnDragData; + if (colDragData) { + this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns); return true; } return false; @@ -164,13 +165,14 @@ export class MovableRow extends React.Component<MovableRowProps> { } createRowDropTarget = (ele: HTMLDivElement) => { - this._rowDropDisposer && this._rowDropDisposer(); + this._rowDropDisposer?.(); if (ele) { this._rowDropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this)); } } rowDrop = (e: Event, de: DragManager.DropEvent) => { + this.onPointerLeave(e as any); const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc)); if (!rowDoc) return false; @@ -203,10 +205,7 @@ export class MovableRow extends React.Component<MovableRowProps> { @action move: DragManager.MoveFunction = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc) => { const targetView = targetCollection && DocumentManager.Instance.getDocumentView(targetCollection); - if (targetView && targetView.props.ContainingCollectionDoc) { - return doc !== targetCollection && doc !== targetView.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); - } - return doc !== targetCollection && this.props.removeDoc(doc) && addDoc(doc); + return doc !== targetCollection && doc !== targetView?.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); } render() { diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 35f892d65..6dbee217a 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -28,7 +28,7 @@ import { CollectionSubView } from "./CollectionSubView"; import { CollectionView } from "./CollectionView"; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; import { setupMoveUpEvents, emptyFunction, returnZero, returnOne, returnFalse } from "../../../Utils"; -import { DocumentView } from "../nodes/DocumentView"; +import { SnappingManager } from "../../util/SnappingManager"; library.add(faCog, faPlus, faSortUp, faSortDown); library.add(faTable); @@ -186,7 +186,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } render() { - return <div className="collectionSchemaView-container"> + return <div className="collectionSchemaView-container" + style={{ + pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined, + width: this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%" + }} > <div className="collectionSchemaView-tableContainer" style={{ width: `calc(100% - ${this.previewWidth()}px)` }} onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} ref={this.createTarget}> {this.schemaTable} </div> @@ -651,7 +655,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { resized={this.resized} onResizedChange={this.onResizedChange} SubComponent={!hasCollectionChild ? undefined : row => (row.original.type !== "collection") ? (null) : - <div className="reactTable-sub"><SchemaTable {...this.props} Document={row.original} childDocs={undefined} /></div>} + <div className="reactTable-sub"><SchemaTable {...this.props} Document={row.original} dataDoc={undefined} childDocs={undefined} /></div>} />; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index e891c4274..180bcdd02 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -123,7 +123,7 @@ class TreeView extends React.Component<TreeViewProps> { protected createTreeDropTarget = (ele: HTMLDivElement) => { this._treedropDisposer?.(); - ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this)), this.props.document); + ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this)), this.props.document); } onPointerEnter = (e: React.PointerEvent): void => { @@ -187,33 +187,36 @@ class TreeView extends React.Component<TreeViewProps> { })} />) + 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); + } + @undoBatch treeDrop = (e: Event, de: DragManager.DropEvent) => { 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); - if (de.complete.linkDragData) { - const sourceDoc = de.complete.linkDragData.linkSourceDocument; + const complete = de.complete; + if (complete.linkDragData) { + const sourceDoc = complete.linkDragData.linkSourceDocument; const destDoc = this.props.document; DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link"); e.stopPropagation(); } - if (de.complete.docDragData) { + const docDragData = complete.docDragData; + if (docDragData) { e.stopPropagation(); - if (de.complete.docDragData.draggedDocuments[0] === this.props.document) return true; - let addDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); + if (docDragData.draggedDocuments[0] === this.props.document) return true; + const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); + let addDoc = parentAddDoc; if (inside) { addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce( - ((flg: boolean, doc) => flg && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc)), true) || addDoc(doc); + (flg: boolean, doc) => flg && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc), true) || parentAddDoc(doc); } - const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId[Id] ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments); - const move = de.complete.docDragData.dropAction === "move" || de.complete.docDragData.dropAction; - return ((!move && (de.complete.docDragData.treeViewId !== this.props.treeViewId[Id])) || de.complete.docDragData.userDropAction) ? - de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d) || added, false) - : de.complete.docDragData.moveDocument ? - movedDocs.reduce((added, d) => de.complete.docDragData?.moveDocument?.(d, undefined, addDoc) || added, false) - : de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d), false); + const move = (!docDragData.dropAction || docDragData.dropAction === "move" || docDragData.dropAction === "same") && docDragData.moveDocument; + return docDragData.droppedDocuments.reduce((added, d) => (move ? docDragData.moveDocument?.(d, undefined, addDoc) : addDoc(d)) || added, false); } return false; } @@ -662,7 +665,16 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll protected createTreeDropTarget = (ele: HTMLDivElement) => { this.treedropDisposer?.(); if (this._mainEle = ele) { - this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.props.Document); + this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.props.Document, 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))) { + dragData.dropAction = targetAction; + } else dragData.dropAction = this.props.Document[Id] === dragData?.treeViewId ? "same" : dragData.dropAction; } } @@ -788,7 +800,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll 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` + paddingTop: `${NumCast(this.props.Document._yPadding, 20)}px`, + pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined }} onKeyPress={this.onKeyPress} onContextMenu={this.onContextMenu} diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index dfc8e6754..48810f1e9 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -220,8 +220,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { - if (de.complete.docDragData && de.complete.docDragData.draggedDocuments.length) { - this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.complete.docDragData?.draggedDocuments || [])); + const docDragData = de.complete.docDragData; + if (docDragData?.draggedDocuments.length) { + this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || [])); e.stopPropagation(); } return true; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index f3fc04752..6cac39f77 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -46,10 +46,15 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const bfield = afield === "anchor1" ? "anchor2" : "anchor1"; // really hacky stuff to make the LinkAnchorBox display where we want it to: - // if there's an element in the DOM with the id of the opposite anchor, then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right + // if there's an element in the DOM with a classname containing the link's id and a targetids attribute containing the other end of the link, + // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right // otherwise, we just use the computed nearest point on the document boundary to the target Document - const targetAhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][afield] as Doc)[Id]); - const targetBhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][bfield] as Doc)[Id]); + const linkId = this.props.LinkDocs[0][Id]; // this link's Id + const AanchorId = (this.props.LinkDocs[0][afield] as Doc)[Id]; // anchor a's id + const BanchorId = (this.props.LinkDocs[0][bfield] as Doc)[Id]; // anchor b's id + const linkEles = Array.from(window.document.getElementsByClassName(linkId)); + 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; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 678ad2a53..736c5fd06 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -52,7 +52,6 @@ library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressAr export const panZoomSchema = createSchema({ _panX: "number", _panY: "number", - scale: "number", currentTimecode: "number", displayTimecode: "number", currentFrame: "number", @@ -76,6 +75,7 @@ export type collectionFreeformViewProps = { forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox) viewDefDivClick?: ScriptField; childPointerEvents?: boolean; + scaleField?: string; }; @observer @@ -108,6 +108,7 @@ 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 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; @@ -115,14 +116,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P private zoomScaling = () => (this.fitToContentScaling / this.parentScaling) * (this.fitToContent ? Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : - this.Document.scale || 1) + NumCast(this.Document[this.scaleFieldKey], 1)) @computed get cachedCenteringShiftX(): number { - const scaling = this.fitToContent ? 1 : this.contentScaling; + const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { - const scaling = this.fitToContent ? 1 : this.contentScaling; + const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { @@ -456,7 +457,8 @@ 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, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height }); + const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), 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(); break; @@ -779,7 +781,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling()) { const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); - this.props.Document.scale = Math.abs(safeScale); + this.props.Document[this.scaleFieldKey] = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); } } @@ -795,7 +797,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (!e.ctrlKey && MarqueeView.DragMarquee) this.setPan(this.panX() + e.deltaX, this.panY() + e.deltaY, "None", true); else this.zoom(e.clientX, e.clientY, e.deltaY); } - this.props.Document.targetScale = NumCast(this.props.Document.scale); + this.props.Document.targetScale = NumCast(this.props.Document[this.scaleFieldKey]); } @action @@ -852,7 +854,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.layoutDoc.scale = scale; + 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] }; const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y); @@ -896,7 +898,7 @@ 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.scale, pt: this.Document.panTransformType }; + const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document.panTransformType }; // if (!willZoom && DocumentView._focusHack.length) { // Doc.BrushDoc(this.props.Document); @@ -915,7 +917,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (afterFocus?.()) { this.Document._panX = savedState.px; this.Document._panY = savedState.py; - this.Document.scale = savedState.s; + this.Document[this.scaleFieldKey] = savedState.s; this.Document.panTransformType = savedState.pt; } }, 500); @@ -924,7 +926,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } setScaleToZoom = (doc: Doc, scale: number = 0.75) => { - this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); + this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); } @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } @@ -1125,7 +1127,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P })); if (this.props.isAnnotationOverlay) { - this.props.Document.scale = Math.max(1, NumCast(this.props.Document.scale)); + this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); } return elements; @@ -1206,7 +1208,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P 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.scale = 1; }, icon: "compress-arrows-alt" }); + 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" }); diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx index 5a27f74e5..ae82c6a65 100644 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx @@ -95,16 +95,14 @@ export default class InkOptionsMenu extends AntimodeMenu { } @computed get shapeButtons() { - return <> - {this._buttons.map((btn, i) => <button - className="antimodeMenu-button" - title={`Draw ${btn}`} - key={btn} - onPointerDown={action(e => GestureOverlay.Instance.InkShape = btn)} - style={{ backgroundColor: btn === GestureOverlay.Instance.InkShape ? "121212" : "" }}> - {this._icons[i]} - </button>)}, - </>; + return this._buttons.map((btn, i) => <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>); } @computed get bezierButton() { @@ -113,7 +111,7 @@ export default class InkOptionsMenu extends AntimodeMenu { title="Bezier changer" key="bezier" onPointerDown={e => this.changeBezier(e)} - style={ { backgroundColor:ActiveInkBezierApprox() ? "121212":"" } }> + style={{ backgroundColor: ActiveInkBezierApprox() ? "121212" : "" }}> B </button>; } @@ -121,7 +119,7 @@ export default class InkOptionsMenu extends AntimodeMenu { render() { const buttons = [ <button className="antimodeMenu-button" title="Drag" key="drag" onPointerDown={e => this.dragStart(e)}> ✜ </button>, - this.shapeButtons, + ...this.shapeButtons, this.bezierButton, this.widthPicker, this.colorPicker, diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 085625e69..5f09fa0ee 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -122,7 +122,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque 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), - _fontFamily: StrCast(Doc.UserDoc().fontFamily), _backgroundColor: StrCast(Doc.UserDoc().backgroundColor), + _fontFamily: StrCast(Doc.UserDoc().fontFamily), title: "-typed text-" }); const template = FormattedTextBox.DefaultLayout; diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index fe89b63ee..a5d355abc 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -162,7 +162,7 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { if (this.flexGrid) { const savedLayouts = this.savedLayoutList; this.childLayoutPairs.forEach(({ layout: doc }) => { - let gridLayout = savedLayouts.find(gridLayout => gridLayout.i === doc[Id]); + const gridLayout = savedLayouts.find(gridLayout => gridLayout.i === doc[Id]); gridLayout && Object.assign(gridLayout, layoutArray.find(layout => layout.i === doc[Id]) || gridLayout); }); diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index 0d6258cf3..d04da8f5b 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -60,9 +60,9 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} /> <div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}> <div> {ActiveInkWidth() ?? 2}</div> - <input type="range" value={ActiveInkWidth() ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveInkWidth(e.target.value)} /> + <input type="range" defaultValue={ActiveInkWidth() ?? 2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveInkWidth(e.target.value)} /> <div> {ActiveInkBezierApprox() ?? 2}</div> - <input type="range" value={ActiveInkBezierApprox() ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveBezierApprox(e.target.value)} /> + <input type="range" defaultValue={ActiveInkBezierApprox() ?? 2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveBezierApprox(e.target.value)} /> <br /> <br /> </div> diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index cce60628d..14a2c25bf 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -76,12 +76,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C const clearButton = (which: string) => { return <div className={`clear-button ${which}`} onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding - onClick={e => this.clearDoc(e, `${which}Doc`)}> + onClick={e => this.clearDoc(e, `compareBox-${which}`)}> <FontAwesomeIcon className={`clear-button ${which}`} icon={"times"} size="sm" /> </div>; }; const displayDoc = (which: string) => { - const whichDoc = Cast(this.dataDoc[`${which}Doc`], Doc, null); + const whichDoc = Cast(this.dataDoc[`compareBox-${which}`], Doc, null); return whichDoc ? <> <ContentFittingDocumentView {...childProps} Document={whichDoc} /> {clearButton(which)} @@ -93,7 +93,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C const displayBox = (which: string, index: number, cover: number) => { return <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} - ref={ele => this.createDropTarget(ele, `${which}Doc`, index)} > + ref={ele => this.createDropTarget(ele, `compareBox-${which}`, index)} > {displayDoc(which)} </div>; }; diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index d9e7d072f..ba075886b 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -1,18 +1,18 @@ import React = require("react"); import { computed } from "mobx"; import { observer } from "mobx-react"; +import { Transform } from "nodemailer/lib/xoauth2"; import "react-table/react-table.css"; -import { Doc, Opt, WidthSym, HeightSym } from "../../../fields/Doc"; -import { NumCast, StrCast, Cast } from "../../../fields/Types"; +import { Doc, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { ScriptField } from "../../../fields/ScriptField"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, returnOne } from "../../../Utils"; +import { emptyFunction } from "../../../Utils"; +import { dropActionType } from "../../util/DragManager"; +import { CollectionView } from "../collections/CollectionView"; import '../DocumentDecorations.scss'; import { DocumentView, DocumentViewProps } from "../nodes/DocumentView"; import "./ContentFittingDocumentView.scss"; -import { dropActionType } from "../../util/DragManager"; -import { CollectionView } from "../collections/CollectionView"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { Transform } from "nodemailer/lib/xoauth2"; interface ContentFittingDocumentViewProps { Document: Doc; diff --git a/src/client/views/nodes/DocHolderBox.tsx b/src/client/views/nodes/DocHolderBox.tsx index 0c5239d66..a4c4663dd 100644 --- a/src/client/views/nodes/DocHolderBox.tsx +++ b/src/client/views/nodes/DocHolderBox.tsx @@ -198,11 +198,10 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (de.complete.docDragData) { - if (de.complete.docDragData.draggedDocuments[0].type === DocumentType.FONTICON) { - const doc = Cast(de.complete.docDragData.draggedDocuments[0].dragFactory, Doc, null); - this.layoutDoc.childLayoutTemplate = doc; - } + const docDragData = de.complete.docDragData; + if (docDragData?.draggedDocuments[0].type === DocumentType.FONTICON) { + const doc = Cast(docDragData.draggedDocuments[0].dragFactory, Doc, null); + this.layoutDoc.childLayoutTemplate = doc; } } protected createDropTarget = (ele: HTMLDivElement) => { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index e4e4dfdd6..da5327f48 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -43,13 +43,13 @@ import "./DocumentView.scss"; import { LinkAnchorBox } from './LinkAnchorBox'; import { RadialMenu } from './RadialMenu'; import React = require("react"); -import { undo } from 'prosemirror-history'; 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, - fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone); + fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone, fa.faKeyboard, fa.faQuestion); export type DocFocusFunc = () => boolean; + export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; @@ -138,7 +138,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); console.log(SelectionManager.SelectedDocuments()); - console.log("START"); if (RadialMenu.Instance._display === false) { this.addHoldMoveListeners(); this.addHoldEndListeners(); @@ -179,8 +178,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @action onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => { - // console.log(InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)); - // const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15); @@ -189,12 +186,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 }); RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "folder", selected: -1 }); - // if (SelectionManager.IsSelected(this, true)) { - // SelectionManager.SelectDoc(this, false); - // } SelectionManager.DeselectAll(); - - } @action @@ -335,7 +327,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { + } 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 @@ -511,8 +503,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } onPointerDown = (e: React.PointerEvent): void => { - // console.log(e.button) - // console.log(e.nativeEvent) // continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document) if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { @@ -687,13 +677,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action setAcl = (acl: "readOnly" | "addOnly" | "ownerOnly") => { - this.layoutDoc.ACL = this.dataDoc.ACL = acl; + this.dataDoc.ACL = this.props.Document.ACL = acl; DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => { if (d.author === Doc.CurrentUserEmail) d.ACL = acl; const data = d[DataSym]; if (data && data.author === Doc.CurrentUserEmail) data.ACL = acl; }); } + @undoBatch + @action + testAcl = (acl: "readOnly" | "addOnly" | "ownerOnly") => { + this.dataDoc.author = this.props.Document.author = "ADMIN"; + this.dataDoc.ACL = this.props.Document.ACL = acl; + DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => { + if (d.author === Doc.CurrentUserEmail) { + d.author = "ADMIN"; + d.ACL = acl; + } + const data = d[DataSym]; + if (data && data.author === Doc.CurrentUserEmail) { + data.author = "ADMIN"; + data.ACL = acl; + } + }); + } @action onContextMenu = async (e: React.MouseEvent | Touch): Promise<void> => { @@ -722,7 +729,6 @@ 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 optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); @@ -744,7 +750,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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)')) }); @@ -755,18 +760,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const more = cm.findByDescription("More..."); const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : []; - moreItems.push({ description: "Make Add Only", event: () => this.setAcl("addOnly"), icon: "concierge-bell" }); - moreItems.push({ description: "Make Read Only", event: () => this.setAcl("readOnly"), icon: "concierge-bell" }); - moreItems.push({ description: "Make Private", event: () => this.setAcl("ownerOnly"), icon: "concierge-bell" }); moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); + moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); - if (!ClientUtils.RELEASE) { - // let copies: ContextMenuProps[] = []; - moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); - // cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); - } if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); @@ -785,65 +783,83 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // a.download = `DocExport-${this.props.Document[Id]}.zip`; // a.click(); }); - - const recommender_subitems: ContextMenuProps[] = []; - - recommender_subitems.push({ - description: "Internal recommendations", - event: () => this.recommender(), - icon: "brain" - }); - - const ext_recommender_subitems: ContextMenuProps[] = []; - - ext_recommender_subitems.push({ - description: "arXiv", - event: () => this.externalRecommendation("arxiv"), - icon: "brain" - }); - ext_recommender_subitems.push({ - description: "Bing", - event: () => this.externalRecommendation("bing"), - icon: "brain" - }); - - recommender_subitems.push({ - description: "External recommendations", - subitems: ext_recommender_subitems, - icon: "brain" - }); - moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); - moreItems.push({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" }); - moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); - moreItems.push({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); + moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "external-link-alt" }); !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); - cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); - runInAction(() => { - const setWriteMode = (mode: DocServer.WriteMode) => { - DocServer.AclsMode = mode; - const mode1 = mode; - const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; - DocServer.setFieldWriteMode("x", mode1); - DocServer.setFieldWriteMode("y", mode1); - DocServer.setFieldWriteMode("_width", mode1); - DocServer.setFieldWriteMode("_height", mode1); - - DocServer.setFieldWriteMode("_panX", mode2); - DocServer.setFieldWriteMode("_panY", mode2); - DocServer.setFieldWriteMode("scale", mode2); - DocServer.setFieldWriteMode("_viewType", mode2); - }; - const aclsMenu: ContextMenuProps[] = []; - aclsMenu.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "external-link-alt" }); - aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" }); - aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" }); - aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" }); - aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" }); - cm.addItem({ description: "Collaboration ...", subitems: aclsMenu, icon: "share" }); + const help = cm.findByDescription("Help..."); + const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; + helpItems.push({ + description: "Keyboard Shortcuts Ctrl+/", + event: () => this.props.addDocTab(Docs.Create.PdfDocument("http://localhost:1050/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "onRight"), + icon: "keyboard" }); + cm.addItem({ description: "Help...", subitems: helpItems, icon: "question" }); + + const existingAcls = cm.findByDescription("Privacy..."); + const aclItems: ContextMenuProps[] = existingAcls && "subitems" in existingAcls ? existingAcls.subitems : []; + aclItems.push({ description: "Make Add Only", event: () => this.setAcl("addOnly"), icon: "concierge-bell" }); + aclItems.push({ description: "Make Read Only", event: () => this.setAcl("readOnly"), icon: "concierge-bell" }); + aclItems.push({ description: "Make Private", event: () => this.setAcl("ownerOnly"), icon: "concierge-bell" }); + aclItems.push({ description: "Test Private", event: () => this.testAcl("ownerOnly"), icon: "concierge-bell" }); + aclItems.push({ description: "Test Readonly", event: () => this.testAcl("readOnly"), icon: "concierge-bell" }); + !existingAcls && cm.addItem({ description: "Privacy...", subitems: aclItems, icon: "question" }); + + // const recommender_subitems: ContextMenuProps[] = []; + + // recommender_subitems.push({ + // description: "Internal recommendations", + // event: () => this.recommender(), + // icon: "brain" + // }); + + // const ext_recommender_subitems: ContextMenuProps[] = []; + + // ext_recommender_subitems.push({ + // description: "arXiv", + // event: () => this.externalRecommendation("arxiv"), + // icon: "brain" + // }); + // ext_recommender_subitems.push({ + // description: "Bing", + // event: () => this.externalRecommendation("bing"), + // icon: "brain" + // }); + + // recommender_subitems.push({ + // description: "External recommendations", + // subitems: ext_recommender_subitems, + // icon: "brain" + // }); + + + //moreItems.push({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" }); + //moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); + //moreItems.push({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); + + // runInAction(() => { + // const setWriteMode = (mode: DocServer.WriteMode) => { + // DocServer.AclsMode = mode; + // const mode1 = mode; + // const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; + // DocServer.setFieldWriteMode("x", mode1); + // DocServer.setFieldWriteMode("y", mode1); + // DocServer.setFieldWriteMode("_width", mode1); + // DocServer.setFieldWriteMode("_height", mode1); + + // DocServer.setFieldWriteMode("_panX", mode2); + // DocServer.setFieldWriteMode("_panY", mode2); + // DocServer.setFieldWriteMode("scale", mode2); + // DocServer.setFieldWriteMode("_viewType", mode2); + // }; + // const aclsMenu: ContextMenuProps[] = []; + // aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" }); + // aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" }); + // aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" }); + // aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" }); + // cm.addItem({ description: "Collaboration ...", subitems: aclsMenu, icon: "share" }); + // }); runInAction(() => { if (!this.topMost && !(e instanceof Touch)) { // DocumentViews should stop propagation of this event diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 83245a89c..3c232eff8 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -117,7 +117,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch const y = this.props.PanelWidth() > 1 ? NumCast(this.layoutDoc[this.fieldKey + "_y"], 100) : 0; const c = StrCast(this.layoutDoc.backgroundColor, "lightblue"); const anchor = this.fieldKey === "anchor1" ? "anchor2" : "anchor1"; - const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .15; + const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25; const timecode = this.dataDoc[anchor + "_timecode"]; const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title) + (timecode !== undefined ? ":" + timecode : ""); diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index d56b87ae5..ad204f3df 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -1,95 +1,112 @@ -import { IReactionDisposer, observable, reaction, runInAction } from "mobx"; -import { baseKeymap, toggleMark } from "prosemirror-commands"; -import { redo, undo } from "prosemirror-history"; -import { keymap } from "prosemirror-keymap"; -import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model"; -import { bulletList, listItem, orderedList } from 'prosemirror-schema-list'; -import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state"; -import { StepMap } from "prosemirror-transform"; -import { EditorView } from "prosemirror-view"; +import { TextSelection } from "prosemirror-state"; import * as ReactDOM from 'react-dom'; -import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../fields/Doc"; -import { Id } from "../../../../fields/FieldSymbols"; -import { List } from "../../../../fields/List"; -import { ObjectField } from "../../../../fields/ObjectField"; -import { listSpec } from "../../../../fields/Schema"; -import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; -import { ComputedField } from "../../../../fields/ScriptField"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils"; +import { Doc } from "../../../../fields/Doc"; import { DocServer } from "../../../DocServer"; - import React = require("react"); -import { schema } from "./schema_rts"; -interface IDashDocCommentView { - node: any; +// creates an inline comment in a note when '>>' is typed. +// the comment sits on the right side of the note and vertically aligns with its anchor in the text. +// the comment can be toggled on/off with the '<-' text anchor. +export class DashDocCommentView { + _fieldWrapper: HTMLDivElement; // container for label and value + + constructor(node: any, view: any, getPos: any) { + this._fieldWrapper = document.createElement("div"); + this._fieldWrapper.style.width = node.attrs.width; + this._fieldWrapper.style.height = node.attrs.height; + this._fieldWrapper.style.fontWeight = "bold"; + this._fieldWrapper.style.position = "relative"; + this._fieldWrapper.style.display = "inline-block"; + this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); }; + + ReactDOM.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />, this._fieldWrapper); + (this as any).dom = this._fieldWrapper; + } + + destroy() { + ReactDOM.unmountComponentAtNode(this._fieldWrapper); + } + + selectNode() { } +} + +interface IDashDocCommentViewInternal { + docid: string; view: any; getPos: any; } -export class DashDocCommentView extends React.Component<IDashDocCommentView>{ - constructor(props: IDashDocCommentView) { - super(props); - } +export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal>{ - targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor - for (let i = this.props.getPos() + 1; i < this.props.view.state.doc.content.size; i++) { - const m = this.props.view.state.doc.nodeAt(i); - if (m && m.type === this.props.view.state.schema.nodes.dashDoc && m.attrs.docid === this.props.node.attrs.docid) { - return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean }; - } - } - const dashDoc = this.props.view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: this.props.node.attrs.docid, float: "right" }); - this.props.view.dispatch(this.props.view.state.tr.insert(this.props.getPos() + 1, dashDoc)); - setTimeout(() => { try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + 2))); } catch (e) { } }, 0); - return undefined; + constructor(props: IDashDocCommentViewInternal) { + super(props) + this.onPointerLeaveCollapsed = this.onPointerLeaveCollapsed.bind(this) + this.onPointerEnterCollapsed = this.onPointerEnterCollapsed.bind(this) + this.onPointerUpCollapsed = this.onPointerUpCollapsed.bind(this) + this.onPointerDownCollapsed = this.onPointerDownCollapsed.bind(this) } - onPointerDownCollapse = (e: any) => e.stopPropagation(); + onPointerLeaveCollapsed(e: any) { + DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); + e.preventDefault(); + e.stopPropagation(); + }; - onPointerUpCollapse = (e: any) => { + onPointerEnterCollapsed(e: any) { + DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); + e.preventDefault(); + e.stopPropagation(); + }; + + onPointerUpCollapsed(e: any) { const target = this.targetNode(); + if (target) { const expand = target.hidden; const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true }); this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs setTimeout(() => { - expand && DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); + expand && DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1)))); } catch (e) { } }, 0); } e.stopPropagation(); - } + }; - onPointerEnterCollapse = (e: any) => { - DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); - e.preventDefault(); + onPointerDownCollapsed(e: any) { e.stopPropagation(); - } - - onPointerLeaveCollapse = (e: any) => { - DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); - e.preventDefault(); - e.stopPropagation(); - } + }; - render() { + targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor + const state = this.props.view.state; + for (let i = this.props.getPos() + 1; i < state.doc.content.size; i++) { + const m = state.doc.nodeAt(i); + if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docid === this.props.docid) { + return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean }; + } + } - const collapsedId = "DashDocCommentView-" + this.props.node.attrs.docid; + const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: this.props.docid, float: "right" }); + this.props.view.dispatch(state.tr.insert(this.props.getPos() + 1, dashDoc)); + setTimeout(() => { try { this.props.view.dispatch(state.tr.setSelection(TextSelection.create(state.tr.doc, this.props.getPos() + 2))); } catch (e) { } }, 0); + return undefined; + }; + render() { return ( <span className="formattedTextBox-inlineComment" - id={collapsedId} - onPointerDown={this.onPointerDownCollapse} - onPointerUp={this.onPointerUpCollapse} - onPointerEnter={this.onPointerEnterCollapse} - onPointerLeave={this.onPointerLeaveCollapse} + id={"DashDocCommentView-" + this.props.docid} + onPointerLeave={this.onPointerLeaveCollapsed} + onPointerEnter={this.onPointerEnterCollapsed} + onPointerUp={this.onPointerUpCollapsed} + onPointerDown={this.onPointerDownCollapsed} > - - </span > - ); + </span> + ) } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index 35ff9c1e6..23cf1e79b 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -25,7 +25,7 @@ margin-left: 2px; margin-right: 5px; position: relative; - display: inline-block; + display: inline; background-color: rgba(155, 155, 155, 0.24); span { min-width: 100%; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 9a1b909c1..8c16f4a1a 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -14,7 +14,6 @@ import "./DashFieldView.scss"; import { observer } from "mobx-react"; import { DocUtils } from "../../../documents/Documents"; - export class DashFieldView { _fieldWrapper: HTMLDivElement; // container for label and value @@ -40,12 +39,10 @@ export class DashFieldView { />, this._fieldWrapper); (this as any).dom = this._fieldWrapper; } - destroy() { - ReactDOM.unmountComponentAtNode(this._fieldWrapper); - } + destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); } selectNode() { } - } + interface IDashFieldViewInternal { fieldKey: string; docid: string; @@ -103,11 +100,14 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna // bcz: this is unfortunate, but since this React component is nested within a non-React text box (prosemirror), we can't // use React events. Essentially, React events occur after native events have been processed, so corresponding React events // will never fire because Prosemirror has handled the native events. So we add listeners for native events here. - return <span contentEditable={true} suppressContentEditableWarning={true} defaultValue={strVal} ref={r => { - r?.addEventListener("keydown", e => this.fieldSpanKeyDown(e, r)); - r?.addEventListener("blur", e => r && this.updateText(r.textContent!, false)); - r?.addEventListener("pointerdown", action((e) => this._showEnumerables = true)); - }} > + return <span className="dashFieldView-fieldSpan" contentEditable={true} + style={{ display: strVal.length < 2 ? "inline-block" : undefined }} + suppressContentEditableWarning={true} defaultValue={strVal} + ref={r => { + r?.addEventListener("keydown", e => this.fieldSpanKeyDown(e, r)); + r?.addEventListener("blur", e => r && this.updateText(r.textContent!, false)); + r?.addEventListener("pointerdown", action((e) => this._showEnumerables = true)); + }} > {strVal} </span>; } @@ -205,9 +205,9 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna {this._fieldKey} </span>} - <div className="dashFieldView-fieldSpan"> - {this.fieldValueContent} - </div> + {/* <div className="dashFieldView-fieldSpan"> */} + {this.fieldValueContent} + {/* </div> */} {!this._showEnumerables ? (null) : <div className="dashFieldView-enumerables" onPointerDown={this.onPointerDownEnumerables} />} diff --git a/src/client/views/nodes/formattedText/FootnoteView.tsx b/src/client/views/nodes/formattedText/FootnoteView.tsx index ee21fb765..1683cc972 100644 --- a/src/client/views/nodes/formattedText/FootnoteView.tsx +++ b/src/client/views/nodes/formattedText/FootnoteView.tsx @@ -6,54 +6,50 @@ import { schema } from "./schema_rts"; import { redo, undo } from "prosemirror-history"; import { StepMap } from "prosemirror-transform"; -import React = require("react"); - -interface IFootnoteView { +export class FootnoteView { innerView: any; outerView: any; node: any; dom: any; getPos: any; -} -export class FootnoteView extends React.Component<IFootnoteView> { - _innerView: any; - _node: any; + constructor(node: any, view: any, getPos: any) { + // We'll need these later + this.node = node; + this.outerView = view; + this.getPos = getPos; + + // The node's representation in the editor (empty, for now) + this.dom = document.createElement("footnote"); - constructor(props: IFootnoteView) { - super(props); - const node = this.props.node; - const outerView = this.props.outerView; - const _innerView = this.props.innerView; - const getPos = this.props.getPos; + this.dom.addEventListener("pointerup", this.toggle, true); + // These are used when the footnote is selected + this.innerView = null; } selectNode() { - const attrs = { ...this.props.node.attrs }; - attrs.visibility = true; this.dom.classList.add("ProseMirror-selectednode"); - if (!this.props.innerView) this.open(); + if (!this.innerView) this.open(); } deselectNode() { - const attrs = { ...this.props.node.attrs }; - attrs.visibility = false; this.dom.classList.remove("ProseMirror-selectednode"); - if (this.props.innerView) this.close(); + if (this.innerView) this.close(); } + open() { // Append a tooltip to the outer node const tooltip = this.dom.appendChild(document.createElement("div")); tooltip.className = "footnote-tooltip"; // And put a sub-ProseMirror into that - this.props.innerView.defineProperty(new EditorView(tooltip, { + this.innerView = new EditorView(tooltip, { // You can use any node as an editor document state: EditorState.create({ - doc: this.props.node, + doc: this.node, plugins: [keymap(baseKeymap), keymap({ - "Mod-z": () => undo(this.props.outerView.state, this.props.outerView.dispatch), - "Mod-y": () => redo(this.props.outerView.state, this.props.outerView.dispatch), + "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch), + "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch), "Mod-b": toggleMark(schema.marks.strong) }), // new Plugin({ @@ -74,11 +70,11 @@ export class FootnoteView extends React.Component<IFootnoteView> { // the parent editor is focused. e.stopPropagation(); document.addEventListener("pointerup", this.ignore, true); - if (this.props.outerView.hasFocus()) this.props.innerView.focus(); + if (this.outerView.hasFocus()) this.innerView.focus(); }) as any } - })); - setTimeout(() => this.props.innerView && this.props.innerView.docView.setSelection(0, 0, this.props.innerView.root, true), 0); + }); + setTimeout(() => this.innerView?.docView.setSelection(0, 0, this.innerView.root, true), 0); } ignore = (e: PointerEvent) => { @@ -86,32 +82,43 @@ export class FootnoteView extends React.Component<IFootnoteView> { document.removeEventListener("pointerup", this.ignore, true); } + toggle = () => { + if (this.innerView) this.close(); + else this.open(); + } + + close() { + this.innerView?.destroy(); + this.innerView = null; + this.dom.textContent = ""; + } + dispatchInner(tr: any) { - const { state, transactions } = this.props.innerView.state.applyTransaction(tr); - this.props.innerView.updateState(state); + const { state, transactions } = this.innerView.state.applyTransaction(tr); + this.innerView.updateState(state); if (!tr.getMeta("fromOutside")) { - const outerTr = this.props.outerView.state.tr, offsetMap = StepMap.offset(this.props.getPos() + 1); + const outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1); for (const transaction of transactions) { - const steps = transaction.steps; - for (const step of steps) { + for (const step of transaction.steps) { outerTr.step(step.map(offsetMap)); } } - if (outerTr.docChanged) this.props.outerView.dispatch(outerTr); + if (outerTr.docChanged) this.outerView.dispatch(outerTr); } } + update(node: any) { - if (!node.sameMarkup(this.props.node)) return false; - this._node = node; //not sure - if (this.props.innerView) { - const state = this.props.innerView.state; + if (!node.sameMarkup(this.node)) return false; + this.node = node; + if (this.innerView) { + const state = this.innerView.state; const start = node.content.findDiffStart(state.doc.content); if (start !== null) { let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content); const overlap = start - Math.min(endA, endB); if (overlap > 0) { endA += overlap; endB += overlap; } - this.props.innerView.dispatch( + this.innerView.dispatch( state.tr .replace(start, endB, node.slice(start, endA)) .setMeta("fromOutside", true)); @@ -119,44 +126,17 @@ export class FootnoteView extends React.Component<IFootnoteView> { } return true; } - onPointerUp = (e: any) => { - this.toggle(e); - } - - toggle = (e: any) => { - e.preventDefault(); - if (this.props.innerView) this.close(); - else { - this.open(); - } - } - - close() { - this.props.innerView && this.props.innerView.destroy(); - this._innerView = null; - this.dom.textContent = ""; - } destroy() { - if (this.props.innerView) this.close(); + if (this.innerView) this.close(); } stopEvent(event: any) { - return this.props.innerView && this.props.innerView.dom.contains(event.target); + return this.innerView?.dom.contains(event.target); } - ignoreMutation() { return true; } - - - render() { - return ( - <div - className="footnote" - onPointerUp={this.onPointerUp}> - <div className="footnote-tooltip" > - - </div > - </div> - ); + ignoreMutation() { + return true; } } + diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 49114d967..df4a05dd7 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -67,6 +67,7 @@ display: inline-block; position: absolute; right: 0; + overflow: hidden; .collectionfreeformview-container { position: relative; @@ -232,6 +233,48 @@ footnote::after { } } +.prosemirror-anchor { + overflow:hidden; + display:inline-grid; +} +.prosemirror-linkBtn { + background:unset; + color:unset; + padding:0; + text-transform: unset; + letter-spacing: unset; + font-size:unset; +} +.prosemirror-links { + display: none; + position: absolute; + background-color: gray; + padding-bottom: 10px; + margin-top: 1em; + z-index: 1; + } + .prosemirror-hrefoptions{ + width:0px; + border:unset; + padding:0px; + + } + + .prosemirror-links a { + float: left; + color: white; + text-decoration: none; + } + + .prosemirror-links a:hover { + background-color: #eee; + color: black; + } + + .prosemirror-anchor:hover .prosemirror-links { + display: grid; + } + .ProseMirror { touch-action: none; span { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 1fab54d7e..ba36a1618 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -34,15 +34,15 @@ import { makeTemplate } from '../../../util/DropConverter'; import buildKeymap from "./ProsemirrorExampleTransfer"; import RichTextMenu from './RichTextMenu'; import { RichTextRules } from "./RichTextRules"; -import { DashDocCommentView, DashDocView, FootnoteView, ImageResizeView, OrderedListView, SummaryView } from "./RichTextSchema"; -// import { DashDocCommentView, DashDocView, DashFieldView, FootnoteView, SummaryView } from "./RichTextSchema"; -// import { OrderedListView } from "./RichTextSchema"; -// import { ImageResizeView } from "./ImageResizeView"; -// import { DashDocCommentView } from "./DashDocCommentView"; -// import { FootnoteView } from "./FootnoteView"; -// import { SummaryView } from "./SummaryView"; -// import { DashDocView } from "./DashDocView"; + +//import { DashDocView } from "./DashDocView"; +import { DashDocView } from "./RichTextSchema"; + +import { DashDocCommentView } from "./DashDocCommentView"; import { DashFieldView } from "./DashFieldView"; +import { SummaryView } from "./SummaryView"; +import { OrderedListView } from "./OrderedListView"; +import { FootnoteView } from "./FootnoteView"; import { schema } from "./schema_rts"; import { SelectionManager } from "../../../util/SelectionManager"; @@ -180,7 +180,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.linkOnDeselect.set(key, value); const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); - const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + id), location: "onRight", title: value }); + const allHrefs = [{ href: Utils.prepend("/doc/" + id), title: value, targetId: id }]; + const link = this._editorView.state.schema.marks.link.create({ allHrefs, location: "onRight", title: value }); const mval = this._editorView.state.schema.marks.metadataVal.create(); const offset = (tx.selection.to === range!.end - 1 ? -1 : 0); tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval); @@ -239,10 +240,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const lastSel = Math.min(flattened.length - 1, this._searchIndex); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; const alink = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: target }, "automatic")!; - const link = this._editorView.state.schema.marks.link.create({ - href: Utils.prepend("/doc/" + alink[Id]), - title: "a link", location: location, linkId: alink[Id], targetId: target[Id] - }); + const allHrefs = [{ href: Utils.prepend("/doc/" + alink[Id]), title: "a link", targetId: target[Id], linkId: alink[Id] }]; + const link = this._editorView.state.schema.marks.link.create({ allHrefs, title: "a link", location }); this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link)); } } @@ -283,8 +282,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { - if (de.complete.docDragData) { - const draggedDoc = de.complete.docDragData.draggedDocuments.length && de.complete.docDragData.draggedDocuments[0]; + const dragData = de.complete.docDragData; + if (dragData) { + const draggedDoc = dragData.draggedDocuments.length && dragData.draggedDocuments[0]; // replace text contents whend dragging with Alt if (draggedDoc && draggedDoc.type === DocumentType.RTF && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.altKey) { if (draggedDoc.data instanceof RichTextField) { @@ -292,8 +292,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp e.stopPropagation(); } // embed document when dragging with a userDropAction or an embedDoc flag set - } else if (de.complete.docDragData.userDropAction || de.complete.docDragData.embedDoc) { - const target = de.complete.docDragData.droppedDocuments[0]; + } else if (dragData.userDropAction || dragData.embedDoc) { + const target = dragData.droppedDocuments[0]; // const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title); // if (link) { target._fitToBox = true; @@ -583,11 +583,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }; } - makeLinkToSelection(linkDocId: string, title: string, location: string, targetDocId: string) { - if (this._editorView) { - const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId }); - this._editorView.dispatch(this._editorView.state.tr.removeMark(this._editorView.state.selection.from, this._editorView.state.selection.to, this._editorView.state.schema.marks.link). - addMark(this._editorView.state.selection.from, this._editorView.state.selection.to, link)); + makeLinkToSelection(linkId: string, title: string, location: string, targetId: string) { + const state = this._editorView?.state; + if (state) { + const href = 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); + sel.from !== sel.to && tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { + 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 }); + tr = tr.addMark(pos, pos + node.nodeSize, link); + } + }); + this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); } } componentDidMount() { @@ -695,7 +706,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 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined; + return linkIndex !== -1 && marks[linkIndex].attrs.allRefs.find((item: { href: string }) => scrollToLinkID === item.href.replace(/.*\/doc\//, "")) ? node : undefined; }; let start = 0; @@ -893,17 +904,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp dispatchTransaction: this.dispatchTransaction, nodeViews: { dashComment(node, view, getPos) { return new DashDocCommentView(node, view, getPos); }, - dashField(node, view, getPos) { return new DashFieldView(node, view, getPos, self); }, dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); }, - // dashDoc(node, view, getPos) { return new DashDocView({ node, view, getPos, self }); }, - - // image(node, view, getPos) { - // //const addDocTab = this.props.addDocTab; - // return new ImageResizeView({ node, view, getPos, addDocTab: this.props.addDocTab }); - // }, - // // WAS : - // //image(node, view, getPos) { return new ImageResizeView(node, view, getPos, this.props.addDocTab); }, - + dashField(node, view, getPos) { return new DashFieldView(node, view, getPos, self); }, summary(node, view, getPos) { return new SummaryView(node, view, getPos); }, ordered_list(node, view, getPos) { return new OrderedListView(); }, footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); } @@ -987,7 +989,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp FormattedTextBox._downEvent = false; if (!(e.nativeEvent as any).formattedHandled) { FormattedTextBoxComment.textBox = this; - FormattedTextBoxComment.update(this._editorView!); + FormattedTextBoxComment.update(this._editorView!, undefined, (e.target as any)?.className === "prosemirror-dropdownlink" ? (e.target as any).href : ""); } (e.nativeEvent as any).formattedHandled = true; @@ -1164,7 +1166,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); this._lastTimedMark = mark; - this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); + // this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); @@ -1205,7 +1207,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); } sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); - sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()), 0); + sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()) / this.props.ContentScaling(), 0); @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); } render() { TraceMobx(); @@ -1213,9 +1215,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground; if (this.props.isSelected()) { - this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props); + setTimeout(() => this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props), 0); } else if (FormattedTextBoxComment.textBox === this) { - FormattedTextBoxComment.Hide(); + setTimeout(() => FormattedTextBoxComment.Hide(), 0); } return ( <div className={"formattedTextBox-cont"} style={{ @@ -1273,6 +1275,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp PanelWidth={this.sidebarWidth} NativeHeight={returnZero} NativeWidth={returnZero} + scaleField={this.annotationKey + "-scale"} annotationsKey={this.annotationKey} isAnnotationOverlay={false} focus={this.props.focus} diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 59a6045ab..000b3c2e5 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -50,7 +50,9 @@ export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (mark return after; } - +// this view appears when clicking on text that has a hyperlink which is configured to show a preview of its target. +// this will also display metadata information about text when the view is configured to display things like other people who authored text. +// export class FormattedTextBoxComment { static tooltip: HTMLElement; static tooltipText: HTMLElement; @@ -84,11 +86,13 @@ export class FormattedTextBoxComment { const keep = e.target && (e.target as any).type === "checkbox" ? true : false; const textBox = FormattedTextBoxComment.textBox; if (FormattedTextBoxComment.linkDoc && !keep && textBox) { - if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { - textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); - } else { - DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, - (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + if (FormattedTextBoxComment.linkDoc.author) { + if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { + textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); + } else { + DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, + (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + } } } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) { textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400, UseCors: true }), "onRight"); @@ -115,7 +119,24 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = ""); } - static update(view: EditorView, lastState?: EditorState) { + static showCommentbox(set: string, view: EditorView, nbef: number) { + const state = view.state; + if (set !== "none") { + // These are in screen coordinates + // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to); + const start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef); + // The box in which the tooltip is positioned, to use as base + const box = (document.getElementsByClassName("mainView-container") as any)[0].getBoundingClientRect(); + // Find a center-ish x position from the selection endpoints (when + // crossing lines, end may be more to the left) + const left = Math.max((start.left + end.left) / 2, start.left + 3); + FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px"; + FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px"; + } + FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set); + } + + static update(view: EditorView, lastState?: EditorState, forceUrl: string = "") { const state = view.state; // Don't do anything if the document/selection didn't change if (lastState && lastState.doc.eq(state.doc) && @@ -160,32 +181,34 @@ export class FormattedTextBoxComment { 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); - if (mark && child && nbef && naft && mark.attrs.showPreview) { - FormattedTextBoxComment.tooltipText.textContent = "external => " + mark.attrs.href; - (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href; - if (mark.attrs.href.startsWith("https://en.wikipedia.org/wiki/")) { - wiki().page(mark.attrs.href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(summary => FormattedTextBoxComment.tooltipText.textContent = summary.substring(0, 500))); + const href = 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; + if (href.startsWith("https://en.wikipedia.org/wiki/")) { + wiki().page(href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(summary => FormattedTextBoxComment.tooltipText.textContent = summary.substring(0, 500))); } else { FormattedTextBoxComment.tooltipText.style.whiteSpace = "pre"; FormattedTextBoxComment.tooltipText.style.overflow = "hidden"; } - if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) { + if (href.indexOf(Utils.prepend("/doc/")) === 0) { FormattedTextBoxComment.tooltipText.textContent = "target not found..."; (FormattedTextBoxComment.tooltipText as any).href = ""; - const docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + const docTarget = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; try { ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText); } catch (e) { } docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => { if (linkDoc instanceof Doc) { - (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href; + (FormattedTextBoxComment.tooltipText as any).href = href; FormattedTextBoxComment.linkDoc = linkDoc; const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc); const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; if (anchor !== target && anchor && target) { target._scrollY = NumCast(anchor?.y); } - if (target) { + if (target?.author) { + FormattedTextBoxComment.showCommentbox("", view, nbef); ReactDOM.render(<ContentFittingDocumentView Document={target} LibraryPath={emptyPath} @@ -214,28 +237,13 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.style.width = NumCast(target._width) ? `${NumCast(target._width)}` : "100%"; FormattedTextBoxComment.tooltip.style.height = NumCast(target._height) ? `${NumCast(target._height)}` : "100%"; } - // let ext = (target && target.type !== DocumentType.PDFANNO && Doc.fieldExtensionDoc(target, "data")) || target; // try guessing that the target doc's data is in the 'data' field. probably need an 'overviewLayout' and then just display the target Document .... - // let text = ext && StrCast(ext.text); - // ext && (FormattedTextBoxComment.tooltipText.textContent = (target && target.type === DocumentType.PDFANNO ? "Quoted from " : "") + "=> " + (text || StrCast(ext.title))); } }); } set = ""; } } - if (set !== "none") { - // These are in screen coordinates - // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to); - const start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef); - // The box in which the tooltip is positioned, to use as base - const box = (document.getElementsByClassName("mainView-container") as any)[0].getBoundingClientRect(); - // Find a center-ish x position from the selection endpoints (when - // crossing lines, end may be more to the left) - const left = Math.max((start.left + end.left) / 2, start.left + 3); - FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px"; - FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px"; - } - FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set); + FormattedTextBoxComment.showCommentbox(set, view, nbef); } destroy() { } diff --git a/src/client/views/nodes/formattedText/ImageResizeView.tsx b/src/client/views/nodes/formattedText/ImageResizeView.tsx deleted file mode 100644 index 401ecd7e6..000000000 --- a/src/client/views/nodes/formattedText/ImageResizeView.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { NodeSelection } from "prosemirror-state"; -import { Doc } from "../../../../fields/Doc"; -import { DocServer } from "../../../DocServer"; -import { DocumentManager } from "../../../util/DocumentManager"; -import React = require("react"); - -import { schema } from "./schema_rts"; - -interface IImageResizeView { - node: any; - view: any; - getPos: any; - addDocTab: any; -} - -export class ImageResizeView extends React.Component<IImageResizeView> { - constructor(props: IImageResizeView) { - super(props); - } - - onClickImg = (e: any) => { - e.stopPropagation(); - e.preventDefault(); - if (this.props.view.state.selection.node && this.props.view.state.selection.node.type !== this.props.view.state.schema.nodes.image) { - this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.view.state.selection.from - 2)))); - } - } - - onPointerDownImg = (e: any) => { - if (e.ctrlKey) { - e.preventDefault(); - e.stopPropagation(); - DocServer.GetRefField(this.props.node.attrs.docid).then(async linkDoc => - (linkDoc instanceof Doc) && - DocumentManager.Instance.FollowLink(linkDoc, this.props.view.state.schema.Document, - document => this.props.addDocTab(document, this.props.node.attrs.location ? this.props.node.attrs.location : "inTab"), false)); - } - } - - onPointerDownHandle = (e: any) => { - e.preventDefault(); - e.stopPropagation(); - const elementImage = document.getElementById("imageId") as HTMLElement; - const wid = Number(getComputedStyle(elementImage).width.replace(/px/, "")); - const hgt = Number(getComputedStyle(elementImage).height.replace(/px/, "")); - const startX = e.pageX; - const startWidth = parseFloat(this.props.node.attrs.width); - - const onpointermove = (e: any) => { - const elementOuter = document.getElementById("outerId") as HTMLElement; - - const currentX = e.pageX; - const diffInPx = currentX - startX; - elementOuter.style.width = `${startWidth + diffInPx}`; - elementOuter.style.height = `${(startWidth + diffInPx) * hgt / wid}`; - }; - - const onpointerup = () => { - document.removeEventListener("pointermove", onpointermove); - document.removeEventListener("pointerup", onpointerup); - const pos = this.props.view.state.selection.from; - const elementOuter = document.getElementById("outerId") as HTMLElement; - this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { ...this.props.node.attrs, width: elementOuter.style.width, height: elementOuter.style.height })); - this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(pos)))); - }; - - document.addEventListener("pointermove", onpointermove); - document.addEventListener("pointerup", onpointerup); - } - - selectNode() { - const elementImage = document.getElementById("imageId") as HTMLElement; - const elementHandle = document.getElementById("handleId") as HTMLElement; - - elementImage.classList.add("ProseMirror-selectednode"); - elementHandle.style.display = ""; - } - - deselectNode() { - const elementImage = document.getElementById("imageId") as HTMLElement; - const elementHandle = document.getElementById("handleId") as HTMLElement; - - elementImage.classList.remove("ProseMirror-selectednode"); - elementHandle.style.display = "none"; - } - - - render() { - - const outerStyle = { - width: this.props.node.attrs.width, - height: this.props.node.attrs.height, - display: "inline-block", - overflow: "hidden", - float: this.props.node.attrs.float - }; - - const imageStyle = { - width: "100%", - }; - - const handleStyle = { - position: "absolute", - width: "20px", - heiht: "20px", - backgroundColor: "blue", - borderRadius: "15px", - display: "none", - bottom: "-10px", - right: "-10px" - - }; - - - - return ( - <div id="outer" - style={outerStyle} - > - <img - id="imageId" - style={imageStyle} - src={this.props.node.src} - onClick={this.onClickImg} - onPointerDown={this.onPointerDownImg} - - > - </img> - <span - id="handleId" - onPointerDown={this.onPointerDownHandle} - > - - </span> - </div > - ); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/OrderedListView.tsx b/src/client/views/nodes/formattedText/OrderedListView.tsx new file mode 100644 index 000000000..c3595e59b --- /dev/null +++ b/src/client/views/nodes/formattedText/OrderedListView.tsx @@ -0,0 +1,8 @@ +export class OrderedListView { + + update(node: any) { + // if attr's of an ordered_list (e.g., bulletStyle) change, + // return false forces the dom node to be recreated which is necessary for the bullet labels to update + return false; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 0e3e7f91e..114288a9d 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -6,11 +6,11 @@ import { liftListItem, sinkListItem } from "./prosemirrorPatches.js"; import { splitListItem, wrapInList, } from "prosemirror-schema-list"; import { EditorState, Transaction, TextSelection } from "prosemirror-state"; import { SelectionManager } from "../../../util/SelectionManager"; -import { Docs } from "../../../documents/Documents"; import { NumCast, BoolCast, Cast, StrCast } from "../../../../fields/Types"; import { Doc } from "../../../../fields/Doc"; import { FormattedTextBox } from "./FormattedTextBox"; import { Id } from "../../../../fields/FieldSymbols"; +import { Docs } from "../../../documents/Documents"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -30,6 +30,7 @@ export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string) }); return tx2; }; + export default function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap { const keys: { [key: string]: any } = {}; @@ -42,77 +43,27 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any keys[key] = cmd; } + //History commands bind("Mod-z", undo); - bind("Shift-Mod-z", redo); bind("Backspace", undoInputRule); - + bind("Shift-Mod-z", redo); !mac && bind("Mod-y", redo); - bind("Alt-ArrowUp", joinUp); - bind("Alt-ArrowDown", joinDown); - bind("Mod-BracketLeft", lift); - bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); - (document.activeElement as any).blur?.(); - SelectionManager.DeselectAll(); - }); - + //Commands to modify Mark bind("Mod-b", toggleMark(schema.marks.strong)); bind("Mod-B", toggleMark(schema.marks.strong)); bind("Mod-e", toggleMark(schema.marks.em)); bind("Mod-E", toggleMark(schema.marks.em)); + bind("Mod-*", toggleMark(schema.marks.code)); + bind("Mod-u", toggleMark(schema.marks.underline)); bind("Mod-U", toggleMark(schema.marks.underline)); - bind("Mod-`", toggleMark(schema.marks.code)); - + //Commands for lists bind("Ctrl-.", wrapInList(schema.nodes.bullet_list)); - - bind("Ctrl-n", wrapInList(schema.nodes.ordered_list)); - - bind("Ctrl->", wrapIn(schema.nodes.blockquote)); - - // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - // let newNode = schema.nodes.footnote.create({}); - // if (dispatch && state.selection.from === state.selection.to) { - // let tr = state.tr; - // tr.replaceSelectionWith(newNode); // replace insertion with a footnote. - // dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display - // tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node) - // tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)))); - // return true; - // } - // return false; - // }); - - - const cmd = chainCommands(exitCode, (state, dispatch) => { - if (dispatch) { - dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView()); - return true; - } - return false; - }); - bind("Mod-Enter", cmd); - bind("Shift-Enter", cmd); - mac && bind("Ctrl-Enter", cmd); - - - bind("Shift-Ctrl-0", setBlockType(schema.nodes.paragraph)); - - bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block)); - - for (let i = 1; i <= 6; i++) { - bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i })); - } - - const hr = schema.nodes.horizontal_rule; - bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); - return true; - }); + bind("Ctrl-i", wrapInList(schema.nodes.ordered_list)); bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { const ref = state.selection; @@ -149,10 +100,46 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any console.log("bullet demote fail"); } }); - bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + + //Command to create a new Tab with a PDF of all the command shortcuts + bind("Mod-/", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + const newDoc = Docs.Create.PdfDocument("http://localhost:1050/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }); + props.addDocTab(newDoc, "onRight"); + }); + + //Commands to modify BlockType + bind("Ctrl->", wrapIn(schema.nodes.blockquote)); + bind("Alt-\\", setBlockType(schema.nodes.paragraph)); + bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block)); + + for (let i = 1; i <= 6; i++) { + bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i })); + } + + //Command to create a horizontal break line + const hr = schema.nodes.horizontal_rule; + bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); + return true; + }); + + //Command to unselect all + bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); + (document.activeElement as any).blur?.(); + SelectionManager.DeselectAll(); + }); + + const splitMetadata = (marks: any, tx: Transaction) => { + marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); + marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); + return tx; + }; + + const addTextOnRight = (force: boolean) => { const layoutDoc = props.Document; const originalDoc = layoutDoc.rootDocument || layoutDoc; - if (originalDoc instanceof Doc) { + if (force || props.Document._singleLine) { const layoutKey = StrCast(originalDoc.layoutKey); const newDoc = Doc.MakeCopy(originalDoc, true); newDoc.y = NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10; @@ -162,18 +149,21 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any Doc.GetProto(newDoc).text = undefined; FormattedTextBox.SelectOnLoad = newDoc[Id]; props.addDocument(newDoc); + return true; } + return false; + }; + + //Command to create a text document to the right of the selected textbox + bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { + return addTextOnRight(true); }); - const splitMetadata = (marks: any, tx: Transaction) => { - marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); - marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); - return tx; - }; - const addTextOnRight = (force: boolean) => { + //Command to create a text document to the bottom of the selected textbox + bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { const layoutDoc = props.Document; const originalDoc = layoutDoc.rootDocument || layoutDoc; - if (force || props.Document._singleLine) { + if (originalDoc instanceof Doc) { const layoutKey = StrCast(originalDoc.layoutKey); const newDoc = Doc.MakeCopy(originalDoc, true); newDoc.x = NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10; @@ -183,13 +173,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any Doc.GetProto(newDoc).text = undefined; FormattedTextBox.SelectOnLoad = newDoc[Id]; props.addDocument(newDoc); - return true; } - return false; - }; - bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { - return addTextOnRight(true); }); + + //command to break line bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { if (addTextOnRight(false)) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); @@ -205,31 +192,73 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any } return true; }); + + //Command to create a blank space bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); dispatch(splitMetadata(marks, state.tr)); return false; }); + + bind("Alt-ArrowUp", joinUp); + bind("Alt-ArrowDown", joinDown); + bind("Mod-BracketLeft", lift); + + const cmd = chainCommands(exitCode, (state, dispatch) => { + if (dispatch) { + dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView()); + return true; + } + return false; + }); + + // mac && bind("Ctrl-Enter", cmd); + // bind("Mod-Enter", cmd); + bind("Shift-Enter", cmd); + + bind(":", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { const range = state.selection.$from.blockRange(state.selection.$to, (node: any) => { return !node.marks || !node.marks.find((m: any) => m.type === schema.marks.metadata); }); + const path = (state.doc.resolve(state.selection.from - 1) as any).path; + const spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1; + const anchor = range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator; + if (anchor >= 0) { + const textsel = TextSelection.create(state.doc, anchor, range!.end); + const text = range ? state.doc.textBetween(textsel.from, textsel.to) : ""; + let whitespace = text.length - 1; + for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { } if (text.endsWith(":")) { dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any). addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any)); } } + return false; }); + // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + // let newNode = schema.nodes.footnote.create({}); + // if (dispatch && state.selection.from === state.selection.to) { + // let tr = state.tr; + // tr.replaceSelectionWith(newNode); // replace insertion with a footnote. + // dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display + // tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node) + // tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)))); + // return true; + // } + // return false; + // }); return keys; } + diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index fd1b26208..03d393cde 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -631,7 +631,7 @@ export default class RichTextMenu extends AntimodeMenu { const node = this.view.state.selection.$from.nodeAfter; const link = node && node.marks.find(m => m.type.name === "link"); if (link) { - const href = link.attrs.href; + const href = link.attrs.allHrefs.length > 0 ? link.attrs.allHrefs[0].href : undefined; if (href) { if (href.indexOf(Utils.prepend("/doc/")) === 0) { const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; @@ -677,7 +677,7 @@ export default class RichTextMenu extends AntimodeMenu { const node = this.view.state.selection.$from.nodeAfter; const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link); - const href = link!.attrs.href; + const href = link!.attrs.allHrefs.length > 0 ? link!.attrs.allHrefs[0].href : undefined; if (href) { if (href.indexOf(Utils.prepend("/doc/")) === 0) { const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; @@ -705,8 +705,8 @@ export default class RichTextMenu extends AntimodeMenu { let startIndex = $start.index(); let endIndex = $start.indexAfter(); - while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.href === href).length) startIndex--; - while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.href === href).length) endIndex++; + while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allHrefs.find((item: { href: string }) => item.href === href)).length) startIndex--; + while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allHrefs.find((item: { href: string }) => item.href === href)).length) endIndex++; let startPos = $start.start(); let endPos = startPos; diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index fbd6c87bb..91187edf9 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -30,7 +30,7 @@ export class RichTextRules { // > blockquote wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote), - // 1. ordered list + // 1. create numerical ordered list wrappingInputRule( /^1\.\s$/, schema.nodes.ordered_list, @@ -42,49 +42,29 @@ export class RichTextRules { }, (type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } }) ), - // a. alphabbetical list + + // A. create alphabetical ordered list wrappingInputRule( - /^a\.\s$/, + /^A\.\s$/, schema.nodes.ordered_list, // match => { () => { - return ({ mapStyle: "alpha", bulletStyle: 1 }); + return ({ mapStyle: "multi", bulletStyle: 1 }); // return ({ order: +match[1] }) }, (match: any, node: any) => { return node.childCount + node.attrs.order === +match[1]; }, - (type: any) => ({ type: type, attrs: { mapStyle: "alpha", bulletStyle: 1 } }) + (type: any) => ({ type: type, attrs: { mapStyle: "multi", bulletStyle: 1 } }) ), - // * bullet list + // * + - create bullet list wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list), - // ``` code block + // ``` create code block textblockTypeInputRule(/^```$/, schema.nodes.code_block), - // create an inline view of a tag stored under the '#' field - new InputRule( - new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_;\-0-9]*)\s$/), - (state, match, start, end) => { - const tag = match[1]; - if (!tag) return state.tr; - const multiple = tag.split(";"); - this.Document[DataSym]["#"] = multiple.length > 1 ? new List(multiple) : tag; - const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" }); - return state.tr.deleteRange(start, end).insert(start, fieldView); - }), - - // # heading - textblockTypeInputRule( - new RegExp(/^(#{1,6})\s$/), - schema.nodes.heading, - match => { - return ({ level: match[1].length }); - } - ), - - // set the font size using #<font-size> + // %<font-size> set the font size new InputRule( new RegExp(/%([0-9]+)\s$/), (state, match, start, end) => { @@ -92,51 +72,7 @@ export class RichTextRules { return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size })); }), - // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document [[ <fieldKey> : <Doc>]] // [[:Doc]] => hyperlink [[fieldKey]] => show field [[fieldKey:Doc]] => show field of doc - new InputRule( - new RegExp(/\[\[([a-zA-Z_@\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@\? \-0-9]+)?\]\]$/), - (state, match, start, end) => { - const fieldKey = match[1]; - const docid = match[3]?.substring(1); - const value = match[2]?.substring(1); - if (!fieldKey) { - if (docid) { - DocServer.GetRefField(docid).then(docx => { - const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid); - DocUtils.Publish(target, docid, returnFalse, returnFalse); - DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to"); - }); - const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid }); - return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link); - } - return state.tr; - } - if (value !== "" && value !== undefined) { - const num = value.match(/^[0-9.]$/); - this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : (num ? Number(value) : value); - } - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid }); - return state.tr.deleteRange(start, end).insert(start, fieldView); - }), - // create an inline view of a document {{ <layoutKey> : <Doc> }} // {{:Doc}} => show default view of document {{<layout>}} => show layout for this doc {{<layout> : Doc}} => show layout for another doc - new InputRule( - new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._/\-]*\))?(:[a-zA-Z_ \-0-9]+)?\}\}$/), - (state, match, start, end) => { - const fieldKey = match[1] || ""; - const fieldParam = match[2]?.replace("…", "...") || ""; - const docid = match[3]?.substring(1); - if (!fieldKey && !docid) return state.tr; - docid && DocServer.GetRefField(docid).then(docx => { - if (!(docx instanceof Doc && docx)) { - const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true }, docid); - DocUtils.Publish(docx, docid, returnFalse, returnFalse); - } - }); - const node = (state.doc.resolve(start) as any).nodeAfter; - const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid, fieldKey: fieldKey + fieldParam, float: "unset", alias: Utils.GenerateGuid() }); - const sm = state.storedMarks || undefined; - return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; - }), + //Create annotation to a field on the text document new InputRule( new RegExp(/>>$/), (state, match, start, end) => { @@ -161,25 +97,7 @@ export class RichTextRules { state.tr; return replaced; }), - // stop using active style - new InputRule( - new RegExp(/%%$/), - (state, match, start, end) => { - const tr = state.tr.deleteRange(start, end); - const marks = state.tr.selection.$anchor.nodeBefore?.marks; - return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr; - }), - // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode) - new InputRule( - new RegExp(/[ti!x]$/), - (state, match, start, end) => { - if (state.selection.to === state.selection.from || !this.EnteringStyle) return null; - const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??"; - const node = (state.doc.resolve(start) as any).nodeAfter; - if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag); - return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; - }), // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode) new InputRule( @@ -214,6 +132,7 @@ export class RichTextRules { } return null; }), + // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) new InputRule( new RegExp(/(%q|q)$/), @@ -235,56 +154,56 @@ export class RichTextRules { return null; }), - // center justify text new InputRule( - new RegExp(/%\^$/), + new RegExp(/%\^/), (state, match, start, end) => { - const node = (state.doc.resolve(start) as any).nodeAfter; - const sm = state.storedMarks || undefined; - const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + const resolved = state.doc.resolve(start) as any; + if (resolved?.parent.type.name === "paragraph") { + return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: "center" }, resolved.parent.marks); + } else { + const node = resolved.nodeAfter; + const sm = state.storedMarks || undefined; + const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + } }), + // left justify text new InputRule( - new RegExp(/%\[$/), + new RegExp(/%\[/), (state, match, start, end) => { - const node = (state.doc.resolve(start) as any).nodeAfter; - const sm = state.storedMarks || undefined; - const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + const resolved = state.doc.resolve(start) as any; + if (resolved?.parent.type.name === "paragraph") { + return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: "left" }, resolved.parent.marks); + } else { + const node = resolved.nodeAfter; + const sm = state.storedMarks || undefined; + const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + } }), + // right justify text new InputRule( - new RegExp(/%\]$/), - (state, match, start, end) => { - const node = (state.doc.resolve(start) as any).nodeAfter; - const sm = state.storedMarks || undefined; - const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); - }), - new InputRule( - new RegExp(/%\(/), - (state, match, start, end) => { - const node = (state.doc.resolve(start) as any).nodeAfter; - const sm = state.storedMarks || []; - const mark = state.schema.marks.summarizeInclusive.create(); - sm.push(mark); - const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark); - const content = selected.selection.content(); - const replaced = node ? selected.replaceRangeWith(start, end, - schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]); - }), - new InputRule( - new RegExp(/%\)/), + new RegExp(/%\]/), (state, match, start, end) => { - return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create()); + const resolved = state.doc.resolve(start) as any; + if (resolved?.parent.type.name === "paragraph") { + return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: "right" }, resolved.parent.marks); + } else { + const node = resolved.nodeAfter; + const sm = state.storedMarks || undefined; + const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + } }), + + + // %f create footnote new InputRule( new RegExp(/%f$/), (state, match, start, end) => { @@ -296,26 +215,160 @@ export class RichTextRules { tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))); }), - // activate a style by name using prefix '%' + // activate a style by name using prefix '%<color name>' new InputRule( new RegExp(/%[a-z]+$/), (state, match, start, end) => { + const color = match[0].substring(1, match[0].length); const marks = RichTextMenu.Instance._brushMap.get(color); + if (marks) { const tr = state.tr.deleteRange(start, end); return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr; } + const isValidColor = (strColor: string) => { const s = new Option().style; s.color = strColor; return s.color === strColor.toLowerCase(); // 'false' if color wasn't assigned }; + if (isValidColor(color)) { return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color })); } + return null; }), + + // stop using active style + new InputRule( + new RegExp(/%%$/), + (state, match, start, end) => { + + const tr = state.tr.deleteRange(start, end); + const marks = state.tr.selection.$anchor.nodeBefore?.marks; + + return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr; + }), + + // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document + // [[ <fieldKey> : <Doc>]] + // [[:Doc]] => hyperlink + // [[fieldKey]] => show field + // [[fieldKey:Doc]] => show field of doc + new InputRule( + new RegExp(/\[\[([a-zA-Z_@\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@\? \-0-9]+)?\]\]$/), + (state, match, start, end) => { + const fieldKey = match[1]; + const docid = match[3]?.substring(1); + const value = match[2]?.substring(1); + if (!fieldKey) { + if (docid) { + DocServer.GetRefField(docid).then(docx => { + const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid); + DocUtils.Publish(target, docid, returnFalse, returnFalse); + DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to"); + }); + const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid }); + return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link); + } + return state.tr; + } + if (value !== "" && value !== undefined) { + const num = value.match(/^[0-9.]$/); + this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : (num ? Number(value) : value); + } + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid }); + return state.tr.deleteRange(start, end).insert(start, fieldView); + }), + + // create an inline view of a document {{ <layoutKey> : <Doc> }} + // {{:Doc}} => show default view of document + // {{<layout>}} => show layout for this doc + // {{<layout> : Doc}} => show layout for another doc + new InputRule( + new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._/\-]*\))?(:[a-zA-Z_ \-0-9]+)?\}\}$/), + (state, match, start, end) => { + const fieldKey = match[1] || ""; + const fieldParam = match[2]?.replace("…", "...") || ""; + const docid = match[3]?.substring(1); + if (!fieldKey && !docid) return state.tr; + docid && DocServer.GetRefField(docid).then(docx => { + if (!(docx instanceof Doc && docx)) { + const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true }, docid); + DocUtils.Publish(docx, docid, returnFalse, returnFalse); + } + }); + const node = (state.doc.resolve(start) as any).nodeAfter; + const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid, fieldKey: fieldKey + fieldParam, float: "unset", alias: Utils.GenerateGuid() }); + const sm = state.storedMarks || undefined; + return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; + }), + + + + // create an inline view of a tag stored under the '#' field + new InputRule( + new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_;\-0-9]*)\s$/), + (state, match, start, end) => { + const tag = match[1]; + if (!tag) return state.tr; + const multiple = tag.split(";"); + this.Document[DataSym]["#"] = multiple.length > 1 ? new List(multiple) : tag; + const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" }); + return state.tr.deleteRange(start, end).insert(start, fieldView); + }), + + + // # heading + textblockTypeInputRule( + new RegExp(/^(#{1,6})\s$/), + schema.nodes.heading, + match => { + return ({ level: match[1].length }); + } + ), + + // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode) + new InputRule( + new RegExp(/[ti!x]$/), + (state, match, start, end) => { + + if (state.selection.to === state.selection.from || !this.EnteringStyle) return null; + + const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??"; + const node = (state.doc.resolve(start) as any).nodeAfter; + + if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag); + + return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; + }), + + new InputRule( + new RegExp(/%\(/), + (state, match, start, end) => { + const node = (state.doc.resolve(start) as any).nodeAfter; + const sm = state.storedMarks || []; + const mark = state.schema.marks.summarizeInclusive.create(); + + sm.push(mark); + const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark); + const content = selected.selection.content(); + const replaced = node ? selected.replaceRangeWith(start, end, + schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : + state.tr; + + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]); + }), + + new InputRule( + new RegExp(/%\)/), + (state, match, start, end) => { + + return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create()); + }), + ] }; } diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx index 1cc7ec8bf..05557e22a 100644 --- a/src/client/views/nodes/formattedText/RichTextSchema.tsx +++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx @@ -1,188 +1,19 @@ -import { IReactionDisposer, observable, reaction, runInAction } from "mobx"; -import { baseKeymap, toggleMark } from "prosemirror-commands"; -import { redo, undo } from "prosemirror-history"; -import { keymap } from "prosemirror-keymap"; -import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model"; -import { bulletList, listItem, orderedList } from 'prosemirror-schema-list'; -import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state"; -import { StepMap } from "prosemirror-transform"; -import { EditorView } from "prosemirror-view"; +import { IReactionDisposer, reaction } from "mobx"; +import { NodeSelection } from "prosemirror-state"; import * as ReactDOM from 'react-dom'; -import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../fields/Doc"; +import { Doc, HeightSym, WidthSym } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; -import { List } from "../../../../fields/List"; import { ObjectField } from "../../../../fields/ObjectField"; -import { listSpec } from "../../../../fields/Schema"; -import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; import { ComputedField } from "../../../../fields/ScriptField"; -import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; +import { emptyFunction, returnEmptyString, returnFalse, returnZero, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; -import { CollectionViewType } from "../../collections/CollectionView"; +import { Transform } from "../../../util/Transform"; import { DocumentView } from "../DocumentView"; import { FormattedTextBox } from "./FormattedTextBox"; -import { DocumentManager } from "../../../util/DocumentManager"; -import { Transform } from "../../../util/Transform"; import React = require("react"); -import { schema } from "./schema_rts"; - -export class OrderedListView { - update(node: any) { - return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update - } -} - -export class ImageResizeView { - _handle: HTMLElement; - _img: HTMLElement; - _outer: HTMLElement; - constructor(node: any, view: any, getPos: any, addDocTab: any) { - //moved - this._handle = document.createElement("span"); - this._img = document.createElement("img"); - this._outer = document.createElement("span"); - this._outer.style.position = "relative"; - this._outer.style.width = node.attrs.width; - this._outer.style.height = node.attrs.height; - this._outer.style.display = "inline-block"; - this._outer.style.overflow = "hidden"; - (this._outer.style as any).float = node.attrs.float; - //moved - this._img.setAttribute("src", node.attrs.src); - this._img.style.width = "100%"; - this._handle.style.position = "absolute"; - this._handle.style.width = "20px"; - this._handle.style.height = "20px"; - this._handle.style.backgroundColor = "blue"; - this._handle.style.borderRadius = "15px"; - this._handle.style.display = "none"; - this._handle.style.bottom = "-10px"; - this._handle.style.right = "-10px"; - const self = this; - //moved - this._img.onclick = function (e: any) { - e.stopPropagation(); - e.preventDefault(); - if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image) { - view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2)))); - } - }; - //moved - this._img.onpointerdown = function (e: any) { - if (e.ctrlKey) { - e.preventDefault(); - e.stopPropagation(); - DocServer.GetRefField(node.attrs.docid).then(async linkDoc => - (linkDoc instanceof Doc) && - DocumentManager.Instance.FollowLink(linkDoc, view.state.schema.Document, - document => addDocTab(document, node.attrs.location ? node.attrs.location : "inTab"), false)); - } - }; - //moved - this._handle.onpointerdown = function (e: any) { - e.preventDefault(); - e.stopPropagation(); - const wid = Number(getComputedStyle(self._img).width.replace(/px/, "")); - const hgt = Number(getComputedStyle(self._img).height.replace(/px/, "")); - const startX = e.pageX; - const startWidth = parseFloat(node.attrs.width); - const onpointermove = (e: any) => { - const currentX = e.pageX; - const diffInPx = currentX - startX; - self._outer.style.width = `${startWidth + diffInPx}`; - self._outer.style.height = `${(startWidth + diffInPx) * hgt / wid}`; - }; - - const onpointerup = () => { - document.removeEventListener("pointermove", onpointermove); - document.removeEventListener("pointerup", onpointerup); - const pos = view.state.selection.from; - view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height })); - view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(pos)))); - }; - - document.addEventListener("pointermove", onpointermove); - document.addEventListener("pointerup", onpointerup); - }; - //Moved - this._outer.appendChild(this._img); - this._outer.appendChild(this._handle); - (this as any).dom = this._outer; - } - - selectNode() { - this._img.classList.add("ProseMirror-selectednode"); - - this._handle.style.display = ""; - } - - deselectNode() { - this._img.classList.remove("ProseMirror-selectednode"); - - this._handle.style.display = "none"; - } -} - -export class DashDocCommentView { - _collapsed: HTMLElement; - _view: any; - constructor(node: any, view: any, getPos: any) { - //moved - this._collapsed = document.createElement("span"); - this._collapsed.className = "formattedTextBox-inlineComment"; - this._collapsed.id = "DashDocCommentView-" + node.attrs.docid; - this._view = view; - //moved - const targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor - for (let i = getPos() + 1; i < view.state.doc.content.size; i++) { - const m = view.state.doc.nodeAt(i); - if (m && m.type === view.state.schema.nodes.dashDoc && m.attrs.docid === node.attrs.docid) { - return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean }; - } - } - const dashDoc = view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: node.attrs.docid, float: "right" }); - view.dispatch(view.state.tr.insert(getPos() + 1, dashDoc)); - setTimeout(() => { try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + 2))); } catch (e) { } }, 0); - return undefined; - }; - //moved - this._collapsed.onpointerdown = (e: any) => { - e.stopPropagation(); - }; - //moved - this._collapsed.onpointerup = (e: any) => { - const target = targetNode(); - if (target) { - const expand = target.hidden; - const tr = view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true }); - view.dispatch(tr.setSelection(TextSelection.create(tr.doc, getPos() + (expand ? 2 : 1)))); // update the attrs - setTimeout(() => { - expand && DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); - try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + (expand ? 2 : 1)))); } catch (e) { } - }, 0); - } - e.stopPropagation(); - }; - //moved - this._collapsed.onpointerenter = (e: any) => { - DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); - e.preventDefault(); - e.stopPropagation(); - }; - //moved - this._collapsed.onpointerleave = (e: any) => { - DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); - e.preventDefault(); - e.stopPropagation(); - }; - - (this as any).dom = this._collapsed; - } - //moved - selectNode() { } -} export class DashDocView { _dashSpan: HTMLDivElement; @@ -192,16 +23,22 @@ export class DashDocView { _renderDisposer: IReactionDisposer | undefined; _textBox: FormattedTextBox; + //moved getDocTransform = () => { const { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer); return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale); } + + //moved contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1; + //moved outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + //moved this._textBox = tbox; + this._dashSpan = document.createElement("div"); this._outer = document.createElement("span"); this._outer.style.position = "relative"; @@ -218,28 +55,35 @@ export class DashDocView { this._dashSpan.style.position = "absolute"; this._dashSpan.style.display = "inline-block"; this._dashSpan.style.whiteSpace = "normal"; + this._dashSpan.onpointerleave = () => { const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid); if (ele) { (ele as HTMLDivElement).style.backgroundColor = ""; } }; + this._dashSpan.onpointerenter = () => { const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid); if (ele) { (ele as HTMLDivElement).style.backgroundColor = "orange"; } }; + const removeDoc = () => { + console.log("DashDocView.removeDoc"); // SMM const pos = getPos(); const ns = new NodeSelection(view.state.doc.resolve(pos)); view.dispatch(view.state.tr.setSelection(ns).deleteSelection()); return true; }; - const alias = node.attrs.alias; + const alias = node.attrs.alias; + const self = this; const docid = node.attrs.docid || tbox.props.Document[Id];// tbox.props.DataDoc?.[Id] || tbox.dataDoc?.[Id]; + DocServer.GetRefField(docid + alias).then(async dashDoc => { + if (!(dashDoc instanceof Doc)) { alias && DocServer.GetRefField(docid).then(async dashDocBase => { if (dashDocBase instanceof Doc) { @@ -253,7 +97,8 @@ export class DashDocView { self.doRender(dashDoc, removeDoc, node, view, getPos); } }); - const self = this; + + this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); if (e.key === "Tab" || e.key === "Enter") { @@ -281,6 +126,7 @@ export class DashDocView { this._dashSpan.style.height = this._outer.style.height = Math.max(20, dim[1]) + "px"; this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")); }, { fireImmediately: true }); + const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => { ReactDOM.unmountComponentAtNode(this._dashSpan); @@ -310,6 +156,7 @@ export class DashDocView { ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc} ContentScaling={this.contentScaling} />, this._dashSpan); + if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") { try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" })); @@ -318,6 +165,7 @@ export class DashDocView { } } }; + this._renderDisposer?.(); this._renderDisposer = reaction(() => { // if (!Doc.AreProtosEqual(finalLayout, dashDoc)) { @@ -337,202 +185,9 @@ export class DashDocView { { fireImmediately: true }); } } + destroy() { ReactDOM.unmountComponentAtNode(this._dashSpan); this._reactionDisposer?.(); } } - -export class FootnoteView { - innerView: any; - outerView: any; - node: any; - dom: any; - getPos: any; - - constructor(node: any, view: any, getPos: any) { - // We'll need these later - this.node = node; - this.outerView = view; - this.getPos = getPos; - - // The node's representation in the editor (empty, for now) - this.dom = document.createElement("footnote"); - this.dom.addEventListener("pointerup", this.toggle, true); - // These are used when the footnote is selected - this.innerView = null; - } - selectNode() { - const attrs = { ...this.node.attrs }; - attrs.visibility = true; - this.dom.classList.add("ProseMirror-selectednode"); - if (!this.innerView) this.open(); - } - - deselectNode() { - const attrs = { ...this.node.attrs }; - attrs.visibility = false; - this.dom.classList.remove("ProseMirror-selectednode"); - if (this.innerView) this.close(); - } - open() { - // Append a tooltip to the outer node - const tooltip = this.dom.appendChild(document.createElement("div")); - tooltip.className = "footnote-tooltip"; - // And put a sub-ProseMirror into that - this.innerView = new EditorView(tooltip, { - // You can use any node as an editor document - state: EditorState.create({ - doc: this.node, - plugins: [keymap(baseKeymap), - keymap({ - "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch), - "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch), - "Mod-b": toggleMark(schema.marks.strong) - }), - // new Plugin({ - // view(newView) { - // // TODO -- make this work with RichTextMenu - // // return FormattedTextBox.getToolTip(newView); - // } - // }) - ], - - }), - // This is the magic part - dispatchTransaction: this.dispatchInner.bind(this), - handleDOMEvents: { - pointerdown: ((view: any, e: PointerEvent) => { - // Kludge to prevent issues due to the fact that the whole - // footnote is node-selected (and thus DOM-selected) when - // the parent editor is focused. - e.stopPropagation(); - document.addEventListener("pointerup", this.ignore, true); - if (this.outerView.hasFocus()) this.innerView.focus(); - }) as any - } - - }); - setTimeout(() => this.innerView && this.innerView.docView.setSelection(0, 0, this.innerView.root, true), 0); - } - - ignore = (e: PointerEvent) => { - e.stopPropagation(); - document.removeEventListener("pointerup", this.ignore, true); - } - - toggle = () => { - if (this.innerView) this.close(); - else { - this.open(); - } - } - close() { - this.innerView && this.innerView.destroy(); - this.innerView = null; - this.dom.textContent = ""; - } - - dispatchInner(tr: any) { - const { state, transactions } = this.innerView.state.applyTransaction(tr); - this.innerView.updateState(state); - - if (!tr.getMeta("fromOutside")) { - const outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1); - for (const transaction of transactions) { - const steps = transaction.steps; - for (const step of steps) { - outerTr.step(step.map(offsetMap)); - } - } - if (outerTr.docChanged) this.outerView.dispatch(outerTr); - } - } - update(node: any) { - if (!node.sameMarkup(this.node)) return false; - this.node = node; - if (this.innerView) { - const state = this.innerView.state; - const start = node.content.findDiffStart(state.doc.content); - if (start !== null) { - let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content); - const overlap = start - Math.min(endA, endB); - if (overlap > 0) { endA += overlap; endB += overlap; } - this.innerView.dispatch( - state.tr - .replace(start, endB, node.slice(start, endA)) - .setMeta("fromOutside", true)); - } - } - return true; - } - - destroy() { - if (this.innerView) this.close(); - } - - stopEvent(event: any) { - return this.innerView && this.innerView.dom.contains(event.target); - } - - ignoreMutation() { return true; } -} - -export class SummaryView { - _collapsed: HTMLElement; - _view: any; - constructor(node: any, view: any, getPos: any) { - this._collapsed = document.createElement("span"); - this._collapsed.className = this.className(node.attrs.visibility); - this._view = view; - const js = node.toJSON; - node.toJSON = function () { - return js.apply(this, arguments); - }; - - this._collapsed.onpointerdown = (e: any) => { - const visible = !node.attrs.visibility; - const attrs = { ...node.attrs, visibility: visible }; - let textSelection = TextSelection.create(view.state.doc, getPos() + 1); - if (!visible) { // update summarized text and save in attrs - textSelection = this.updateSummarizedText(getPos() + 1); - attrs.text = textSelection.content(); - attrs.textslice = attrs.text.toJSON(); - } - view.dispatch(view.state.tr. - setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed) - replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it - setNodeMarkup(getPos(), undefined, attrs)); // update the attrs - e.preventDefault(); - e.stopPropagation(); - this._collapsed.className = this.className(visible); - }; - (this as any).dom = this._collapsed; - } - selectNode() { } - - deselectNode() { } - - className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed"); - - updateSummarizedText(start?: any) { - const mtype = this._view.state.schema.marks.summarize; - const mtypeInc = this._view.state.schema.marks.summarizeInclusive; - let endPos = start; - - const visited = new Set(); - for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) { - let skip = false; - this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => { - if (node.isLeaf && !visited.has(node) && !skip) { - if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) { - visited.add(node); - endPos = i + node.nodeSize - 1; - } - else skip = true; - } - }); - } - return TextSelection.create(this._view.state.doc, start, endPos); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx index 89908d8ee..922285bd0 100644 --- a/src/client/views/nodes/formattedText/SummaryView.tsx +++ b/src/client/views/nodes/formattedText/SummaryView.tsx @@ -1,81 +1,81 @@ import { TextSelection } from "prosemirror-state"; import { Fragment, Node, Slice } from "prosemirror-model"; - +import * as ReactDOM from 'react-dom'; import React = require("react"); -interface ISummaryView { - node: any; - view: any; - getPos: any; - self: any; -} -export class SummaryView extends React.Component<ISummaryView> { +// an elidable textblock that collapses when its '<-' is clicked and expands when its '...' anchor is clicked. +// this node actively edits prosemirror (as opposed to just changing how things are rendered) and thus doesn't +// really need a react view. However, it would be cleaner to figure out how to do this just as a react rendering +// method instead of changing prosemirror's text when the expand/elide buttons are clicked. +export class SummaryView { + _fieldWrapper: HTMLSpanElement; // container for label and value - onPointerDown = (e: any) => { - const visible = !this.props.node.attrs.visibility; - const attrs = { ...this.props.node.attrs, visibility: visible }; - let textSelection = TextSelection.create(this.props.view.state.doc, this.props.getPos() + 1); - if (!visible) { // update summarized text and save in attrs - textSelection = this.updateSummarizedText(this.props.getPos() + 1); - attrs.text = textSelection.content(); - attrs.textslice = attrs.text.toJSON(); - } - this.props.view.dispatch(this.props.view.state.tr. - setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed) - replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : this.props.node.attrs.text). // collapse/expand it - setNodeMarkup(this.props.getPos(), undefined, attrs)); // update the attrs - e.preventDefault(); - e.stopPropagation(); - const _collapsed = document.getElementById('collapse') as HTMLElement; - _collapsed.className = this.className(visible); + constructor(node: any, view: any, getPos: any) { + const self = this; + this._fieldWrapper = document.createElement("span"); + this._fieldWrapper.className = this.className(node.attrs.visibility); + this._fieldWrapper.onpointerdown = function (e: any) { self.onPointerDown(e, node, view, getPos); } + this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); }; + + const js = node.toJSON; + node.toJSON = function () { return js.apply(this, arguments); }; + + ReactDOM.render(<SummaryViewInternal />, this._fieldWrapper); + (this as any).dom = this._fieldWrapper; } - updateSummarizedText(start?: any) { - const mtype = this.props.view.state.schema.marks.summarize; - const mtypeInc = this.props.view.state.schema.marks.summarizeInclusive; + className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed"); + destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); } + selectNode() { } + + updateSummarizedText(start: any, view: any) { + const mtype = view.state.schema.marks.summarize; + const mtypeInc = view.state.schema.marks.summarizeInclusive; let endPos = start; const visited = new Set(); - for (let i: number = start + 1; i < this.props.view.state.doc.nodeSize - 1; i++) { + for (let i: number = start + 1; i < view.state.doc.nodeSize - 1; i++) { let skip = false; - this.props.view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => { - if (this.props.node.isLeaf && !visited.has(node) && !skip) { - if (this.props.node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) { + view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => { + if (node.isLeaf && !visited.has(node) && !skip) { + if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) { visited.add(node); - endPos = i + this.props.node.nodeSize - 1; + endPos = i + node.nodeSize - 1; } else skip = true; } }); } - return TextSelection.create(this.props.view.state.doc, start, endPos); + return TextSelection.create(view.state.doc, start, endPos); } - className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed"); - - selectNode() { } - - deselectNode() { } + onPointerDown = (e: any, node: any, view: any, getPos: any) => { + const visible = !node.attrs.visibility; + const attrs = { ...node.attrs, visibility: visible }; + let textSelection = TextSelection.create(view.state.doc, getPos() + 1); + if (!visible) { // update summarized text and save in attrs + textSelection = this.updateSummarizedText(getPos() + 1, view); + attrs.text = textSelection.content(); + attrs.textslice = attrs.text.toJSON(); + } + view.dispatch(view.state.tr. + setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed) + replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it + setNodeMarkup(getPos(), undefined, attrs)); // update the attrs + e.preventDefault(); + e.stopPropagation(); + this._fieldWrapper.className = this.className(visible); + } +} +interface ISummaryView { +} +// currently nothing needs to be rendered for the internal view of a summary. +export class SummaryViewInternal extends React.Component<ISummaryView> { render() { - const _view = this.props.node.view; - const js = this.props.node.toJSon; - - this.props.node.toJSON = function () { - return js.apply(this, arguments); - }; - - const spanCollapsedClassName = this.className(this.props.node.attrs.visibility); - - return ( - <span - className={spanCollapsedClassName} - id='collapse' - onPointerDown={this.onPointerDown} - > - - </span> - ); - + return <> </>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index ebaa23e99..49d5c96a4 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -9,14 +9,20 @@ const codeDOM: DOMOutputSpecArray = ["code", 0]; // :: Object [Specs](#model.MarkSpec) for the marks in the schema. export const marks: { [index: string]: MarkSpec } = { + splitter: { + attrs: { + id: { default: "" } + }, + toDOM(node: any) { + return ["div", { className: "dummy" }, 0]; + } + }, // :: MarkSpec A link. Has `href` and `title` attributes. `title` // defaults to the empty string. Rendered and parsed as an `<a>` // element. link: { attrs: { - href: {}, - targetId: { default: "" }, - linkId: { default: "" }, + allHrefs: { default: [] as { href: string, title: string, linkId: string, targetId: string }[] }, showPreview: { default: true }, location: { default: null }, title: { default: null }, @@ -25,13 +31,25 @@ export const marks: { [index: string]: MarkSpec } = { inclusive: false, parseDOM: [{ tag: "a[href]", getAttrs(dom: any) { - return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title"), targetId: dom.getAttribute("id") }; + return { allHrefs: [{ href: dom.getAttribute("href"), title: dom.getAttribute("title"), linkId: dom.getAttribute("linkids"), targetId: dom.getAttribute("targetids") }], location: dom.getAttribute("location"), }; } }], toDOM(node: any) { + 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"]] : - ["a", { ...node.attrs, id: node.attrs.linkId + node.attrs.targetId, title: `${node.attrs.title}` }, 0]; + 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" }, + ["span", { class: "prosemirror-linkBtn" }, + ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}` }, 0], + ["input", { class: "prosemirror-hrefoptions" }], + ], + ["div", { class: "prosemirror-links" }, ...node.attrs.allHrefs.map((item: { href: string, title: string }) => + ["a", { class: "prosemirror-dropdownlink", href: item.href }, item.title] + )] + ]; } }, diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index e39e96607..c02dd6786 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -126,16 +126,24 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu // file address of the pdf const { url: { href } } = Cast(this.dataDoc[this.props.fieldKey], PdfField)!; const { url: relative } = this.props; - const pathComponents = relative.split("/pdfs/")[1].split("/"); - const coreFilename = pathComponents.pop()!.split(".")[0]; - const params: any = { - coreFilename, - pageNum: this.Document.curPage || 1, - }; - if (pathComponents.length) { - params.subtree = `${pathComponents.join("/")}/`; + if (relative.includes("/pdfs/")) { + const pathComponents = relative.split("/pdfs/")[1].split("/"); + const coreFilename = pathComponents.pop()!.split(".")[0]; + const params: any = { + coreFilename, + pageNum: this.Document.curPage || 1, + }; + if (pathComponents.length) { + params.subtree = `${pathComponents.join("/")}/`; + } + this._coverPath = href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; + } else { + const params: any = { + coreFilename: relative.split("/")[relative.split("/").length - 1], + pageNum: this.Document.curPage || 1, + }; + this._coverPath = "http://cs.brown.edu/~bcz/face.gif";//href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; } - this._coverPath = href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; runInAction(() => this._showWaiting = this._showCover = true); this.props.startupLive && this.setupPdfJsViewer(); this._mainCont.current!.scrollTop = this.layoutDoc._scrollTop || 0; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index ffef9a384..70aa2d60b 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -100,7 +100,7 @@ export const UpdatingFromServer = Symbol("UpdatingFromServer"); const CachedUpdates = Symbol("Cached updates"); -function fetchProto(doc: Doc) { +export function fetchProto(doc: Doc) { if (doc.author !== Doc.CurrentUserEmail) { const acl = Doc.Get(doc, "ACL", true); switch (acl) { @@ -116,21 +116,9 @@ function fetchProto(doc: Doc) { } } - const proto = doc.proto; - if (proto instanceof Promise) { - proto.then(proto => { - if (proto.author !== Doc.CurrentUserEmail) { - if (proto.ACL === "ownerOnly") { - proto[AclSym] = doc[AclSym] = AclPrivate; - return undefined; - } else if (proto.ACL === "readOnly") { - proto[AclSym] = doc[AclSym] = AclReadonly; - } else if (proto.ACL === "addOnly") { - proto[AclSym] = doc[AclSym] = AclAddonly; - } - } - }); - return proto; + if (doc.proto instanceof Promise) { + doc.proto.then(fetchProto); + return doc.proto; } } @@ -442,7 +430,8 @@ export namespace Doc { if (allowDuplicates !== true) { const pind = list.reduce((l, d, i) => d instanceof Doc && d[Id] === doc[Id] ? i : l, -1); if (pind !== -1) { - list.splice(pind, 1); + return true; + //list.splice(pind, 1); // bcz: this causes schemaView docs in the Catalog to move to the bottom of the schema view when they are dragged even though they haven't left the collection } } if (first) { diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index f81ec8c6d..66959882d 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -392,7 +392,7 @@ export namespace RichTextUtils { const { attrs } = mark; switch (converted) { case "link": - let url = attrs.href; + let url = attrs.allHrefs.length ? attrs.allHrefs[0].href : ""; const delimiter = "/doc/"; const alreadyShared = "?sharing=true"; if (new RegExp(window.location.origin + delimiter).test(url) && !url.endsWith(alreadyShared)) { diff --git a/src/mobile/MobileInkOverlay.tsx b/src/mobile/MobileInkOverlay.tsx index 973931615..59c73ed27 100644 --- a/src/mobile/MobileInkOverlay.tsx +++ b/src/mobile/MobileInkOverlay.tsx @@ -114,7 +114,8 @@ export default class MobileInkOverlay extends React.Component { altKey: false, metaKey: false, ctrlKey: false, - shiftKey: false + shiftKey: false, + embedKey: false } } ) |