diff options
author | bobzel <zzzman@gmail.com> | 2020-06-16 17:44:28 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-16 17:44:28 -0400 |
commit | 4a0332994b38c8910077e6cc711e9e78c2964b67 (patch) | |
tree | 8709d8277610316109c13f6e914adf7d0ecc9eb4 | |
parent | 157e80d5ee76f1c2386e2cde69fcc13188d27d8b (diff) | |
parent | 1cc009bde8ca5b44383740286689f81c02729158 (diff) |
Merge pull request #363 from browngraphicslab/RichTextEdition
Rich text edition
-rw-r--r-- | deploy/assets/cheat-sheet.pdf | bin | 0 -> 167300 bytes | |||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 153 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/DashDocCommentView.tsx | 141 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/DashFieldView.tsx | 7 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FootnoteView.tsx | 118 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 27 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx | 7 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/ImageResizeView.tsx | 138 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/OrderedListView.tsx | 8 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts | 172 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/RichTextRules.ts | 315 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/RichTextSchema.tsx | 397 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/SummaryView.tsx | 116 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 26 |
14 files changed, 611 insertions, 1014 deletions
diff --git a/deploy/assets/cheat-sheet.pdf b/deploy/assets/cheat-sheet.pdf Binary files differnew file mode 100644 index 000000000..fcc5484ea --- /dev/null +++ b/deploy/assets/cheat-sheet.pdf diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index d1877c3a0..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 @@ -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)) { @@ -739,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); @@ -761,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)')) }); @@ -772,20 +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: "Test Private", event: () => this.testAcl("ownerOnly"), icon: "concierge-bell" }); - moreItems.push({ description: "Test Readonly", event: () => this.testAcl("readOnly"), 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" }); @@ -804,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/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.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 60a9c2a27..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; 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.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 600671179..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"; @@ -904,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); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 5ac173602..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; @@ -235,9 +237,6 @@ 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))); } }); } 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 77b93b9d2..114288a9d 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -10,6 +10,7 @@ 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; @@ -29,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 } = {}; @@ -41,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; @@ -148,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; @@ -161,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; @@ -182,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()); @@ -204,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/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/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; |