diff options
Diffstat (limited to 'src')
77 files changed, 858 insertions, 616 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index e527634fd..dba802f98 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -395,6 +395,8 @@ export function returnZero() { return 0; } export function returnEmptyString() { return ""; } +export function returnEmptyFilter() { return [] as string[]; } + export let emptyPath = []; export function emptyFunction() { } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 56ed6febb..dac3a1aaa 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -161,6 +161,7 @@ export interface DocumentOptions { targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script searchFileTypes?: List<string>; // file types allowed in a search query strokeWidth?: number; + stayInCollection?: boolean;// whether the document should remain in its collection when someone tries to drag and drop it elsewhere treeViewPreventOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expand/collapse state to be independent of other views of the same document in the tree view treeViewHideTitle?: boolean; // whether to hide the title of a tree view treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items. @@ -489,7 +490,7 @@ export namespace Docs { Scripting.addGlobal(Buxton); - const delegateKeys = ["x", "y", "layoutKey", "dropAction", "childDropAction", "isLinkButton", "isBackground", "removeDropProperties", "treeViewOpen"]; + const delegateKeys = ["x", "y", "layoutKey", "dropAction", "lockedPosiiton", "childDropAction", "isLinkButton", "isBackground", "removeDropProperties", "treeViewOpen"]; /** * This function receives the relevant document prototype and uses @@ -658,6 +659,7 @@ export namespace Docs { I._backgroundColor = "transparent"; I._width = options._width; I._height = options._height; + I.author = Doc.CurrentUserEmail; I.data = new InkField(points); return I; // return I; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 5edb2225f..d892a9290 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -21,6 +21,7 @@ import { MainView } from "../views/MainView"; import { DocumentType } from "../documents/DocumentTypes"; import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView"; +import { LabelBox } from "../views/nodes/LabelBox"; export class CurrentUserUtils { private static curr_id: string; @@ -323,9 +324,17 @@ export class CurrentUserUtils { iconView.isTemplateDoc = makeTemplate(iconView); doc["template-icon-view"] = new PrefetchProxy(iconView); } + if (doc["template-icon-view-pdf"] === undefined) { + const iconPdfView = Docs.Create.LabelDocument({ + title: "icon_" + DocumentType.PDF, textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("title"), _backgroundColor: "dimGray", + _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") + }); + iconPdfView.isTemplateDoc = makeTemplate(iconPdfView, true, "icon_" + DocumentType.PDF); + doc["template-icon-view-pdf"] = new PrefetchProxy(iconPdfView); + } if (doc["template-icon-view-rtf"] === undefined) { const iconRtfView = Docs.Create.LabelDocument({ - title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset", + title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("text"), _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") }); iconRtfView.isTemplateDoc = makeTemplate(iconRtfView, true, "icon_" + DocumentType.RTF); @@ -345,11 +354,11 @@ export class CurrentUserUtils { } if (doc["template-icons"] === undefined) { doc["template-icons"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, - doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc], { title: "icon templates", _height: 75 })); + doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc, doc["template-icon-view-pdf"] as Doc], { title: "icon templates", _height: 75 })); } else { const templateIconsDoc = Cast(doc["template-icons"], Doc, null); const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, - doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc]; + doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc, doc["template-icon-view-pdf"] as Doc]; DocListCastAsync(templateIconsDoc.data).then(async curIcons => { await Promise.all(curIcons!); requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype)); @@ -583,12 +592,17 @@ export class CurrentUserUtils { } if (doc["tabs-button-tools"] === undefined) { + const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], { + _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack", forceActive: true + })) as any as Doc; doc["tabs-button-tools"] = new PrefetchProxy(Docs.Create.ButtonDocument({ _width: 35, _height: 25, title: "Tools", _fontSize: 10, letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", - sourcePanel: new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], { - _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack", forceActive: true - })) as any as Doc, + sourcePanel: toolsStack, + onDragStart: ScriptField.MakeFunction('getAlias(this.dragFactory, true)'), + dragFactory: toolsStack, + removeDropProperties: new List<string>(["lockedPosition"]), + stayInCollection: true, targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"), })); @@ -613,8 +627,8 @@ export class CurrentUserUtils { static setupCatalog(doc: Doc) { if (doc.myCatalog === undefined) { doc.myCatalog = new PrefetchProxy(Docs.Create.SchemaDocument([], [], { - title: "CATALOG", _height: 1000, _fitWidth: true, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, lockedPosition: true, - childDropAction: "alias", targetDropAction: "same", treeViewExpandedView: "layout" + title: "CATALOG", _height: 1000, _fitWidth: true, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, + childDropAction: "alias", targetDropAction: "same", stayInCollection: true, })); } return doc.myCatalog as Doc; @@ -623,7 +637,7 @@ export class CurrentUserUtils { // setup Recently Closed library item if (doc.myRecentlyClosed === undefined) { doc.myRecentlyClosed = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, + title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, stayInCollection: true, })); } // this is equivalent to using PrefetchProxies to make sure the recentlyClosed doc is ready @@ -641,13 +655,18 @@ export class CurrentUserUtils { const recentlyClosed = CurrentUserUtils.setupRecentlyClosed(doc); if (doc["tabs-button-library"] === undefined) { + const libraryStack = new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], { + title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", + lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" + })) as any as Doc; doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({ _width: 50, _height: 25, title: "Library", _fontSize: 10, targetDropAction: "same", letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", - sourcePanel: new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], { - title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", - lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" - })) as any as Doc, + sourcePanel: libraryStack, + onDragStart: ScriptField.MakeFunction('getAlias(this.dragFactory, true)'), + dragFactory: libraryStack, + removeDropProperties: new List<string>(["lockedPosition"]), + stayInCollection: true, targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;") })); @@ -791,6 +810,7 @@ export class CurrentUserUtils { } static async updateUserDocument(doc: Doc) { + doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode; doc.title = Doc.CurrentUserEmail; doc.activeInkPen = doc; doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)"); diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index e46225b4a..28b1ca6cf 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -144,7 +144,7 @@ export namespace DictationManager { recognizer.start(); return new Promise<string>((resolve, reject) => { - recognizer.onerror = (e: SpeechRecognitionError) => { + recognizer.onerror = (e: any) => { // e is SpeechRecognitionError but where is that defined? if (!(indefinite && e.error === "no-speech")) { recognizer.stop(); reject(e); @@ -335,7 +335,7 @@ export namespace DictationManager { const prompt = "Press alt + r to start dictating here..."; const head = 3; const anchor = head + prompt.length; - const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`; + const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`; proto.data = new RichTextField(proseMirrorState); proto.backgroundColor = "#eeffff"; target.props.addDocTab(newBox, "onRight"); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 21564c92e..26e7250f4 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -328,7 +328,7 @@ export namespace DragManager { dragLabel.style.zIndex = "100001"; dragLabel.style.fontSize = "10"; dragLabel.style.position = "absolute"; - dragLabel.innerText = "press 'a' to embed on drop"; + // dragLabel.innerText = "press 'a' to embed on drop"; // bcz: need to move this to a status bar dragDiv.appendChild(dragLabel); DragManager.Root().appendChild(dragDiv); } diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index ea1769d85..f9837298d 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -54,10 +54,12 @@ export function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string return any; } export function convertDropDataToButtons(data: DragManager.DocumentDragData) { - data && data.draggedDocuments.map((doc, i) => { + data?.draggedDocuments.map((doc, i) => { let dbox = doc; // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant - if (!doc.onDragStart && !doc.isButtonBar) { + if (doc.type === DocumentType.FONTICON) { + dbox = Doc.MakeAlias(doc); + } else if (!doc.onDragStart && !doc.isButtonBar) { const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; if (layoutDoc.type !== DocumentType.FONTICON) { !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc); diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 3b42fcaee..7bdf631fc 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -100,9 +100,9 @@ export namespace InteractionUtils { const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]); newPoints.pop(); const bezierCurves = fitCurve(newPoints, parseInt(bezier)); - for (var i = 0; i < bezierCurves.length; i++) { + for (const curve of bezierCurves) { for (var t = 0; t < 1; t += 0.01) { - const point = beziercurve(t, bezierCurves[i]); + const point = beziercurve(t, curve); pts.push({ X: point[0], Y: point[1] }); } } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 05515e502..eb905d237 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -10,13 +10,17 @@ export namespace SelectionManager { @observable IsDragging: boolean = false; SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap(); - + clearSelection() { + if (window.getSelection) { window.getSelection()?.removeAllRanges(); } + else if (document.getSelection()) { document.getSelection()?.empty(); } + } @action SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it if (!manager.SelectedDocuments.get(docView)) { if (!ctrlPressed) { this.DeselectAll(); + this.clearSelection(); } manager.SelectedDocuments.set(docView, true); diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 314b52bf3..c7b7bb215 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -78,10 +78,12 @@ export namespace UndoManager { let currentBatch: UndoBatch | undefined; let batchCounter = 0; let undoing = false; + let tempEvents: UndoEvent[] | undefined = undefined; export function AddEvent(event: UndoEvent): void { if (currentBatch && batchCounter && !undoing) { currentBatch.push(event); + tempEvents?.push(event); } } @@ -135,7 +137,7 @@ export namespace UndoManager { const EndBatch = action((cancel: boolean = false) => { batchCounter--; - if (batchCounter === 0 && currentBatch && currentBatch.length) { + if (batchCounter === 0 && currentBatch?.length) { if (!cancel) { undoStack.push(currentBatch); } @@ -144,6 +146,13 @@ export namespace UndoManager { } }); + export function ClearTempBatch() { + tempEvents = undefined; + } + export function RunInTempBatch<T>(fn: () => T) { + tempEvents = []; + return runInAction(fn); + } //TODO Make this return the return value export function RunInBatch<T>(fn: () => T, batchName: string) { const batch = StartBatch(batchName); @@ -153,7 +162,16 @@ export namespace UndoManager { batch.end(); } } - + export const UndoTempBatch = action(() => { + if (tempEvents) { + undoing = true; + for (let i = tempEvents.length - 1; i >= 0; i--) { + tempEvents[i].undo(); + } + undoing = false; + } + tempEvents = undefined; + }); export const Undo = action(() => { if (undoStack.length === 0) { return; diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index 9a6121d20..2d26afcf6 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -131,15 +131,15 @@ export default abstract class AntimodeMenu extends React.Component { } protected getDragger = () => { - return <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />; + return <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} />; } protected getElement(buttons: JSX.Element[]) { return ( <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} style={{ left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay }}> + <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} /> {buttons} - <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> </div> ); } @@ -148,8 +148,8 @@ export default abstract class AntimodeMenu extends React.Component { return ( <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} style={{ left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, height: "auto" }}> + {hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} /> : (null)} {rows} - {hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> : <></>} </div> ); } diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 5b66b63ed..941d7b44a 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -236,7 +236,7 @@ export class ContextMenu extends React.Component { <span className="icon-background"> <FontAwesomeIcon icon="search" size="lg" /> </span> - <input className="contextMenu-item contextMenu-description search" type="text" placeholder="Search . . ." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> + <input className="contextMenu-item contextMenu-description search" type="text" placeholder="Search Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> </span> {this.menuItems} </> diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 0a96b058b..11aaaaf8c 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -172,6 +172,20 @@ $linkGap : 3px; } +.documentDecorations-iconifyButton { + opacity: 1; + grid-column-start: 4; + grid-column-end: 6; + pointer-events: all; + text-align: center; + left: -20px; + top: -2px; + cursor: pointer; + position: absolute; + background: transparent; + width: 20px; +} + .documentDecorations-closeButton { opacity: 1; grid-column-start: 4; @@ -222,13 +236,15 @@ $linkGap : 3px; } .link-button-container { - margin-top: $linkGap; + padding: $linkGap; + border-radius: 10px; width: max-content; height: auto; display: flex; flex-direction: row; z-index: 998; position: absolute; + background: $alt-accent; } .linkButtonWrapper { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 6ca7331d6..beb6155ca 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -70,6 +70,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> get Bounds(): { x: number, y: number, b: number, r: number } { return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { if (documentView.props.renderDepth === 0 || + documentView.props.treeViewId || Doc.AreProtosEqual(documentView.props.Document, Doc.UserDoc())) { return bounds; } @@ -174,7 +175,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } @undoBatch @action - onCloseClick = async (e: PointerEvent | undefined) => { + onCloseClick = async (e: React.MouseEvent | undefined) => { if (!e?.button) { const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; const selected = SelectionManager.SelectedDocuments().slice(); @@ -459,7 +460,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}> <FontAwesomeIcon size="lg" icon="cog" /> </div>) : ( - <div className="documentDecorations-minimizeButton" title="Iconify" onPointerDown={this.onIconifyDown}> + <div className="documentDecorations-minimizeButton" title="Iconify" onClick={this.onCloseClick}> {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> </div>); @@ -468,16 +469,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <> <input ref={this._keyinput} className="documentDecorations-title" type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle} onBlur={e => this.titleBlur(true)} onChange={action(e => this._accumulatedTitle = e.target.value)} onKeyPress={this.titleEntered} /> - {minimal ? (null) : <div className="publishBox" title="make document referenceable by its title" - onPointerDown={action(e => { - if (!seldoc.props.Document.customTitle) { - seldoc.props.Document.customTitle = true; - StrCast(Doc.GetProto(seldoc.props.Document).title).startsWith("-") && (Doc.GetProto(seldoc.props.Document).title = StrCast(seldoc.props.Document.title).substring(1)); - this._accumulatedTitle = StrCast(seldoc.props.Document.title); - } - DocUtils.Publish(seldoc.props.Document, this._accumulatedTitle, seldoc.props.addDocument, seldoc.props.removeDocument); - })}> - <FontAwesomeIcon size="lg" color={SelectionManager.SelectedDocuments()[0].props.Document.title === SelectionManager.SelectedDocuments()[0].props.Document[Id] ? "green" : undefined} icon="sticky-note"></FontAwesomeIcon> + {minimal ? (null) : <div className="publishBox" // title="make document referenceable by its title" + // onPointerDown={action(e => { + // if (!seldoc.props.Document.customTitle) { + // seldoc.props.Document.customTitle = true; + // StrCast(Doc.GetProto(seldoc.props.Document).title).startsWith("-") && (Doc.GetProto(seldoc.props.Document).title = StrCast(seldoc.props.Document.title).substring(1)); + // this._accumulatedTitle = StrCast(seldoc.props.Document.title); + // } + // DocUtils.Publish(seldoc.props.Document, this._accumulatedTitle, seldoc.props.addDocument, seldoc.props.removeDocument); + // })} + > + {/* <FontAwesomeIcon size="lg" color={SelectionManager.SelectedDocuments()[0].props.Document.title === SelectionManager.SelectedDocuments()[0].props.Document[Id] ? "green" : undefined} icon="sticky-note"></FontAwesomeIcon> */} </div>} </> : <> @@ -518,6 +520,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> }}> {maximizeIcon} {titleArea} + <div className="documentDecorations-iconifyButton" title={`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`} onPointerDown={this.onIconifyDown}> + {"_"} + </div> <div className="documentDecorations-closeButton" title="Open Document in Tab" onPointerDown={this.onMaximizeDown}> {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."} </div> diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 2121b27f0..98777a92c 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -7,7 +7,7 @@ import { Cast, FieldValue, NumCast } from "../../fields/Types"; import MobileInkOverlay from "../../mobile/MobileInkOverlay"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; import { MobileInkOverlayContent } from "../../server/Message"; -import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero } from "../../Utils"; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { DocServer } from "../DocServer"; import { DocUtils } from "../documents/Documents"; @@ -837,6 +837,7 @@ export default class GestureOverlay extends Touchable { parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} + docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} />; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index a3b144055..27755737e 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -17,7 +17,6 @@ import { undoBatch, UndoManager } from "../util/UndoManager"; import { CollectionDockingView } from "./collections/CollectionDockingView"; import { MarqueeView } from "./collections/collectionFreeForm/MarqueeView"; import { DocumentDecorations } from "./DocumentDecorations"; -import { InkingStroke } from "./InkingStroke"; import { MainView } from "./MainView"; import { DocumentView } from "./nodes/DocumentView"; @@ -75,7 +74,7 @@ export default class KeyManager { case "a": DragManager.CanEmbed = true; break; case " ": - MarqueeView.DragMarquee = !MarqueeView.DragMarquee; + // MarqueeView.DragMarquee = !MarqueeView.DragMarquee; // bcz: this needs a better disclosure UI break; case "escape": const main = MainView.Instance; @@ -104,6 +103,7 @@ export default class KeyManager { } UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.removeDocument?.(dv.props.Document)), "delete"); + SelectionManager.DeselectAll(); break; case "arrowleft": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(-1, 0)), "nudge left"); @@ -253,8 +253,8 @@ export default class KeyManager { case "x": if (SelectionManager.SelectedDocuments().length) { const bds = DocumentDecorations.Instance.Bounds; - const pt = [bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2]; - const text = `__DashDocId(${pt[0]},${pt[1]}):` + SelectionManager.SelectedDocuments().map(dv => dv.Document[Id]).join(":"); + const pt = SelectionManager.SelectedDocuments()[0].props.ScreenToLocalTransform().transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2); + const text = `__DashDocId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.SelectedDocuments().map(dv => dv.Document[Id]).join(":"); SelectionManager.SelectedDocuments().length && navigator.clipboard.writeText(text); DocumentDecorations.Instance.onCloseClick(undefined); stopPropagation = false; @@ -262,14 +262,14 @@ export default class KeyManager { } break; case "c": - if (SelectionManager.SelectedDocuments().length) { + if (DocumentDecorations.Instance.Bounds.r - DocumentDecorations.Instance.Bounds.x > 2) { const bds = DocumentDecorations.Instance.Bounds; const pt = SelectionManager.SelectedDocuments()[0].props.ScreenToLocalTransform().transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2); - const text = `__DashDocId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.SelectedDocuments().map(dv => dv.Document[Id]).join(":"); + const text = `__DashCloneId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.SelectedDocuments().map(dv => dv.Document[Id]).join(":"); SelectionManager.SelectedDocuments().length && navigator.clipboard.writeText(text); stopPropagation = false; - preventDefault = false; } + preventDefault = false; break; } @@ -280,10 +280,12 @@ export default class KeyManager { }); public paste(e: ClipboardEvent) { - if (e.clipboardData?.getData("text/plain") !== "" && e.clipboardData?.getData("text/plain").startsWith("__DashDocId(")) { + const plain = e.clipboardData?.getData("text/plain"); + const clone = plain?.startsWith("__DashCloneId("); + if (plain && (plain.startsWith("__DashDocId(") || clone)) { const first = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; if (first?.props.Document.type === DocumentType.COL) { - const docids = e.clipboardData.getData("text/plain").split(":"); + const docids = plain.split(":"); let count = 1; const list: Doc[] = []; const targetDataDoc = Doc.GetProto(first.props.Document); @@ -295,7 +297,7 @@ export default class KeyManager { list.push(doc); } if (count === docids.length) { - const added = list.filter(d => !docList.includes(d)); + const added = list.filter(d => !docList.includes(d)).map(d => clone ? Doc.MakeClone(d) : d); if (added.length) { added.map(doc => doc.context = targetDataDoc); undoBatch(() => { diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 5129ef65d..34ec0de9c 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; import { documentSchema } from "../../fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; -import { Cast, StrCast } from "../../fields/Types"; +import { Cast, StrCast, BoolCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { InteractionUtils } from "../util/InteractionUtils"; @@ -54,7 +54,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const scaleY = (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); const strokeColor = StrCast(this.layoutDoc.color, ActiveInkColor()); const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, - StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5); + StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", "none", (this.props.isSelected() || BoolCast(this.props.backgroundHalo?.())) && strokeWidth <= 5); const hpoints = InteractionUtils.CreatePolyline(data, left, top, this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, (strokeWidth + 15), StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", this.props.active() ? "visiblestroke" : "none", false); diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index a7048eb23..e84969565 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -84,6 +84,7 @@ height: 100%; position: absolute; display: flex; + user-select: none; } .mainView-flyoutContainer { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 3677746cd..d6c46e3b0 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -20,7 +20,7 @@ import { listSpec } from '../../fields/Schema'; import { BoolCast, Cast, FieldValue, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; -import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, returnTrue, Utils } from '../../Utils'; +import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, returnTrue, Utils, returnEmptyFilter } from '../../Utils'; import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { Docs, DocumentOptions } from '../documents/Documents'; @@ -58,7 +58,7 @@ import { DocumentManager } from '../util/DocumentManager'; @observer export class MainView extends React.Component { public static Instance: MainView; - private _buttonBarHeight = 26; + private _buttonBarHeight = 36; private _flyoutSizeOnDown = 0; private _urlState: HistoryUtil.DocUrl; private _docBtnRef = React.createRef<HTMLDivElement>(); @@ -80,6 +80,11 @@ export class MainView extends React.Component { componentDidMount() { const tag = document.createElement('script'); + const proto = DocServer.GetRefField("rtfProto").then(proto => { + (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), + msg => msg && alert(msg)); + }); + tag.src = "https://www.youtube.com/iframe_api"; const firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag); @@ -201,9 +206,9 @@ export class MainView extends React.Component { const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); - const cloneWorkspace = ScriptField.MakeScript(`cloneWorkspace()`); - workspaceDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, cloneWorkspace!]); - workspaceDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "New Workspace Layout"]); + const copyWorkspace = ScriptField.MakeScript(`copyWorkspace()`); + workspaceDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, copyWorkspace!]); + workspaceDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Workspace"]); Doc.AddDocToList(workspaces, "data", workspaceDoc); // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) @@ -310,6 +315,7 @@ export class MainView extends React.Component { parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} + docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} />; @@ -389,7 +395,7 @@ export class MainView extends React.Component { return (null); } return <div className="mainView-flyoutContainer" > - <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px`, backgroundColor: StrCast(this.sidebarButtonsDoc.backgroundColor) }}> + <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight - 10/*margin-top*/}px`, backgroundColor: StrCast(this.sidebarButtonsDoc.backgroundColor) }}> <DocumentView Document={this.sidebarButtonsDoc} DataDoc={undefined} @@ -412,6 +418,7 @@ export class MainView extends React.Component { parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} + docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div> @@ -438,6 +445,7 @@ export class MainView extends React.Component { parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} + docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> <button className="mainView-settings" key="settings" onClick={() => SettingsManager.Instance.open()}> @@ -529,6 +537,7 @@ export class MainView extends React.Component { renderDepth={0} focus={emptyFunction} whenActiveChanged={emptyFunction} + docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div>; @@ -578,7 +587,7 @@ export class MainView extends React.Component { } Scripting.addGlobal(function freezeSidebar() { MainView.expandFlyout(); }); Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().fontFamily = "Comic Sans MS"; Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }); -Scripting.addGlobal(function cloneWorkspace() { +Scripting.addGlobal(function copyWorkspace() { const copiedWorkspace = Doc.MakeCopy(Cast(Doc.UserDoc().activeWorkspace, Doc, null), true); const workspaces = Cast(Doc.UserDoc().myWorkspaces, Doc, null); Doc.AddDocToList(workspaces, "data", copiedWorkspace); diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index f6e5e1705..37d8dd23b 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { Doc, DocListCast, Opt } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { NumCast, Cast } from "../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils, setupMoveUpEvents } from "../../Utils"; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils, setupMoveUpEvents, returnEmptyFilter } from "../../Utils"; import { Transform } from "../util/Transform"; import { CollectionFreeFormLinksView } from "./collections/collectionFreeForm/CollectionFreeFormLinksView"; import { DocumentView } from "./nodes/DocumentView"; @@ -202,6 +202,7 @@ export class OverlayView extends React.Component { backgroundColor={returnEmptyString} addDocTab={returnFalse} pinToPres={emptyFunction} + docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div>; diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx index 108eb83d6..0a4334302 100644 --- a/src/client/views/Palette.tsx +++ b/src/client/views/Palette.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { Doc } from "../../fields/Doc"; import { NumCast } from "../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyString, returnZero, returnFalse, returnOne, returnTrue } from "../../Utils"; +import { emptyFunction, emptyPath, returnEmptyString, returnZero, returnFalse, returnOne, returnTrue, returnEmptyFilter } from "../../Utils"; import { Transform } from "../util/Transform"; import { DocumentView } from "./nodes/DocumentView"; import "./Palette.scss"; @@ -59,6 +59,7 @@ export default class Palette extends React.Component<PaletteProps> { parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} + docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div> diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index dd65681d4..e27f6b95a 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -32,10 +32,11 @@ export class PreviewCursor extends React.Component<{}> { // tests for URL and makes web document const re: any = /^https?:\/\//g; - if (e.clipboardData.getData("text/plain") !== "") { + const plain = e.clipboardData.getData("text/plain"); + if (plain) { // tests for youtube and makes video document - if (e.clipboardData.getData("text/plain").indexOf("www.youtube.com/watch") !== -1) { - const url = e.clipboardData.getData("text/plain").replace("youtube.com/watch?v=", "youtube.com/embed/"); + if (plain.indexOf("www.youtube.com/watch") !== -1) { + const url = plain.replace("youtube.com/watch?v=", "youtube.com/embed/"); undoBatch(() => PreviewCursor._addDocument(Docs.Create.VideoDocument(url, { title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5, @@ -43,8 +44,8 @@ export class PreviewCursor extends React.Component<{}> { })))(); } - else if (re.test(e.clipboardData.getData("text/plain"))) { - const url = e.clipboardData.getData("text/plain"); + else if (re.test(plain)) { + const url = plain; undoBatch(() => PreviewCursor._addDocument(Docs.Create.WebDocument(url, { title: url, _width: 500, _height: 300, UseCors: true, // nativeWidth: 300, nativeHeight: 472.5, @@ -52,10 +53,11 @@ export class PreviewCursor extends React.Component<{}> { })))(); } - else if (e.clipboardData.getData("text/plain").startsWith("__DashDocId(")) { - const docids = e.clipboardData.getData("text/plain").split(":"); + else if (plain.startsWith("__DashDocId(") || plain.startsWith("__DashCloneId(")) { + const clone = plain.startsWith("__DashCloneId("); + const docids = plain.split(":"); const strs = docids[0].split(","); - const ptx = Number(strs[0].substring("__DashDocId(".length)); + const ptx = Number(strs[0].substring((clone ? "__DashCloneId(" : "__DashDocId(").length)); const pty = Number(strs[1].substring(0, strs[1].length - 1)); let count = 1; const list: Doc[] = []; @@ -65,7 +67,7 @@ export class PreviewCursor extends React.Component<{}> { count++; if (doc instanceof Doc) { i === 1 && (first = doc); - const alias = Doc.MakeClone(doc); + const alias = clone ? Doc.MakeClone(doc) : doc; const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx; const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty; alias.x = newPoint[0] + deltaX; @@ -115,9 +117,9 @@ export class PreviewCursor extends React.Component<{}> { (e.keyCode < 112 || e.keyCode > 123) && // F1 thru F12 keys !e.key.startsWith("Arrow") && !e.defaultPrevented) { - if ((!e.ctrlKey || (e.keyCode >= 48 && e.keyCode <= 57)) && !e.metaKey) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) { + if ((!e.metaKey && !e.ctrlKey) || (e.keyCode >= 48 && e.keyCode <= 57) || (e.keyCode >= 65 && e.keyCode <= 90)) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) { PreviewCursor.Visible && PreviewCursor._onKeyPress?.(e); - PreviewCursor.Visible = false; + ((!e.ctrlKey && !e.metaKey) || e.key !== "v") && (PreviewCursor.Visible = false); } } else if (PreviewCursor.Visible) { if (e.key === "ArrowRight") { diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx index 8ca81c070..cdde32c21 100644 --- a/src/client/views/RecommendationsBox.tsx +++ b/src/client/views/RecommendationsBox.tsx @@ -6,7 +6,7 @@ import "./RecommendationsBox.scss"; import { Doc, DocListCast, WidthSym, HeightSym } from "../../fields/Doc"; import { DocumentIcon } from "./nodes/DocumentIcon"; import { StrCast, NumCast } from "../../fields/Types"; -import { returnFalse, emptyFunction, returnEmptyString, returnOne, emptyPath, returnZero } from "../../Utils"; +import { returnFalse, emptyFunction, returnEmptyString, returnOne, emptyPath, returnZero, returnEmptyFilter } from "../../Utils"; import { Transform } from "../util/Transform"; import { ObjectField } from "../../fields/ObjectField"; import { DocumentView } from "./nodes/DocumentView"; @@ -56,7 +56,7 @@ export class RecommendationsBox extends React.Component<FieldViewProps> { } const returnXDimension = () => 150; const returnYDimension = () => 150; - const scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension()); + const scale = () => returnXDimension() / NumCast(renderDoc._nativeWidth, returnXDimension()); //let scale = () => 1; const newRenderDoc = Doc.MakeAlias(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt newRenderDoc.height = NumCast(this.props.Document.documentIconHeight); @@ -82,6 +82,7 @@ export class RecommendationsBox extends React.Component<FieldViewProps> { parentActive={returnFalse} whenActiveChanged={returnFalse} bringToFront={emptyFunction} + docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} ContentScaling={scale} diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index f135823a8..916e631d0 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -10,7 +10,7 @@ import { Doc, DocListCast } from "../../fields/Doc"; import { Docs, DocUtils, } from "../documents/Documents"; import { StrCast, Cast } from "../../fields/Types"; import { CollectionTreeView } from "./collections/CollectionTreeView"; -import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZero } from "../../Utils"; +import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZero, returnEmptyFilter } from "../../Utils"; import { Transform } from "../util/Transform"; import { ScriptField, ComputedField } from "../../fields/ScriptField"; import { Scripting } from "../util/Scripting"; @@ -140,6 +140,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { CollectionView={undefined} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} + docFilters={returnEmptyFilter} rootSelected={returnFalse} onCheckedClick={this.scriptField!} onChildClick={this.scriptField!} diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 657e2a08b..4e5491df5 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -8,11 +8,10 @@ import * as GoldenLayout from "../../../client/goldenLayout"; import { DateField } from '../../../fields/DateField'; import { Doc, DocListCast, Field, Opt, DataSym } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; -import { List } from '../../../fields/List'; import { FieldId } from "../../../fields/RefField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, returnTrue, Utils, returnZero } from "../../../Utils"; +import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; @@ -843,6 +842,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { backgroundColor={CollectionDockingView.Instance?.props.backgroundColor} addDocTab={this.addDocTab} pinToPres={DockedFrameRenderer.PinDoc} + docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} />; } diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index f1002044a..f38eeaf41 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -124,6 +124,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} + docFilters={this.props.docFilters} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div>; diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index cc7a9f5ac..e0b53e762 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -43,6 +43,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @observable private heading: string = ""; @observable private color: string = "#f1efeb"; @observable private collapsed: boolean = false; + @observable private _paletteOn = false; private set _heading(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.heading = this.heading = value)); } private set _color(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.color = this.color = value)); } private set _collapsed(value: boolean) { runInAction(() => this.props.headingObject && (this.props.headingObject.collapsed = this.collapsed = value)); } @@ -293,11 +294,10 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr {noChrome ? evContents : <EditableView {...headerEditableViewProps} />} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionColor"> - <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}> - <button className="collectionStackingView-sectionColorButton"> - <FontAwesomeIcon icon="palette" size="lg" /> - </button> - </ Flyout > + <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}> + <FontAwesomeIcon icon="palette" size="lg" /> + </button> + {this._paletteOn ? this.renderColorPicker() : (null)} </div> } {noChrome ? (null) : <button className="collectionStackingView-sectionDelete" onClick={noChrome ? undefined : this.collapseSection}> @@ -305,7 +305,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr </button>} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionOptions"> - <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> + <Flyout anchorPoint={anchorPoints.TOP_CENTER} content={this.renderMenu()}> <button className="collectionStackingView-sectionOptionButton"> <FontAwesomeIcon icon="ellipsis-v" size="lg" /> </button> diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index baf9d4156..2b8110e27 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -3,7 +3,7 @@ import { action, observable, trace } from "mobx"; import { observer } from "mobx-react"; import { CellInfo } from "react-table"; import "react-table/react-table.css"; -import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; +import { emptyFunction, returnFalse, returnZero, returnOne, returnEmptyFilter } from "../../../Utils"; import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { KeyCodes } from "../../util/KeyCodes"; @@ -159,6 +159,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { bringToFront: emptyFunction, rootSelected: returnFalse, fieldKey: this.props.rowProps.column.id as string, + docFilters: returnEmptyFilter, ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, isSelected: returnFalse, diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 6dbee217a..56a2a517c 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -133,6 +133,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { PanelWidth={this.previewWidth} PanelHeight={this.previewHeight} ScreenToLocalTransform={this.getPreviewTransform} + docFilters={this.docFilters} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} moveDocument={this.props.moveDocument} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 342773db8..8fc74a9c6 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -241,11 +241,15 @@ } .collectionStackingView-sectionColorButton { - height: 35px; + height: 30px; + display: inherit; } .collectionStackingView-colorPicker { width: 78px; + z-index: 10; + position: relative; + background: white; .colorOptions { display: flex; @@ -279,7 +283,7 @@ } .collectionStackingView-sectionOptionButton { - height: 35px; + height: 30px; } .collectionStackingView-optionPicker { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 9f1b5d63c..1c58b4bcc 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -217,6 +217,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) ScreenToLocalTransform={dxf} opacity={opacity} focus={this.focusDocument} + docFilters={this.docFilters} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index bcd55f0fe..b60ed853b 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -50,6 +50,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC private dropDisposer?: DragManager.DragDropDisposer; private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); + @observable _paletteOn = false; @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; _ele: HTMLElement | null = null; @@ -326,11 +327,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC <EditableView {...headerEditableViewProps} /> {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionColor"> - <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}> - <button className="collectionStackingView-sectionColorButton"> - <FontAwesomeIcon icon="palette" size="lg" /> - </button> - </ Flyout > + <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}> + <FontAwesomeIcon icon="palette" size="lg" /> + </button> + {this._paletteOn ? this.renderColorPicker() : (null)} </div> } {evContents === `NO ${key.toUpperCase()} VALUE` ? diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 93d20c475..00d6d59c8 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -10,7 +10,7 @@ import { WebField } from "../../../fields/URLField"; import { Cast, ScriptCast, NumCast } from "../../../fields/Types"; import { GestureUtils } from "../../../pen-gestures/GestureUtils"; import { Upload } from "../../../server/SharedMediaTypes"; -import { Utils, returnFalse } from "../../../Utils"; +import { Utils, returnFalse, returnEmptyFilter } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Networking } from "../../Network"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; @@ -101,8 +101,13 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: get childDocList() { return Cast(this.dataField, listSpec(Doc)); } + docFilters = () => { + return this.props.ignoreFields?.includes("_docFilters") ? [] : + this.props.docFilters !== returnEmptyFilter ? this.props.docFilters() : + Cast(this.props.Document._docFilters, listSpec("string"), []); + } @computed get childDocs() { - const docFilters = this.props.ignoreFields?.includes("_docFilters") ? [] : Cast(this.props.Document._docFilters, listSpec("string"), []); + const docFilters = this.docFilters(); const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields for (let i = 0; i < docFilters.length; i += 3) { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index d631a492e..68dc0ced2 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -8,7 +8,7 @@ import { PrefetchProxy } from '../../../fields/Proxy'; import { Document, listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils'; +import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils, returnEmptyFilter } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from '../../util/DocumentManager'; @@ -250,7 +250,7 @@ class TreeView extends React.Component<TreeViewProps> { const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); if (aspect) return this.docWidth() * aspect; if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return layoutDoc._fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection._height) : + return layoutDoc._fitWidth ? (!this.props.document._nativeHeight ? NumCast(this.props.containingCollection._height) : Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, NumCast(this.props.containingCollection._height)))) : NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; @@ -312,8 +312,8 @@ class TreeView extends React.Component<TreeViewProps> { @computed get renderContent() { TraceMobx(); - const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined; - if (expandKey !== undefined) { + const expandKey = this.treeViewExpandedView; + if (["links", this.fieldKey].includes(expandKey)) { const remDoc = (doc: Doc | Doc[]) => this.remove(doc, expandKey); const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true), true); @@ -338,13 +338,14 @@ class TreeView extends React.Component<TreeViewProps> { const layoutDoc = Doc.Layout(this.props.document); const panelHeight = layoutDoc.type === DocumentType.RTF ? this.rtfHeight : this.docHeight; const panelWidth = layoutDoc.type === DocumentType.RTF ? this.rtfWidth : this.docWidth; - return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.props.document[Id] + this.props.document.title}> + return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.props.document[Id]}> <ContentFittingDocumentView Document={layoutDoc} DataDoc={this.dataDoc} LibraryPath={emptyPath} renderDepth={this.props.renderDepth + 1} rootSelected={returnTrue} + treeViewId={this.props.treeViewId[Id]} backgroundColor={this.props.backgroundColor} fitToBox={this.boundsOfCollectionDocument !== undefined} FreezeDimensions={true} @@ -354,6 +355,7 @@ class TreeView extends React.Component<TreeViewProps> { PanelHeight={panelHeight} focus={returnFalse} ScreenToLocalTransform={this.docTransform} + docFilters={returnEmptyFilter} ContainingCollectionDoc={this.props.containingCollection} ContainingCollectionView={undefined} addDocument={returnFalse} @@ -471,6 +473,7 @@ class TreeView extends React.Component<TreeViewProps> { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildViews)} + docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={this.props.containingCollection} />} @@ -484,12 +487,13 @@ class TreeView extends React.Component<TreeViewProps> { TraceMobx(); const sorting = this.props.document[`${this.fieldKey}-sortAscending`]; //setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0); - return <div className="treeViewItem-container" ref={this.createTreeDropTarget}> + return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onPointerDown={e => this.props.active() && SelectionManager.DeselectAll()}> <li className="collection-child"> <div className="treeViewItem-header" ref={this._header} onClick={e => { if (this.props.active(true)) { e.stopPropagation(); e.preventDefault(); + SelectionManager.DeselectAll(); } }} onPointerDown={e => { @@ -728,7 +732,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll layoutItems.push({ description: (this.props.Document.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.props.Document.treeViewHideTitle = !this.props.Document.treeViewHideTitle, icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" }); } - ContextMenu.Instance.addItem({ + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: "Buxton Layout", icon: "eye", event: () => { const { ImageDocument, PdfDocument } = Docs.Create; const { Document } = this.props; @@ -790,6 +794,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll console.log(e); } render() { + TraceMobx(); if (!(this.props.Document instanceof Doc)) return (null); const dropAction = StrCast(this.props.Document.childDropAction) as dropActionType; const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 0a84e6f72..29e5c78a9 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -17,7 +17,7 @@ import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; +import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, returnEmptyFilter } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; @@ -45,6 +45,7 @@ import { CollectionTreeView } from "./CollectionTreeView"; import { CollectionGridView } from './collectionGrid/CollectionGridView'; import './CollectionView.scss'; import { CollectionViewBaseChrome } from './CollectionViewChromes'; +import { UndoManager } from '../../util/UndoManager'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -166,7 +167,17 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus return true; } const first = doc instanceof Doc ? doc : doc[0]; - return !first?.stayInCollection && addDocument !== returnFalse && this.removeDocument(doc) ? addDocument(doc) : false; + if (!first?.stayInCollection && addDocument !== returnFalse) { + if (UndoManager.RunInTempBatch(() => this.removeDocument(doc))) { + const added = addDocument(doc); + if (!added) UndoManager.UndoTempBatch(); + else UndoManager.ClearTempBatch(); + + return added; + } + UndoManager.ClearTempBatch(); + } + return false; } showIsTagged = () => { @@ -180,8 +191,9 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus // return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />; } + screenToLocalTransform = () => this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth()); private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { - const props: SubCollectionViewProps = { ...this.props, ...renderProps, CollectionView: this, annotationsKey: "" }; + const props: SubCollectionViewProps = { ...this.props, ...renderProps, ScreenToLocalTransform: this.screenToLocalTransform, CollectionView: this, annotationsKey: "" }; switch (type) { case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />); case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />); @@ -455,6 +467,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus DataDoc={facetCollection} fieldKey={`${this.props.fieldKey}-filter`} CollectionView={this} + docFilters={returnEmptyFilter} ContainingCollectionDoc={this.props.ContainingCollectionDoc} ContainingCollectionView={this.props.ContainingCollectionView} PanelWidth={this.facetWidth} @@ -503,13 +516,10 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus ChildLayoutTemplate: this.childLayoutTemplate, ChildLayoutString: this.childLayoutString, }; - return (<div className={"collectionView"} - style={{ - pointerEvents: this.props.Document.isBackground ? "none" : undefined, - boxShadow: Doc.UserDoc().renderStyle === "comic" || this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : - `${Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "rgb(30, 32, 31)" : "#9c9396"} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` - }} - onContextMenu={this.onContextMenu}> + const boxShadow = Doc.UserDoc().renderStyle === "comic" || this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : + `${Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`; + return (<div className={"collectionView"} onContextMenu={this.onContextMenu} + style={{ pointerEvents: this.props.Document.isBackground ? "none" : undefined, boxShadow }}> {this.showIsTagged()} <div className="collectionView-facetCont" style={{ width: `calc(100% - ${this.facetWidth()}px)` }}> {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index ec2d78735..086a56f9d 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -15,7 +15,7 @@ padding-bottom: 1px; height: 32px; border-bottom: .5px solid rgb(180, 180, 180); - overflow: hidden; + overflow: visible; .collectionViewBaseChrome { display: flex; diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index ec908f034..d293a1fb1 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -40,28 +40,41 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro _templateCommand = { params: ["target", "source"], title: "=> item view", script: "this.target.childLayout = getDocTemplate(this.source?.[0])", - immediate: (source: Doc[]) => this.target.childLayout = Doc.getDocTemplate(source?.[0]), + immediate: undoBatch((source: Doc[]) => source.length && (this.target.childLayout = Doc.getDocTemplate(source?.[0]))), initialize: emptyFunction, }; _narrativeCommand = { params: ["target", "source"], title: "=> child click view", script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])", - immediate: (source: Doc[]) => this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]), + immediate: undoBatch((source: Doc[]) => source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]))), initialize: emptyFunction, }; _contentCommand = { - params: ["target", "source"], title: "=> content", + params: ["target", "source"], title: "=> clear content", script: "getProto(this.target).data = copyField(this.source);", - immediate: (source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source), // Doc.aliasDocs(source), + immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), // Doc.aliasDocs(source), initialize: emptyFunction, }; _viewCommand = { - params: ["target"], title: "=> saved view", + params: ["target"], title: "=> reset view", script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", - immediate: (source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target.scale = 1; }, + immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target.scale = 1; }), initialize: (button: Doc) => { button.restoredPanX = this.target._panX; button.restoredPanY = this.target._panY; button.restoredScale = this.target.scale; }, }; - _freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand]; + _clusterCommand = { + params: ["target"], title: "=> fit content", + script: "this.target._fitToBox = !this.target._fitToBox;", + immediate: undoBatch((source: Doc[]) => this.target._fitToBox = !this.target._fitToBox), + initialize: emptyFunction + }; + _fitContentCommand = { + params: ["target"], title: "=> toggle clusters", + script: "this.target.useClusters = !this.target.useClusters;", + immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters), + initialize: emptyFunction + }; + + _freeform_commands = [this._viewCommand, this._fitContentCommand, this._clusterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; _stacking_commands = [this._contentCommand, this._templateCommand]; _masonry_commands = [this._contentCommand, this._templateCommand]; _schema_commands = [this._templateCommand, this._narrativeCommand]; @@ -84,6 +97,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @observable private _currentKey: string = ""; componentDidMount = action(() => { + this._currentKey = this._currentKey || (this._buttonizableCommands.length ? this._buttonizableCommands[0]?.title : ""); // chrome status is one of disabled, collapsed, or visible. this determines initial state from document switch (this.props.CollectionView.props.Document._chromeStatus) { case "disabled": @@ -248,7 +262,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY)); return true; - }, emptyFunction, emptyFunction); + }, emptyFunction, () => { + this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate([])); + }); } @computed get templateChrome() { @@ -259,10 +275,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro <FontAwesomeIcon icon="bullseye" size="2x" /> </div> <select - className="collectionViewBaseChrome-cmdPicker" - onPointerDown={stopPropagation} - onChange={this.commandChanged} - value={this._currentKey}> + className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey} + style={{ width: this.props.PanelWidth() < 300 ? 15 : undefined }}> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} /> {this._buttonizableCommands.map(cmd => <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> @@ -280,7 +294,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro <FontAwesomeIcon icon="bullseye" size="2x" /> </div> <select - className="collectionViewBaseChrome-viewPicker" + className="collectionViewBaseChrome-viewPicker" style={{ width: this.props.PanelWidth() < 300 ? 15 : undefined }} onPointerDown={stopPropagation} onChange={this.viewChanged} value={StrCast(this.props.CollectionView.props.Document._viewType)}> @@ -713,7 +727,7 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewChro </span> <select className="collectionGridViewChrome-viewPicker" - style={{ marginRight: 5 }} + style={{ marginRight: 5, width: this.props.PanelWidth() < 300 ? 25 : undefined }} onPointerDown={stopPropagation} onChange={this.changeCompactType} value={StrCast(this.props.CollectionView.props.Document.gridStartCompaction, StrCast(this.props.CollectionView.props.Document.gridCompaction))}> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 6cac39f77..a24693c30 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -56,27 +56,27 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const targetAhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(AanchorId)); const targetBhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(BanchorId)); if (!targetBhyperlink) { - this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; - this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; + this.props.A.rootDoc[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; + this.props.A.rootDoc[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; } else { setTimeout(() => { - (this.props.A.props.Document[(this.props.A.props as any).fieldKey] as Doc); + (this.props.A.rootDoc[(this.props.A.props as any).fieldKey] as Doc); const m = targetBhyperlink.getBoundingClientRect(); const mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - this.props.A.props.Document[afield + "_x"] = mp[0] / this.props.A.props.PanelWidth() * 100; - this.props.A.props.Document[afield + "_y"] = mp[1] / this.props.A.props.PanelHeight() * 100; + this.props.A.rootDoc[afield + "_x"] = Math.min(1, mp[0] / this.props.A.props.PanelWidth()) * 100; + this.props.A.rootDoc[afield + "_y"] = Math.min(1, mp[1] / this.props.A.props.PanelHeight()) * 100; }, 0); } if (!targetAhyperlink) { - this.props.A.props.Document[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100; - this.props.A.props.Document[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100; + this.props.A.rootDoc[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100; + this.props.A.rootDoc[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100; } else { setTimeout(() => { - (this.props.B.props.Document[(this.props.B.props as any).fieldKey] as Doc); + (this.props.B.rootDoc[(this.props.B.props as any).fieldKey] as Doc); const m = targetAhyperlink.getBoundingClientRect(); const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - this.props.B.props.Document[bfield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100; - this.props.B.props.Document[bfield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100; + this.props.B.rootDoc[bfield + "_x"] = Math.min(1, mp[0] / this.props.B.props.PanelWidth()) * 100; + this.props.B.rootDoc[bfield + "_y"] = Math.min(1, mp[1] / this.props.B.props.PanelHeight()) * 100; }, 0); } }) diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index bed871f8d..2f7c5ff4b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -45,7 +45,7 @@ import React = require("react"); import { CollectionViewType } from "../CollectionView"; import { Timeline } from "../../animationtimeline/Timeline"; import { SnappingManager } from "../../../util/SnappingManager"; -import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../../InkingStroke"; +import { ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../../InkingStroke"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -158,8 +158,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } if (retVal) { const newBoxes = (newBox instanceof Doc) ? [newBox] : newBox; - for (let i = 0; i < newBoxes.length; i++) { - const newBox = newBoxes[i]; + for (const newBox of newBoxes) { if (newBox.activeFrame !== undefined) { const x = newBox.x; const y = newBox.y; @@ -240,7 +239,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @undoBatch @action internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData, xp: number, yp: number) { - if (linkDragData.linkSourceDocument === this.props.Document) return false; + if (linkDragData.linkSourceDocument === this.props.Document || this.props.Document.annotationOn) return false; const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); this.props.addDocument(source); linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation"); // TODODO this is where in text links get passed @@ -258,8 +257,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp); } else if (de.complete.docDragData?.droppedDocuments.length && this.internalDocDrop(e, de, de.complete.docDragData, xp, yp)) { return true; - } else { - UndoManager.Undo(); } return false; } @@ -865,7 +862,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => { const state = HistoryUtil.getState(); - // TODO This technically isn't correct if type !== "doc", as + // TODO This technically isn't correct if type !== "doc", as // currently nothing is done, but we should probably push a new state if (state.type === "doc" && this.Document._panX !== undefined && this.Document._panY !== undefined) { const init = state.initializers![this.Document[Id]]; @@ -961,6 +958,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.Document, + docFilters: this.docFilters, focus: this.focusDocument, backgroundColor: this.getClusterColor, backgroundHalo: this.backgroundHalo, @@ -1130,6 +1128,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); } + this.Document.useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; } @@ -1199,57 +1198,58 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onContextMenu = (e: React.MouseEvent) => { if (this.props.annotationsKey) return; - ContextMenu.Instance?.addItem({ - description: (this._timelineVisible ? "Close" : "Open") + " Animation Timeline", event: action(() => { - this._timelineVisible = !this._timelineVisible; - }), icon: this._timelineVisible ? faEyeSlash : faEye - }); + const appearance = ContextMenu.Instance?.findByDescription("Appearance..."); + const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; + appearanceItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); + appearanceItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); + appearanceItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); + appearanceItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); + !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); const options = ContextMenu.Instance?.findByDescription("Options..."); - const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - - optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); - optionItems.push({ description: "toggle snap line display", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); - optionItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); - optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); - optionItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); - optionItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); - this.props.ContainingCollectionView && optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" }); + const optionItems = options && "subitems" in options ? options.subitems : []; + !this.props.isAnnotationOverlay && + optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: faEye }); + this.props.ContainingCollectionView && + optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" }); + optionItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " snap lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" }); optionItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); - // layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); - optionItems.push({ - description: "Import document", icon: "upload", event: ({ x, y }) => { - const input = document.createElement("input"); - input.type = "file"; - input.accept = ".zip"; - input.onchange = async _e => { - const upload = Utils.prepend("/uploadDoc"); - const formData = new FormData(); - const file = input.files && input.files[0]; - if (file) { - formData.append('file', file); - formData.append('remap', "true"); - const response = await fetch(upload, { method: "POST", body: formData }); - const json = await response.json(); - if (json !== "error") { - const doc = await DocServer.GetRefField(json); - if (doc instanceof Doc) { - const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); - doc.x = xx, doc.y = yy; - this.props.addDocument?.(doc); + if (!Doc.UserDoc().noviceMode) { + optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); + optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); + optionItems.push({ + description: "Import document", icon: "upload", event: ({ x, y }) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".zip"; + input.onchange = async _e => { + const upload = Utils.prepend("/uploadDoc"); + const formData = new FormData(); + const file = input.files && input.files[0]; + if (file) { + formData.append('file', file); + formData.append('remap', "true"); + const response = await fetch(upload, { method: "POST", body: formData }); + const json = await response.json(); + if (json !== "error") { + const doc = await DocServer.GetRefField(json); + if (doc instanceof Doc) { + const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); + doc.x = xx, doc.y = yy; + this.props.addDocument?.(doc); + } } } - } - }; - input.click(); - } - }); - optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); - ContextMenu.Instance?.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); + }; + input.click(); + } + }); + } + !options && ContextMenu.Instance?.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); } - @observable _timelineVisible = false; + @observable showTimeline = false; intersectRect(r1: { left: number, top: number, width: number, height: number }, r2: { left: number, top: number, width: number, height: number }) { @@ -1350,7 +1350,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> {this.children} </CollectionFreeFormViewPannableContents> - {this._timelineVisible ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} + {this.showTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} </MarqueeView>; } @@ -1449,4 +1449,4 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF {this.props.children()} </div>; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx index 676dc10f4..fdf40dd96 100644 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx @@ -122,7 +122,6 @@ export default class InkOptionsMenu extends AntimodeMenu { render() { const buttons = [ - <button className="antimodeMenu-button" title="Drag" key="drag" onPointerDown={e => this.dragStart(e)}> ✜ </button>, ...this.shapeButtons, this.bezierButton, this.widthPicker, diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 5f09fa0ee..1bc7c6fb5 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,6 +1,6 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt } from "../../../../fields/Doc"; +import { Doc, Opt, DocListCast, DataSym } from "../../../../fields/Doc"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; @@ -20,12 +20,14 @@ import { CollectionView } from "../CollectionView"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); +import { DateField } from "../../../../fields/DateField"; +import { DocServer } from "../../../DocServer"; interface MarqueeViewProps { getContainerTransform: () => Transform; getTransform: () => Transform; activeDocuments: () => Doc[]; - selectDocuments: (docs: Doc[], ink: { Document: Doc, Ink: Map<any, any> }[]) => void; + selectDocuments: (docs: Doc[]) => void; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; nudge: (x: number, y: number) => boolean; @@ -80,11 +82,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }); ContextMenu.Instance.displayMenu(this._downX, this._downY); + e.stopPropagation(); } else if (e.key === ":") { DocUtils.addDocumentCreatorMenuItems(this.props.addLiveTextDocument, this.props.addDocument, x, y); ContextMenu.Instance.displayMenu(this._downX, this._downY); + e.stopPropagation(); + } else if (e.key === "a" && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.props.selectDocuments(this.props.activeDocuments()); + e.stopPropagation(); } else if (e.key === "q" && e.ctrlKey) { e.preventDefault(); (async () => { @@ -108,6 +116,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque y += 40 * this.props.getTransform().Scale; }); })(); + e.stopPropagation(); } else if (e.key === "b" && e.ctrlKey) { e.preventDefault(); navigator.clipboard.readText().then(text => { @@ -118,7 +127,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.pasteTable(ns, x, y); } }); - } else if (!e.ctrlKey) { + e.stopPropagation(); + } else if (!e.ctrlKey && !e.metaKey) { FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : ""; const tbox = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: NumCast(Doc.UserDoc().fontSize), @@ -132,8 +142,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template; } this.props.addLiveTextDocument(tbox); + e.stopPropagation(); } - e.stopPropagation(); } //heuristically converts pasted text into a table. // assumes each entry is separated by a tab @@ -225,7 +235,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // let inkselect = this.ink ? this.marqueeInkSelect(this.ink.inkData) : new Map(); // let inks = inkselect.size ? [{ Document: this.inkDoc, Ink: inkselect }] : []; const docs = mselect.length ? mselect : [this.props.Document]; - this.props.selectDocuments(docs, []); + this.props.selectDocuments(docs); } const hideMarquee = () => { this.hideMarquee(); @@ -354,7 +364,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque selected.forEach(d => this.props.removeDocument(d)); const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2); this.props.addDocument(newCollection!); - this.props.selectDocuments([newCollection!], []); + this.props.selectDocuments([newCollection!]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } @@ -379,7 +389,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t" ? Docs.Create.StackingDocument : undefined); this.props.addDocument(newCollection); - this.props.selectDocuments([newCollection], []); + this.props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } @@ -492,7 +502,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.props.addDocument(newCollection); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); - setTimeout(() => this.props.selectDocuments([newCollection], []), 0); + setTimeout(() => this.props.selectDocuments([newCollection]), 0); } @undoBatch diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index a5d355abc..2015ca930 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -163,7 +163,7 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { const savedLayouts = this.savedLayoutList; this.childLayoutPairs.forEach(({ layout: doc }) => { const gridLayout = savedLayouts.find(gridLayout => gridLayout.i === doc[Id]); - gridLayout && Object.assign(gridLayout, layoutArray.find(layout => layout.i === doc[Id]) || gridLayout); + if (gridLayout) Object.assign(gridLayout, layoutArray.find(layout => layout.i === doc[Id]) || gridLayout); }); if (this.props.Document.gridStartCompaction) { diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index c0e1a0232..776266ce6 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -234,6 +234,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={dxf} focus={this.props.focus} + docFilters={this.docFilters} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 602246d07..1703ff4dc 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -233,6 +233,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={dxf} focus={this.props.focus} + docFilters={this.docFilters} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index f934945a6..404d69730 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -144,7 +144,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF boxShadow: this.Opacity === 0 ? undefined : // if it's not visible, then no shadow this.layoutDoc.z ? `#9c9396 ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow - this.props.backgroundHalo?.() ? (`${this.props.backgroundColor?.(this.props.Document)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent + this.props.backgroundHalo?.() && this.props.Document.type !== DocumentType.INK ? (`${this.props.backgroundColor?.(this.props.Document)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent this.layoutDoc.isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big StrCast(this.layoutDoc.boxShadow, ""), borderRadius: StrCast(Doc.Layout(this.layoutDoc).borderRounding), diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 14a2c25bf..f140cc6e5 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -39,10 +39,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C @undoBatch private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { - event.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place - const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; - if (droppedDocs?.length) { - this.dataDoc[fieldKey] = droppedDocs[0]; + if (dropEvent.complete.docDragData) { + event.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place + const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; + if (droppedDocs?.length) { + this.dataDoc[fieldKey] = droppedDocs[0]; + } } } diff --git a/src/client/views/nodes/DocHolderBox.tsx b/src/client/views/nodes/DocHolderBox.tsx index a4c4663dd..0cf5505cc 100644 --- a/src/client/views/nodes/DocHolderBox.tsx +++ b/src/client/views/nodes/DocHolderBox.tsx @@ -119,6 +119,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do Document={containedDoc} DataDoc={undefined} LibraryPath={emptyPath} + docFilters={this.props.docFilters} ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected ContainingCollectionDoc={undefined} fitToBox={true} @@ -147,6 +148,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do Document={containedDoc} DataDoc={undefined} LibraryPath={emptyPath} + docFilters={this.props.docFilters} ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected ContainingCollectionDoc={undefined} fitToBox={true} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 126e9ac14..f1438fd54 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import { Doc, Opt, Field, AclSym, AclPrivate } from "../../../fields/Doc"; import { Cast, StrCast, NumCast } from "../../../fields/Types"; import { OmitKeys, Without, emptyPath } from "../../../Utils"; -import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox"; +import { DirectoryImportBox } from "../../util/Import & Export/DirectoryImportBox"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 2f868d558..707058aa7 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -17,10 +17,8 @@ import { GestureUtils } from '../../../pen-gestures/GestureUtils'; import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { ClientRecommender } from '../../ClientRecommender'; -import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; -import { ClientUtils } from '../../util/ClientUtils'; import { DocumentManager } from "../../util/DocumentManager"; import { SnappingManager } from '../../util/SnappingManager'; import { DragManager, dropActionType } from "../../util/DragManager"; @@ -54,6 +52,7 @@ export type DocFocusFunc = () => boolean; export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; + docFilters: () => string[]; FreezeDimensions?: boolean; NativeWidth: () => number; NativeHeight: () => number; @@ -138,7 +137,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.removeEndListeners(); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - console.log(SelectionManager.SelectedDocuments()); if (RadialMenu.Instance._display === false) { this.addHoldMoveListeners(); this.addHoldEndListeners(); @@ -327,20 +325,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const alias = Doc.MakeAlias(this.props.Document); DocUtils.makeCustomViewClicked(alias, undefined, "onClick"); this.props.addDocTab(alias, "onRight"); - // UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"); - //ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click"); } else if (this.props.Document.links && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { DocListCast(this.props.Document.links).length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey); } else { if ((this.props.Document.onDragStart || (this.props.Document.rootDocument)) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTEmplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template } else { - // if (this.props.Document.type === DocumentType.RTF) { - // DocumentView._focusHack = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY) || [0, 0]; - // DocumentView._focusHack = [DocumentView._focusHack[0] + NumCast(this.props.Document.x), DocumentView._focusHack[1] + NumCast(this.props.Document.y)]; - - // this.props.focus(this.props.Document, false); - // } SelectionManager.SelectDoc(this, e.ctrlKey || e.shiftKey); } preventDefault = false; @@ -580,7 +570,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @undoBatch - deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument?.(this.props.Document); } + deleteClicked = (): void => { + if (Doc.UserDoc().activeWorkspace === this.props.Document) { + alert("Can't delete the active workspace"); + } else { + SelectionManager.DeselectAll(); + this.props.removeDocument?.(this.props.Document); + } + } @undoBatch @@ -734,7 +731,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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); - optionItems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); templateDoc && optionItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); if (!options) { options = { description: "Options...", subitems: optionItems, icon: "compass" }; @@ -794,12 +790,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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" }); + helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("http://localhost:1050/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "onRight"), icon: "keyboard" }); + helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); + cm.addItem({ description: "Help...", subitems: helpItems, icon: "question" }); const existingAcls = cm?.findByDescription("Privacy..."); const aclItems: ContextMenuProps[] = existingAcls && "subitems" in existingAcls ? existingAcls.subitems : []; @@ -1003,8 +996,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); @computed get contents() { TraceMobx(); - return (<> + return (<div style={{ position: "absolute", width: "100%", height: "100%" }}> <DocumentContentsView key={1} + docFilters={this.props.docFilters} ContainingCollectionView={this.props.ContainingCollectionView} ContainingCollectionDoc={this.props.ContainingCollectionDoc} NativeWidth={this.NativeWidth} @@ -1015,6 +1009,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu LayoutTemplate={this.props.LayoutTemplate} makeLink={this.makeLink} rootSelected={this.rootSelected} + backgroundHalo={this.props.backgroundHalo} dontRegisterView={this.props.dontRegisterView} fitToBox={this.props.fitToBox} LibraryPath={this.props.LibraryPath} @@ -1040,7 +1035,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClick={this.onClickHandler} layoutKey={this.finalLayoutKey} /> {this.anchors} - </> + </div> ); } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index e9dc43bd8..c57738361 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -27,6 +27,8 @@ export interface FieldViewProps { LibraryPath: Doc[]; onClick?: ScriptField; dropAction: dropActionType; + backgroundHalo?: () => boolean; + docFilters: () => string[]; isSelected: (outsideReaction?: boolean) => boolean; select: (isCtrlPressed: boolean) => void; rootSelected: (outsideReaction?: boolean) => boolean; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index f5e996ca4..225d03104 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -396,12 +396,23 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD const aspect = (rotation % 180) ? nativeHeight / nativeWidth : 1; const shift = (rotation % 180) ? (nativeHeight - nativeWidth) * (1 - 1 / aspect) : 0; this.resize(srcpath); + let transformOrigin = "center center"; + let transform = `translate(0%, 0%) rotate(${rotation}deg) scale(${aspect})`; + if (rotation === 90 || rotation === -270) { + transformOrigin = "top left"; + transform = `translate(100%, 0%) rotate(${rotation}deg) scale(${aspect})`; + } else if (rotation === 180) { + transform = `rotate(${rotation}deg) scale(${aspect})`; + } else if (rotation === 270 || rotation === -90) { + transformOrigin = "right top"; + transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`; + } return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget}> <div className="imageBox-fader" > <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={srcpath} - style={{ transform: `scale(${aspect}) translate(0px, ${shift}px) rotate(${rotation}deg)` }} + style={{ transform, transformOrigin }} width={nativeWidth} ref={this._imgRef} onError={this.onError} /> @@ -409,7 +420,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD <img className="imageBox-fadeaway" key={"fadeaway" + this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={fadepath} - style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})`, }} + style={{ transform, transformOrigin }} width={nativeWidth} ref={this._imgRef} onError={this.onError} /></div>} @@ -447,7 +458,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD TraceMobx(); return (<div className={`imageBox`} onContextMenu={this.specificContextMenu} style={{ - transform: this.props.PanelWidth() ? `translate(0px, ${this.ycenter}px)` : `scale(${this.props.ContentScaling()})`, + transform: this.props.PanelWidth() ? undefined : `scale(${this.props.ContentScaling()})`, width: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, height: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, pointerEvents: this.layoutDoc.isBackground ? "none" : undefined, @@ -473,6 +484,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD CollectionView={undefined} ScreenToLocalTransform={this.screenToLocalTransform} renderDepth={this.props.renderDepth + 1} + docFilters={this.props.docFilters} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> {this.contentFunc} </CollectionFreeFormView> diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index e983852ea..d375466c9 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -18,6 +18,7 @@ import { KeyValuePair } from "./KeyValuePair"; import React = require("react"); import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; +import e = require("express"); export type KVPScript = { script: CompiledScript; @@ -31,10 +32,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> { private _mainCont = React.createRef<HTMLDivElement>(); private _keyHeader = React.createRef<HTMLTableHeaderCellElement>(); + private _keyInput = React.createRef<HTMLInputElement>(); + private _valInput = React.createRef<HTMLInputElement>(); @observable private rows: KeyValuePair[] = []; - @observable private _keyInput: string = ""; - @observable private _valueInput: string = ""; + @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); } get fieldDocToLayout() { return this.props.fieldKey ? Cast(this.props.Document[this.props.fieldKey], Doc, null) : this.props.Document; } @@ -42,10 +44,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> { onEnterKey = (e: React.KeyboardEvent): void => { if (e.key === 'Enter') { e.stopPropagation(); - if (this._keyInput && this._valueInput && this.fieldDocToLayout) { - if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput, this._valueInput)) { - this._keyInput = ""; - this._valueInput = ""; + if (this._keyInput.current?.value && this._valInput.current?.value && this.fieldDocToLayout) { + if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput.current.value, this._valInput.current.value)) { + this._keyInput.current.value = ""; + this._valInput.current.value = ""; + document.body.focus(); } } } @@ -103,7 +106,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { rowHeight = () => 30; - createTable = () => { + @computed get createTable() { const doc = this.fieldDocToLayout; if (!doc) { return <tr><td>Loading...</td></tr>; @@ -136,30 +139,18 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } return rows; } - - @action - keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => { - this._keyInput = e.currentTarget.value; + @computed get newKeyValue() { + return <tr className="keyValueBox-valueRow"> + <td className="keyValueBox-td-key" onClick={(e) => { this._keyInput.current!.select(); e.stopPropagation(); }} style={{ width: `${100 - this.splitPercentage}%` }}> + <input style={{ width: "100%" }} ref={this._keyInput} type="text" placeholder="Key" /> + </td> + <td className="keyValueBox-td-value" onClick={(e) => { this._valInput.current!.select(); e.stopPropagation(); }} style={{ width: `${this.splitPercentage}%` }}> + <input style={{ width: "100%" }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} /> + </td> + </tr>; } @action - valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => { - this._valueInput = e.currentTarget.value; - } - - newKeyValue = () => - ( - <tr className="keyValueBox-valueRow"> - <td className="keyValueBox-td-key" style={{ width: `${100 - this.splitPercentage}%` }}> - <input style={{ width: "100%" }} type="text" value={this._keyInput} placeholder="Key" onChange={this.keyChanged} /> - </td> - <td className="keyValueBox-td-value" style={{ width: `${this.splitPercentage}%` }}> - <input style={{ width: "100%" }} type="text" value={this._valueInput} placeholder="Value" onChange={this.valueChanged} onKeyDown={this.onEnterKey} /> - </td> - </tr> - ) - - @action onDividerMove = (e: PointerEvent): void => { const nativeWidth = this._mainCont.current!.getBoundingClientRect(); this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)); @@ -260,8 +251,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> { >Key</th> <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th> </tr> - {this.createTable()} - {this.newKeyValue()} + {this.createTable} + {this.newKeyValue} </tbody> </table> {dividerDragger} diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 3cbe3e494..4568a6b16 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,7 +1,7 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import { Doc, Field, Opt } from '../../../fields/Doc'; -import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils'; +import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; @@ -56,6 +56,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { Document: this.props.doc, DataDoc: this.props.doc, LibraryPath: [], + docFilters:returnEmptyFilter, ContainingCollectionView: undefined, ContainingCollectionDoc: undefined, fieldKey: this.props.keyName, diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index ad9e49369..360ead18e 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -77,7 +77,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument paddingBottom: NumCast(this.layoutDoc._yPadding), whiteSpace: this.layoutDoc._singleLine ? "pre" : "pre-wrap" }} > - {StrCast(this.rootDoc.text, StrCast(this.rootDoc.title))} + {StrCast(this.rootDoc[this.fieldKey], StrCast(this.rootDoc.title))} </div> <div className="labelBox-fieldKeyParams" > {!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)} diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 3c232eff8..233acc481 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -17,6 +17,7 @@ import { LinkEditor } from "../linking/LinkEditor"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SelectionManager } from "../../util/SelectionManager"; import { TraceMobx } from "../../../fields/util"; +import { Id } from "../../../fields/FieldSymbols"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -55,8 +56,8 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch DragManager.StartDocumentDrag([this._ref.current!], dragData, down[0], down[1]); return true; } else if (dragdist > separation) { - this.layoutDoc[this.fieldKey + "_x"] = (pt[0] - bounds.left) / bounds.width * 100; - this.layoutDoc[this.fieldKey + "_y"] = (pt[1] - bounds.top) / bounds.height * 100; + this.rootDoc[this.fieldKey + "_x"] = (pt[0] - bounds.left) / bounds.width * 100; + this.rootDoc[this.fieldKey + "_y"] = (pt[1] - bounds.top) / bounds.height * 100; } } return false; @@ -113,8 +114,8 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch render() { TraceMobx(); - const x = this.props.PanelWidth() > 1 ? NumCast(this.layoutDoc[this.fieldKey + "_x"], 100) : 0; - const y = this.props.PanelWidth() > 1 ? NumCast(this.layoutDoc[this.fieldKey + "_y"], 100) : 0; + const x = this.props.PanelWidth() > 1 ? NumCast(this.rootDoc[this.fieldKey + "_x"], 100) : 0; + const y = this.props.PanelWidth() > 1 ? NumCast(this.rootDoc[this.fieldKey + "_y"], 100) : 0; const c = StrCast(this.layoutDoc.backgroundColor, "lightblue"); const anchor = this.fieldKey === "anchor1" ? "anchor2" : "anchor1"; const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25; @@ -130,7 +131,8 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch </div> ); const small = this.props.PanelWidth() <= 1; - return <div className={`linkAnchorBox-cont${small ? "-small" : ""}`} onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu} + return <div className={`linkAnchorBox-cont${small ? "-small" : ""} ${this.rootDoc[Id]}`} + onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu} ref={this._ref} style={{ background: c, left: !small ? `calc(${x}% - 7.5px)` : undefined, diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index b19ddf1f5..974ddecdf 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -11,32 +11,53 @@ .pdfBox-ui { position: absolute; - width: 100%; - height: 100%; - z-index: 1; - pointer-events: none; + width: 100%; + height: 100%; + z-index: 1; + pointer-events: none; - .pdfBox-overlayButton { - border-bottom-left-radius: 50%; + .pdfBox-pageNums { display: flex; - justify-content: space-evenly; - align-items: center; - height: 20px; - background: none; - padding: 0; + flex-direction: row; + height: 25px; position: absolute; - pointer-events: all; - - .pdfBox-overlayButton-arrow { - width: 0; - height: 0; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - border-right: 15px solid #121721; - transition: all 0.5s; + left: 5px; + top: 5px; + .pdfBox-overlayButton-fwd, + .pdfBox-overlayButton-back { + background: #121721; + height: 25px; + width: 25px; + display: flex; + position: relative; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; } - - .pdfBox-overlayButton-iconCont { + } + + .pdfBox-overlayButton { + border-bottom-left-radius: 50%; + display: flex; + justify-content: space-evenly; + align-items: center; + height: 20px; + background: none; + padding: 0; + position: absolute; + pointer-events: all; + + .pdfBox-overlayButton-arrow { + width: 0; + height: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + border-right: 15px solid #121721; + transition: all 0.5s; + } + + .pdfBox-overlayButton-iconCont { background: #121721; height: 20px; width: 25px; @@ -49,29 +70,6 @@ } } - .pdfBox-overlayButton-fwd, - .pdfBox-overlayButton-back { - background: #121721; - height: 25px; - width: 25px; - display: flex; - position: relative; - align-items: center; - justify-content: center; - border-radius: 3px; - pointer-events: all; - position: absolute; - top: 5; - } - - .pdfBox-overlayButton-fwd { - left: 45; - } - - .pdfBox-overlayButton-back { - left: 25; - } - .pdfBox-nextIcon, .pdfBox-prevIcon { background: #121721; @@ -363,4 +361,4 @@ padding: 0px; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 4b4348d3c..c1a0be9ea 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -146,11 +146,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" /> </button> </>; + const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`; + const curPage = this.Document.curPage || 1; return !this.active() ? (null) : (<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true} onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none" }}> <div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> - <button className="pdfBox-overlayButton" title="Open Search Bar" /> + <button className="pdfBox-overlayButton" title={searchTitle} /> <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" color="white" /></button> @@ -161,16 +163,22 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="lg" /> </button> </div> - <button className="pdfBox-overlayButton" key="search" onClick={action(() => this._searching = !this._searching)} title="Open Search Bar" style={{ bottom: 0, right: 0 }}> + <button className="pdfBox-overlayButton" key="search" onClick={action(() => { + this._searching = !this._searching; + this.search("mxytzlaf", true); + })} title={searchTitle} style={{ bottom: 0, right: 0 }}> <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="lg" /></div> </button> - <input value={`${(this.Document.curPage || 1)}`} - onChange={e => this.gotoPage(Number(e.currentTarget.value))} - style={{ left: 5, top: 5, height: "20px", width: "20px", position: "absolute", pointerEvents: "all" }} - onClick={action(() => this._pageControls = !this._pageControls)} /> - {this._pageControls ? pageBtns : (null)} + + <div className="pdfBox-pageNums"> + <input value={curPage} + onChange={e => this.gotoPage(Number(e.currentTarget.value))} + style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: "all" }} + onClick={action(() => this._pageControls = !this._pageControls)} /> + {this._pageControls ? pageBtns : (null)} + </div> <div className="pdfBox-settingsCont" key="settings" onPointerDown={(e) => e.stopPropagation()}> <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings" > <div className="pdfBox-settingsButton-arrow" style={{ transform: `scaleX(${this._flyout ? -1 : 1})` }} /> @@ -238,7 +246,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded} setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView} renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - addDocTab={this.props.addDocTab} focus={this.props.focus} + addDocTab={this.props.addDocTab} focus={this.props.focus} docFilters={this.props.docFilters} pinToPres={this.props.pinToPres} addDocument={this.addDocument} Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling} ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select} @@ -252,7 +260,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum _pdfjsRequested = false; render() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null); - if (this.props.isSelected() || this.props.renderDepth <= 1 || this.props.Document._scrollY !== undefined) this._everActive = true; + if (this.props.isSelected() || this.props.renderDepth === 0 || this.props.Document._scrollY !== undefined) this._everActive = true; if (pdfUrl && (this._everActive || this.props.Document._scrollTop || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) { if (pdfUrl instanceof PdfField && this._pdf) { return this.renderPdfView; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index e4dbceca6..71556bfd3 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -383,6 +383,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD CollectionView={undefined} ScreenToLocalTransform={this.props.ScreenToLocalTransform} renderDepth={this.props.renderDepth + 1} + docFilters={this.props.docFilters} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> {this.contentFunc} </CollectionFreeFormView> diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index fb9e57b51..b726a6df9 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -488,6 +488,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum CollectionView={undefined} ScreenToLocalTransform={this.scrollXf} renderDepth={this.props.renderDepth + 1} + docFilters={this.props.docFilters} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> </CollectionFreeFormView> </div> diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 55b3f6f1e..5c3f3dcc9 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -5,7 +5,7 @@ import { Id } from "../../../../fields/FieldSymbols"; import { ObjectField } from "../../../../fields/ObjectField"; import { ComputedField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero } from "../../../../Utils"; +import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero, returnEmptyFilter } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; import { DocumentView } from "../DocumentView"; @@ -254,6 +254,7 @@ export class DashDocView extends React.Component<IDashDocView> { whenActiveChanged={returnFalse} bringToFront={emptyFunction} dontRegisterView={false} + docFilters={this.props.tbox?.props.docFilters||returnEmptyFilter} ContainingCollectionView={this._textBox.props.ContainingCollectionView} ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc} ContentScaling={this.contentScaling} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 7b0ceb6cf..5c084ae92 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -73,7 +73,7 @@ .collectionfreeformview-container { position: relative; } - + >.formattedTextBox-sidebar-handle { right: unset; left: -5; @@ -93,10 +93,13 @@ left: 10%; } -.formattedTextBox-inner-rounded, -.formattedTextBox-inner { +.formattedTextBox-inner-rounded, .formattedTextBox-inner-rounded-selected, +.formattedTextBox-inner, .formattedTextBox-inner-selected { height: 100%; - white-space: pre-wrap; + white-space: pre-wrap; + .ProseMirror:hover { + background: rgba(200,200,200,0.8); + } hr { display: block; unicode-bidi: isolate; @@ -249,34 +252,37 @@ footnote::after { .prosemirror-links { display: none; position: absolute; - background-color: gray; - padding-bottom: 10px; - margin-top: 1em; + background-color: dimgray; + margin-top: 1.5em; z-index: 1; + padding: 5; + border-radius: 2px; } .prosemirror-hrefoptions{ width:0px; border:unset; padding:0px; - } - + .prosemirror-links a { float: left; color: white; text-decoration: none; + border-radius: 3px; } - + .prosemirror-links a:hover { background-color: #eee; color: black; } - + .prosemirror-anchor:hover .prosemirror-links { display: grid; } .ProseMirror { + padding: 0px; + height: max-content; touch-action: none; span { font-family: inherit; @@ -291,6 +297,9 @@ footnote::after { margin-left: 1em; font-family: inherit; } + .bullet { p {display: inline; font-family: inherit} margin-left: 0; } + .bullet1 { p {display: inline; font-family: inherit} } + .bullet2,.bullet3,.bullet4,.bullet5,.bullet6 { p {display: inline; font-family: inherit} font-size: smaller; } .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; } .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;} @@ -305,6 +314,8 @@ footnote::after { .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;} + .bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; margin-left: -1em; width: 1em; content:" " } + .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; } .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; } .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; } @@ -312,11 +323,21 @@ footnote::after { .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; } .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; } .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; } - + .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; } .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; } .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; } - .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } + .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } +} + +.formattedTextBox-inner-rounded-selected, +.formattedTextBox-inner-selected { + .ProseMirror { + padding:10px; + } + .ProseMirror:hover { + background: unset; + } } @media only screen and (max-width: 1000px) { @@ -327,11 +348,11 @@ footnote::after { height: 100%; min-height: 100%; } - + .ProseMirror:focus { outline: none !important; } - + .formattedTextBox-cont { touch-action: none; cursor: text; @@ -350,7 +371,7 @@ footnote::after { display: flex; flex-direction: row; transition: opacity 1s; - + .formattedTextBox-dictation { height: 12px; width: 10px; @@ -359,7 +380,7 @@ footnote::after { position: absolute; } } - + .formattedTextBox-outer { position: relative; overflow: auto; @@ -367,7 +388,7 @@ footnote::after { width: 100%; height: 100%; } - + .formattedTextBox-sidebar-handle { position: absolute; top: calc(50% - 17.5px); @@ -377,12 +398,12 @@ footnote::after { border-radius: 20px; cursor:grabbing; } - + .formattedTextBox-cont>.formattedTextBox-sidebar-handle { right: 0; left: unset; } - + .formattedTextBox-sidebar, .formattedTextBox-sidebar-inking { border-left: dashed 1px black; @@ -390,21 +411,21 @@ footnote::after { display: inline-block; position: absolute; right: 0; - + .collectionfreeformview-container { position: relative; } - + >.formattedTextBox-sidebar-handle { right: unset; left: -5; } } - + .formattedTextBox-sidebar-inking { pointer-events: all; } - + .formattedTextBox-inner-rounded { height: 70%; width: 85%; @@ -413,11 +434,11 @@ footnote::after { top: 15%; left: 10%; } - + .formattedTextBox-inner-rounded, .formattedTextBox-inner { height: 100%; - white-space: pre-wrap; + white-space: pre-wrap; hr { display: block; unicode-bidi: isolate; @@ -430,7 +451,7 @@ footnote::after { border-width: 1px; } } - + // .menuicon { // display: inline-block; // border-right: 1px solid rgba(0, 0, 0, 0.2); @@ -442,21 +463,21 @@ footnote::after { // text-align: center; // min-width: 1.4em; // } - + .strong, .heading { font-weight: bold; } - + .em { font-style: italic; } - + .userMarkOpen { background: rgba(255, 255, 0, 0.267); display: inline; } - + .userMark { background: rgba(255, 255, 0, 0.267); font-size: 2px; @@ -469,28 +490,28 @@ footnote::after { text-align: center; align-content: center; } - + footnote { display: inline-block; position: relative; cursor: pointer; - + div { padding: 0 !important; } } - + footnote::after { content: counter(prosemirror-footnote); vertical-align: super; font-size: 75%; counter-increment: prosemirror-footnote; } - + .ProseMirror { counter-reset: prosemirror-footnote; } - + .footnote-tooltip { cursor: auto; font-size: 75%; @@ -504,11 +525,11 @@ footnote::after { min-width: 50px; width: max-content; } - + .prosemirror-attribution { font-size: 8px; } - + .footnote-tooltip::before { border: 5px solid silver; border-top-width: 0px; @@ -521,8 +542,8 @@ footnote::after { height: 0; width: 0; } - - + + .formattedTextBox-inlineComment { position: relative; width: 40px; @@ -534,7 +555,7 @@ footnote::after { background: orange; } } - + .formattedTextBox-summarizer { opacity: 0.5; position: relative; @@ -544,7 +565,7 @@ footnote::after { content: "←"; } } - + .formattedTextBox-summarizer-collapsed { opacity: 0.5; position: relative; @@ -554,13 +575,13 @@ footnote::after { content: "..."; } } - + .ProseMirror { touch-action: none; span { font-family: inherit; } - + ol, ul { counter-reset: deci1 0 multi1 0; padding-left: 1em; @@ -570,7 +591,7 @@ footnote::after { margin-left: 1em; font-family: inherit; } - + .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; } .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;} .decimal3-ol { counter-reset: deci3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} @@ -578,12 +599,12 @@ footnote::after { .decimal5-ol { counter-reset: deci5; p {display: inline; font-family: inherit} font-size: smaller; } .decimal6-ol { counter-reset: deci6; p {display: inline; font-family: inherit} font-size: smaller; } .decimal7-ol { counter-reset: deci7; p {display: inline; font-family: inherit} font-size: smaller; } - + .multi1-ol { counter-reset: multi1; p {display: inline; font-family: inherit} margin-left: 0; padding-left: 1.2em } .multi2-ol { counter-reset: multi2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1.4em;} .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;} - + .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; } .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; } .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; } @@ -591,7 +612,7 @@ footnote::after { .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; } .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; } .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; } - + .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; } .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; } .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index ae508cd67..a5a40a31f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -13,8 +13,9 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from " import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../../fields/DateField'; -import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../../fields/Doc"; +import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclSym } from "../../../../fields/Doc"; import { documentSchema } from '../../../../fields/documentSchemas'; +import applyDevTools = require("prosemirror-dev-tools"); import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { PrefetchProxy } from '../../../../fields/Proxy'; @@ -22,7 +23,7 @@ import { RichTextField } from "../../../../fields/RichTextField"; import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { createSchema, makeInterface } from "../../../../fields/Schema"; import { Cast, DateCast, NumCast, StrCast } from "../../../../fields/Types"; -import { TraceMobx } from '../../../../fields/util'; +import { TraceMobx, OVERRIDE_ACL } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../../DocServer"; @@ -57,7 +58,6 @@ import { FieldView, FieldViewProps } from "../FieldView"; import "./FormattedTextBox.scss"; import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment'; import React = require("react"); -import { InkingStroke } from '../../InkingStroke'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -199,22 +199,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template const json = JSON.stringify(state.toJSON()); - if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) { - this._applyingChange = true; - this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); - if ((!curTemp && !curProto) || curText || curLayout?.Data.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if (json !== curLayout?.Data) { - !curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize)); - !curText && tx.storedMarks?.map(m => m.type.name === "pFontFamily" && (Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily)); - this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText); - this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited + if (!this.dataDoc[AclSym]) { + if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) { + this._applyingChange = true; + this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + if ((!curTemp && !curProto) || curText || curLayout?.Data.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) + if (json !== curLayout?.Data) { + !curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize)); + !curText && tx.storedMarks?.map(m => m.type.name === "pFontFamily" && (Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily)); + this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText); + this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited + } + } else { // if we've deleted all the text in a note driven by a template, then restore the template data + this.dataDoc[this.props.fieldKey] = undefined; + this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data))); + this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have } - } else { // if we've deleted all the text in a note driven by a template, then restore the template data - this.dataDoc[this.props.fieldKey] = undefined; - this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data))); - this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have + this._applyingChange = false; } - this._applyingChange = false; + } else { + const json = JSON.parse(Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!); + json.selection = state.toJSON().selection; + this._editorView.updateState(EditorState.fromJSON(this.config, json)); } this.updateTitle(); this.tryUpdateHeight(); @@ -417,42 +423,66 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; - const funcs: ContextMenuProps[] = []; - this.rootDoc.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); - !this.layoutDoc.isTemplateDoc && funcs.push({ - description: "Convert to use as a style", event: () => { - this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc); - Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc); - }, icon: "eye" + const appearance = ContextMenu.Instance.findByDescription("Appearance..."); + const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; + + const changeItems: ContextMenuProps[] = []; + const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null); + DocListCast(noteTypesDoc?.data).forEach(note => { + changeItems.push({ + description: StrCast(note.title), event: undoBatch(() => { + Doc.setNativeView(this.rootDoc); + DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); + }), icon: "eye" + }); }); - this.layoutDoc.isTemplateDoc && funcs.push({ - description: "Make New Template", event: () => { - const title = this.rootDoc.title as string; - this.rootDoc.layout = (this.layoutDoc as Doc).layout as string; - this.rootDoc.title = this.layoutDoc.isTemplateForField as string; - this.rootDoc.isTemplateDoc = false; - this.rootDoc.isTemplateForField = ""; - this.rootDoc.layoutKey = "layout"; - this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title); - setTimeout(() => { - this.rootDoc._width = this.layoutDoc._width || 300; // the width and height are stored on the template, since we're getting rid of the old template - this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields - this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, "string", null); - }, 10); + changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); + appearanceItems.push({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" }); + const uicontrols: ContextMenuProps[] = []; + uicontrols.push({ description: "Toggle Sidebar", event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" }); + uicontrols.push({ description: "Toggle Dictation Icon", event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); + uicontrols.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" }); + !Doc.UserDoc().noviceMode && uicontrols.push({ + description: "Broadcast Message", event: () => DocServer.GetRefField("rtfProto").then(proto => + proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)), icon: "expand-arrows-alt" + }); + + appearanceItems.push({ description: "UI Controls...", subitems: uicontrols, icon: "asterisk" }); + this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); + Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); + appearanceItems.push({ + description: "Convert to be a template style", event: () => { + if (!this.layoutDoc.isTemplateDoc) { + const title = StrCast(this.rootDoc.title); + this.rootDoc.title = "text"; + this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title); + } else { + const title = StrCast(this.rootDoc.title); + this.rootDoc.title = "text"; + this.rootDoc.layout = (this.layoutDoc as Doc).layout as string; + this.rootDoc.title = this.layoutDoc.isTemplateForField as string; + this.rootDoc.isTemplateDoc = false; + this.rootDoc.isTemplateForField = ""; + this.rootDoc.layoutKey = "layout"; + this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title); + setTimeout(() => { + this.rootDoc._autoHeight = this.layoutDoc._autoHeight; // autoHeight, width and height + this.rootDoc._width = this.layoutDoc._width || 300; // are stored on the template, since we're getting rid of the old template + this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields + this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, "string", null); + }, 10); + } Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc); }, icon: "eye" }); + !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); + + const funcs: ContextMenuProps[] = []; + //funcs.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); funcs.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); funcs.push({ description: "Toggle Single Line", event: () => this.layoutDoc._singleLine = !this.layoutDoc._singleLine, icon: "expand-arrows-alt" }); - const uicontrols: ContextMenuProps[] = []; - uicontrols.push({ description: "Toggle Sidebar", event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" }); - uicontrols.push({ description: "Toggle Dictation Icon", event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); - uicontrols.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" }); - - funcs.push({ description: "UI Controls...", subitems: uicontrols, icon: "asterisk" }); - const highlighting: ContextMenuProps[] = []; ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => highlighting.push({ @@ -469,21 +499,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp funcs.push({ description: "highlighting...", subitems: highlighting, icon: "hand-point-right" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); - - const change = cm.findByDescription("Change Perspective..."); - const changeItems: ContextMenuProps[] = change && "subitems" in change ? change.subitems : []; - - const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null); - DocListCast(noteTypesDoc?.data).forEach(note => { - changeItems.push({ - description: StrCast(note.title), event: undoBatch(() => { - Doc.setNativeView(this.rootDoc); - DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); - }), icon: "eye" - }); - }); - changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); - !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" }); + this._downX = this._downY = Number.NaN; } recordDictation = () => { @@ -584,10 +600,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }; } - makeLinkToSelection(linkId: string, title: string, location: string, targetId: string) { + makeLinkToSelection(linkId: string, title: string, location: string, targetId: string, targetHref?: string) { const state = this._editorView?.state; if (state) { - const href = Utils.prepend("/doc/" + linkId); + const href = targetHref ?? Utils.prepend("/doc/" + linkId); const sel = state.selection; const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); let tr = state.tr.addMark(sel.from, sel.to, splitter); @@ -595,11 +611,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { const allHrefs = [{ href, title, targetId, linkId }]; allHrefs.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.link.name)?.attrs.allHrefs ?? [])); - const link = state.schema.marks.link.create({ href, allHrefs, title, location, linkId, targetId }); + const link = state.schema.marks.link.create({ allHrefs, title, location, linkId }); tr = tr.addMark(pos, pos + node.nodeSize, link); } }); + OVERRIDE_ACL(true); this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); + OVERRIDE_ACL(false); } } componentDidMount() { @@ -914,11 +932,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); + // applyDevTools.applyDevTools(this._editorView); const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field); if (startupText) { const { state: { tr }, dispatch } = this._editorView; dispatch(tr.insertText(startupText)); } + (this._editorView as any).TextView = this; } const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad; @@ -989,15 +1009,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (!FormattedTextBox._downEvent) return; FormattedTextBox._downEvent = false; if (!(e.nativeEvent as any).formattedHandled) { + const editor = this._editorView!; FormattedTextBoxComment.textBox = this; - FormattedTextBoxComment.update(this._editorView!, undefined, (e.target as any)?.className === "prosemirror-dropdownlink" ? (e.target as any).href : ""); + const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); + const node = pcords && editor.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) + !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(node && pcords ? + new NodeSelection(editor.state.doc.resolve(pcords.pos)) : new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); + FormattedTextBoxComment.update(editor, undefined, (e.target as any)?.className === "prosemirror-dropdownlink" ? (e.target as any).href : ""); } (e.nativeEvent as any).formattedHandled = true; if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) { e.stopPropagation(); } - this._downX = this._downY = Number.NaN; } @action @@ -1018,6 +1042,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (bounds && this.layoutDoc._chromeStatus !== "disabled") { const x = Math.min(Math.max(bounds.left, 0), window.innerWidth - RichTextMenu.Instance.width); let y = Math.min(Math.max(0, bounds.top - RichTextMenu.Instance.height - 50), window.innerHeight - RichTextMenu.Instance.height); + console.log("y = " + y + " hgt = " + RichTextMenu.Instance.height + " cords = " + coords.top); if (coords && coords.left > x && coords.left < x + RichTextMenu.Instance.width && coords.top > y && coords.top < y + RichTextMenu.Instance.height + 50) { y = Math.min(bounds.bottom, window.innerHeight - RichTextMenu.Instance.height); } @@ -1052,48 +1077,42 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } (e.nativeEvent as any).formattedHandled = true; - if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientX - this._downX) < 4) { - this.props.select(e.ctrlKey); - this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false); - } if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events e.stopPropagation(); + this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false); } } // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. - hitBulletTargets(x: number, y: number, select: boolean, highlightOnly: boolean) { + hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean) { clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); - const pos = this._editorView!.posAtCoords({ left: x, top: y }); - if (pos && this.props.isSelected(true)) { - // let beforeEle = document.querySelector("." + hit.className) as Element; // const before = hit ? window.getComputedStyle(hit, ':before') : undefined; - //const node = this._editorView!.state.doc.nodeAt(pos.pos); - const $pos = this._editorView!.state.doc.resolve(pos.pos); - let list_node = $pos.node().type === schema.nodes.list_item ? $pos.node() : undefined; - if ($pos.node().type === schema.nodes.ordered_list) { - for (let off = 1; off < 100; off++) { - const pos = this._editorView!.posAtCoords({ left: x + off, top: y }); - const node = pos && this._editorView!.state.doc.nodeAt(pos.pos); - if (node?.type === schema.nodes.list_item) { - list_node = node; - break; - } + const clickPos = this._editorView!.posAtCoords({ left: x, top: y }); + let olistPos = clickPos?.pos; + if (clickPos && olistPos && this.props.isSelected(true)) { + const clickNode = this._editorView?.state.doc.nodeAt(olistPos); + const nodeBef = this._editorView?.state.doc.nodeAt(Math.max(0, olistPos - 1)); + olistPos = nodeBef?.type === this._editorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos; + let $olistPos = this._editorView?.state.doc.resolve(olistPos); + let olistNode = (nodeBef !== null || clickNode?.type === this._editorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef; + if (olistNode?.type === this._editorView?.state.schema.nodes.list_item) { + if ($olistPos && ($olistPos as any).path.length > 3) { + olistNode = $olistPos.parent; + $olistPos = this._editorView?.state.doc.resolve(($olistPos as any).path[($olistPos as any).path.length - 4]); } } - if (list_node && pos.inside >= 0 && this._editorView!.state.doc.nodeAt(pos.inside)!.attrs.bulletStyle === list_node.attrs.bulletStyle) { - if (select) { - const $olist_pos = this._editorView!.state.doc.resolve($pos.pos - $pos.parentOffset - 1); + const listNode = this._editorView?.state.doc.nodeAt(clickPos.pos); + if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list) { + if (!collapse) { if (!highlightOnly) { - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection($olist_pos))); + this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection($olistPos!))); } - addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); - } else if (Math.abs(pos.pos - pos.inside) < 2) { + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); + } else if (listNode && listNode.type === this._editorView.state.schema.nodes.list_item) { if (!highlightOnly) { - const offset = this._editorView!.state.doc.nodeAt(pos.inside)?.type === schema.nodes.ordered_list ? 1 : 0; - this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.inside + offset, list_node.type, { ...list_node.attrs, visibility: !list_node.attrs.visibility })); - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pos.inside + offset))); + this._editorView.dispatch(this._editorView.state.tr.setNodeMarkup(clickPos.pos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility })); + this._editorView.dispatch(this._editorView.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, clickPos.pos))); } - addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); } } } @@ -1190,18 +1209,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.dataDoc._nativeHeight, 0); const dh = NumCast(this.rootDoc._height, 0); const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); - if (Math.abs(newHeight - dh) > 1) { // bcz: Argh! without this, we get into a React crash if the same document is opened in a freeform view and in the treeview. no idea why, but after dragging the freeform document, selecting it, and selecting text, it will compute to 1 pixel higher than the treeview which causes a cycle - if (this.rootDoc !== this.layoutDoc.doc && !this.layoutDoc.resolvedDataDoc) { - // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... - console.log("Delayed height adjustment..."); - setTimeout(() => { - this.rootDoc._height = newHeight; - this.dataDoc._nativeHeight = nh ? scrollHeight : undefined; - }, 10); - } else { + if (this.rootDoc !== this.layoutDoc.doc && !this.layoutDoc.resolvedDataDoc) { + // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... + console.log("Delayed height adjustment..."); + setTimeout(() => { this.rootDoc._height = newHeight; this.dataDoc._nativeHeight = nh ? scrollHeight : undefined; - } + }, 10); + } else { + this.rootDoc._height = newHeight; + this.dataDoc._nativeHeight = nh ? scrollHeight : undefined; } } } @@ -1220,16 +1237,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } else if (FormattedTextBoxComment.textBox === this) { setTimeout(() => FormattedTextBoxComment.Hide(), 0); } + const selPad = this.props.isSelected() ? -10 : 0; + const selclass = this.props.isSelected() ? "-selected" : ""; return ( - <div className={"formattedTextBox-cont"} style={{ - transform: `scale(${scale})`, - transformOrigin: "top left", - width: `${100 / scale}%`, - height: `calc(${100 / scale}% - ${this.props.ChromeHeight?.() || 0}px)`, - ...this.styleFromLayoutString(scale) - }}> + <div className={"formattedTextBox-cont"} + style={{ + transform: `scale(${scale})`, + transformOrigin: "top left", + width: `${100 / scale}%`, + height: `calc(${100 / scale}% - ${this.props.ChromeHeight?.() || 0}px)`, + ...this.styleFromLayoutString(scale) + }}> <div className={`formattedTextBox-cont`} ref={this._ref} style={{ + overflow: this.layoutDoc._autoHeight ? "hidden" : undefined, width: "100%", height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : undefined, background: Doc.UserDoc().renderStyle === "comic" ? "transparent" : this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""), @@ -1260,12 +1281,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } })} > - <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} onScroll={this.onscrolled} ref={this._scrollRef}> - <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} + <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !this.props.isSelected() ? "none" : undefined }} onScroll={this.onscrolled} ref={this._scrollRef}> + <div className={`formattedTextBox-inner${rounded}${selclass}`} ref={this.createDropTarget} style={{ - padding: `${NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0)}px`, - pointerEvents: ((this.layoutDoc.isLinkButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined - }} /> + padding: `${Math.max(0, NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0) + selPad)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0) + selPad}px`, + pointerEvents: !this.props.isSelected() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : "all") : undefined + }} + /> </div> {!this.layoutDoc._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ? <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> : diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 000b3c2e5..0d8e22251 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -4,7 +4,7 @@ import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; import { Doc, DocCastAsync } from "../../../../fields/Doc"; import { Cast, FieldValue, NumCast } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne } from "../../../../Utils"; +import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne, returnEmptyFilter } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { DocumentManager } from "../../../util/DocumentManager"; import { schema } from "./schema_rts"; @@ -222,9 +222,10 @@ export class FormattedTextBoxComment { addDocTab={returnFalse} pinToPres={returnFalse} dontRegisterView={true} + docFilters={returnEmptyFilter} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} - renderDepth={1} + renderDepth={0} PanelWidth={() => Math.min(350, NumCast(target._width, 350))} PanelHeight={() => Math.min(250, NumCast(target._height, 250))} focus={emptyFunction} diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 0a4c52ef9..75cfe6bd1 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -16,16 +16,13 @@ const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : export type KeyMap = { [key: string]: any }; -export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string) => { - let fontSize: number | undefined = undefined; +export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string, from?: number, to?: number) => { tx2.doc.descendants((node: any, offset: any, index: any) => { - if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) { + if ((from === undefined || to === undefined || (from <= offset + node.nodeSize && to >= offset)) && (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item)) { const path = (tx2.doc.resolve(offset) as any).path; let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0); if (node.type === schema.nodes.ordered_list) depth++; - fontSize = depth === 1 && node.attrs.setFontSize ? Number(node.attrs.setFontSize) : fontSize; - const fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined; - tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle ? mapStyle : node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks); + tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle || node.attrs.mapStyle, bulletStyle: depth, }, node.marks); } }); return tx2; @@ -62,7 +59,6 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any bind("Mod-U", toggleMark(schema.marks.underline)); //Commands for lists - bind("Ctrl-.", wrapInList(schema.nodes.bullet_list)); bind("Ctrl-i", wrapInList(schema.nodes.ordered_list)); bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 03d393cde..1a961ae21 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -12,7 +12,7 @@ import { faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSubscr import { updateBullets } from "./ProsemirrorExampleTransfer"; import { FieldViewProps } from "../FieldView"; import { Cast, StrCast } from "../../../../fields/Types"; -import { FormattedTextBoxProps } from "./FormattedTextBox"; +import { FormattedTextBoxProps, FormattedTextBox } from "./FormattedTextBox"; import { unimplementedFunction, Utils } from "../../../../Utils"; import { wrapInList } from "prosemirror-schema-list"; import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../../../fields/SchemaHeaderField'; @@ -104,7 +104,7 @@ export default class RichTextMenu extends AntimodeMenu { { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType }, { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType }, { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType }, - { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType }, + //{ node: undefined, title: "Set list type", label: "Remove", command: this.changeListType }, ]; this.fontColors = [ @@ -189,9 +189,9 @@ export default class RichTextMenu extends AntimodeMenu { const node = (state.selection as NodeSelection).node; if (node?.type === schema.nodes.ordered_list) { let attrs = node.attrs; - if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family }; - if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize }; - if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color }; + if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family }; + if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: `${mark.attrs.fontSize}px` }; + if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color }; const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema); dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from)))); } else { @@ -307,7 +307,6 @@ export default class RichTextMenu extends AntimodeMenu { function onClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.view && command && command(self.view.state, self.view.dispatch, self.view); self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view); self.setActiveMarkButtons(self.getActiveMarksOnSelection()); @@ -378,26 +377,24 @@ export default class RichTextMenu extends AntimodeMenu { // TODO: remove doesn't work //remove all node type and apply the passed-in one to the selected text - changeListType = (nodeType: NodeType | undefined) => { + changeListType = (nodeType: Node | undefined) => { if (!this.view) return; - if (nodeType === schema.nodes.bullet_list) { - wrapInList(nodeType)(this.view.state, this.view.dispatch); - } else { - const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); - if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => { - const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle); - marks && tx3.ensureMarks([...marks]); - marks && tx3.setStoredMarks([...marks]); - - this.view!.dispatch(tx2); - })) { - const tx2 = this.view.state.tr; - const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle); + const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); + if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => { + const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1); + marks && tx3.ensureMarks([...marks]); + marks && tx3.setStoredMarks([...marks]); + + this.view!.dispatch(tx2); + })) { + const tx2 = this.view.state.tr; + if (nodeType && this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list) { + const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view.state.selection.from, this.view.state.selection.to); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); - this.view.dispatch(tx3); + this.view.dispatch(tx3.setSelection(new NodeSelection(tx3.doc.resolve(this.view.state.selection.$from.pos)))); } } } @@ -429,7 +426,6 @@ export default class RichTextMenu extends AntimodeMenu { function onBrushClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.view && self.fillBrush(self.view.state, self.view.dispatch); } @@ -503,13 +499,11 @@ export default class RichTextMenu extends AntimodeMenu { function onColorClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch); } function changeColor(e: React.PointerEvent, color: string) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.setActiveColor(color); self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch); } @@ -556,13 +550,11 @@ export default class RichTextMenu extends AntimodeMenu { function onHighlightClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch); } function changeHighlight(e: React.PointerEvent, color: string) { e.preventDefault(); e.stopPropagation(); - self.view && self.view.focus(); self.setActiveHighlight(color); self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch); } @@ -661,15 +653,8 @@ export default class RichTextMenu extends AntimodeMenu { } // TODO: should check for valid URL - makeLinkToURL = (target: String, lcoation: string) => { - if (!this.view) return; - - let node = this.view.state.selection.$from.nodeAfter; - let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location }); - this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link)); - this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link)); - node = this.view.state.selection.$from.nodeAfter; - link = node && node.marks.find(m => m.type.name === "link"); + makeLinkToURL = (target: string, lcoation: string) => { + ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRight", "", target); } deleteLink = () => { @@ -762,13 +747,14 @@ export default class RichTextMenu extends AntimodeMenu { this.collapsed = !this.collapsed; setTimeout(() => { const x = Math.min(this._left, window.innerWidth - RichTextMenu.Instance.width); - RichTextMenu.Instance.jumpTo(x, this._top); + RichTextMenu.Instance.jumpTo(x, this._top, true); }, 0); } render() { const row1 = <div className="antimodeMenu-row" key="row1" style={{ display: this.collapsed ? "none" : undefined }}>{[ + !this.collapsed ? this.getDragger() : (null), this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), @@ -783,6 +769,7 @@ export default class RichTextMenu extends AntimodeMenu { ]}</div>; const row2 = <div className="antimodeMenu-row row-2" key="antimodemenu row2"> + {this.collapsed ? this.getDragger() : (null)} <div key="row" style={{ display: this.collapsed ? "none" : undefined }}> {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"), this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"), @@ -797,7 +784,6 @@ export default class RichTextMenu extends AntimodeMenu { <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}> <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> </button> - {this.getDragger()} </div> </div>; @@ -842,7 +828,6 @@ class ButtonDropdown extends React.Component<ButtonDropdownProps> { onDropdownClick = (e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); - this.props.view && this.props.view.focus(); this.toggleDropdown(); } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 91187edf9..ba3230801 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -59,7 +59,16 @@ export class RichTextRules { ), // * + - create bullet list - wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list), + wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.ordered_list, + // match => { + () => { + return ({ mapStyle: "bullet" }); + // return ({ order: +match[1] }) + }, + (match: any, node: any) => { + return node.childCount + node.attrs.order === +match[1]; + }, + (type: any) => ({ type: type, attrs: { mapStyle: "bullet" } })), // ``` create code block textblockTypeInputRule(/^```$/, schema.nodes.code_block), diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx index 05557e22a..a989abd6a 100644 --- a/src/client/views/nodes/formattedText/RichTextSchema.tsx +++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx @@ -152,6 +152,7 @@ export class DashDocView { whenActiveChanged={returnFalse} bringToFront={emptyFunction} dontRegisterView={false} + docFilters={this._textBox.props.docFilters} ContainingCollectionView={this._textBox.props.ContainingCollectionView} ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc} ContentScaling={this.contentScaling} diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx index 922285bd0..c017db034 100644 --- a/src/client/views/nodes/formattedText/SummaryView.tsx +++ b/src/client/views/nodes/formattedText/SummaryView.tsx @@ -14,7 +14,7 @@ export class SummaryView { 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.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(); }; diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 49d5c96a4..1de211f28 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -53,21 +53,45 @@ export const marks: { [index: string]: MarkSpec } = { } }, + /** FONT SIZES */ + pFontSize: { + attrs: { fontSize: { default: 10 } }, + parseDOM: [{ + tag: "span", getAttrs(dom: any) { + return { fontSize: dom.style.fontSize ? Number(dom.style.fontSize.replace("px", "")) : "" }; + } + }], + toDOM: (node) => node.attrs.fontSize ? ['span', { style: `font-size: ${node.attrs.fontSize}px;` }] : ['span', 0] + }, + /* FONTS */ + pFontFamily: { + attrs: { family: { default: "" } }, + parseDOM: [{ + tag: "span", getAttrs(dom: any) { + const cstyle = getComputedStyle(dom); + if (cstyle.font) { + if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" }; + if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" }; + if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" }; + if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" }; + if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" }; + if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" }; + } + } + }], + toDOM: (node) => node.attrs.family ? ['span', { style: `font-family: "${node.attrs.family}";` }] : ['span', 0] + }, // :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text. pFontColor: { - attrs: { - color: { default: "#000" } - }, + attrs: { color: { default: "" } }, inclusive: true, parseDOM: [{ tag: "span", getAttrs(dom: any) { return { color: dom.getAttribute("color") }; } }], - toDOM(node: any) { - return node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0]; - } + toDOM: (node) => node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0] }, marker: { @@ -277,38 +301,4 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [{ tag: "code" }], toDOM() { return codeDOM; } }, - - /* FONTS */ - pFontFamily: { - attrs: { - family: { default: "Crimson Text" }, - }, - parseDOM: [{ - tag: "span", getAttrs(dom: any) { - const cstyle = getComputedStyle(dom); - if (cstyle.font) { - if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" }; - if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" }; - if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" }; - if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" }; - if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" }; - if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" }; - } - } - }], - toDOM: (node) => ['span', { - style: `font-family: "${node.attrs.family}";` - }] - }, - - /** FONT SIZES */ - pFontSize: { - attrs: { - fontSize: { default: 10 } - }, - parseDOM: [{ style: 'font-size: 10px;' }], - toDOM: (node) => ['span', { - style: `font-size: ${node.attrs.fontSize}px;` - }] - }, }; diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index af39ef9c7..33ef67ff5 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -218,48 +218,85 @@ export const nodes: { [index: string]: NodeSpec } = { group: 'block', attrs: { bulletStyle: { default: 0 }, - mapStyle: { default: "decimal" }, - setFontSize: { default: undefined }, - setFontFamily: { default: "inherit" }, - setFontColor: { default: "inherit" }, - inheritedFontSize: { default: undefined }, + mapStyle: { default: "decimal" },// "decimal", "multi", "bullet" + fontColor: { default: "inherit" }, + fontSize: { default: undefined }, + fontFamily: { default: undefined }, visibility: { default: true }, indent: { default: undefined } }, + parseDOM: [ + { + tag: "ul", getAttrs(dom: any) { + return { + bulletStyle: dom.getAttribute("data-bulletStyle"), + mapStyle: dom.getAttribute("data-mapStyle"), + fontColor: dom.style.color, + fontSize: dom.style["font-size"], + fontFamily: dom.style["font-family"], + indent: dom.style["margin-left"] + }; + } + }, + { + style: 'list-style-type=disc', getAttrs(dom: any) { + return { mapStyle: "bullet" }; + } + }, + { + tag: "ol", getAttrs(dom: any) { + return { + bulletStyle: dom.getAttribute("data-bulletStyle"), + mapStyle: dom.getAttribute("data-mapStyle"), + fontColor: dom.style.color, + fontSize: dom.style["font-size"], + fontFamily: dom.style["font-family"], + indent: dom.style["margin-left"] + }; + } + }], toDOM(node: Node<any>) { - if (node.attrs.mapStyle === "bullet") return ['ul', 0]; const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ""; - const fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize; - const ffam = node.attrs.setFontFamily; - const color = node.attrs.setFontColor; + const fsize = node.attrs.fontSize ? `font-size: ${node.attrs.fontSize};` : ""; + const ffam = node.attrs.fontFamily ? `font-family:${node.attrs.fontFamily};` : ""; + const fcol = node.attrs.fontColor ? `color: ${node.attrs.fontColor};` : ""; + const marg = node.attrs.indent ? `margin-left: ${node.attrs.indent};` : ""; + if (node.attrs.mapStyle === "bullet") { + return ['ul', { + "data-mapStyle": node.attrs.mapStyle, + "data-bulletStyle": node.attrs.bulletStyle, + style: `${fsize} ${ffam} ${fcol} ${marg}` + }, 0]; + } return node.attrs.visibility ? - ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}; color:${color}; margin-left: ${node.attrs.indent}` }, 0] : + ['ol', { + class: `${map}-ol`, + "data-mapStyle": node.attrs.mapStyle, + "data-bulletStyle": node.attrs.bulletStyle, + style: `list-style: none; ${fsize} ${ffam} ${fcol} ${marg}` + }, 0] : ['ol', { class: `${map}-ol`, style: `list-style: none;` }]; } }, - bullet_list: { - ...bulletList, - content: 'list_item+', - group: 'block', - // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }], - toDOM(node: Node<any>) { - return ['ul', 0]; - } - }, - list_item: { + ...listItem, attrs: { bulletStyle: { default: 0 }, - mapStyle: { default: "decimal" }, + mapStyle: { default: "decimal" }, // "decimal", "multi", "bullet" visibility: { default: true } }, - ...listItem, content: 'paragraph block*', + parseDOM: [{ + tag: "li", getAttrs(dom: any) { + return { mapStyle: dom.getAttribute("data-mapStyle"), bulletStyle: dom.getAttribute("data-bulletStyle") }; + } + }], toDOM(node: any) { const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ""; - return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."]; - //return ["li", { class: `${map}` }, 0]; + return node.attrs.visibility ? + ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, 0] : + ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, "..."]; } }, };
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index ff328068b..a641bce67 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -31,6 +31,7 @@ export default class PDFMenu extends AntimodeMenu { super(props); PDFMenu.Instance = this; + PDFMenu.Instance._canFade = false; } pointerDown = (e: React.PointerEvent) => { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 57b7bceca..b39b67cb3 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -51,6 +51,7 @@ interface IViewerProps { fieldKey: string; Document: Doc; DataDoc?: Doc; + docFilters: () => string[]; ContainingCollectionView: Opt<CollectionView>; PanelWidth: () => number; PanelHeight: () => number; @@ -110,6 +111,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu private _downX: number = 0; private _downY: number = 0; private _coverPath: any; + private _viewerIsSetup = false; @computed get allAnnotations() { return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]).filter( @@ -159,7 +161,14 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu }, { fireImmediately: true }); this._selectionReactionDisposer = reaction(() => this.props.isSelected(), - () => (SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(), + selected => { + if (!selected) { + this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); + PDFMenu.Instance.fadeOut(true); + } + (SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(); + }, { fireImmediately: true }); this._reactionDisposer = reaction( () => this.Document._scrollY, @@ -215,8 +224,8 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action setupPdfJsViewer = async () => { - this._selectionReactionDisposer && this._selectionReactionDisposer(); - this._selectionReactionDisposer = undefined; + if (this._viewerIsSetup) return; + else this._viewerIsSetup = true; this._showWaiting = true; this.props.setPdfViewer(this); await this.initialLoad(); diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index 793d4068f..6fd3455b6 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -194,6 +194,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc whenActiveChanged={returnFalse} bringToFront={returnFalse} opacity={returnOne} + docFilters={this.props.docFilters} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} ContentScaling={returnOne} diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 24d6e9d6f..74262d81a 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -7,7 +7,7 @@ import { observer } from "mobx-react"; import { Doc } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue, returnOne, returnZero } from "../../../Utils"; +import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue, returnOne, returnZero, returnEmptyString, returnEmptyFilter } from "../../../Utils"; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, SetupDrag } from "../../util/DragManager"; @@ -164,6 +164,7 @@ export class SearchItem extends React.Component<SearchItemProps> { removeDocument={returnFalse} addDocTab={returnFalse} pinToPres={returnFalse} + docFilters={returnEmptyFilter} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} ScreenToLocalTransform={Transform.Identity} diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 96587af44..8c8720179 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -134,7 +134,7 @@ export class Doc extends RefField { has: (target, key) => target[AclSym] !== AclPrivate && key in target.__fields, ownKeys: target => { const obj = {} as any; - (target[AclSym] !== AclPrivate) && Object.assign(obj, target.___fields); + if (target[AclSym] !== AclPrivate) Object.assign(obj, target.___fields); runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__); return Object.keys(obj); }, diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index 66959882d..7c7bf3e12 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -256,7 +256,7 @@ export namespace RichTextUtils { }; const list = (schema: any, items: Node[]): Node => { - return schema.node("bullet_list", null, items); + return schema.node("ordered_list", { mapStyle: "bullet" }, items); }; const paragraphNode = (schema: any, runs: docs_v1.Schema$TextRun[]): Node => { diff --git a/src/fields/util.ts b/src/fields/util.ts index 54e7eca28..ad7b6ea7a 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -101,12 +101,16 @@ export function makeReadOnly() { export function makeEditable() { _setter = _setterImpl; } +var _overrideAcl = false; +export function OVERRIDE_ACL(val: boolean) { + _overrideAcl = val; +} const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox", "LODdisable", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"]; export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; - if (target[AclSym]) return true; + if (target[AclSym] && !_overrideAcl) return true; if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { console.log(prop + " is deprecated - switch to _" + prop); @@ -125,8 +129,8 @@ export function setter(target: any, in_prop: string | symbol | number, value: an export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { let prop = in_prop; - if (in_prop === AclSym) return target[AclSym]; - if (target[AclSym] === AclPrivate) return undefined; + if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym]; + if (target[AclSym] === AclPrivate && !_overrideAcl) return undefined; if (prop === LayoutSym) { return target.__LAYOUT__; } diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index ceda74da2..a1719c015 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -9,13 +9,12 @@ import { faThumbtack, faTree, faTv, faBook, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faHome, faLongArrowAltLeft, faBars, faTh, faChevronLeft } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from 'mobx'; +import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as rp from 'request-promise'; import { Doc, DocListCast } from '../fields/Doc'; -import { FieldValue, Cast } from '../fields/Types'; import { CurrentUserUtils } from '../client/util/CurrentUserUtils'; -import { emptyPath, emptyFunction, returnFalse, returnOne, returnTrue, returnZero, Utils } from '../Utils'; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter } from '../Utils'; import { DocServer } from '../client/DocServer'; import { Docs, DocumentOptions } from '../client/documents/Documents'; import { Scripting } from '../client/util/Scripting'; @@ -37,10 +36,13 @@ import GestureOverlay from "../client/views/GestureOverlay"; import { ScriptField } from "../fields/ScriptField"; import InkOptionsMenu from "../client/views/collections/collectionFreeForm/InkOptionsMenu"; import { RadialMenu } from "../client/views/nodes/RadialMenu"; -import { UndoManager } from "../client/util/UndoManager"; +import { UndoManager, undoBatch } from "../client/util/UndoManager"; import { MainView } from "../client/views/MainView"; import { List } from "../fields/List"; import { AudioUpload } from "./AudioUpload"; +import { Cast, FieldValue } from '../fields/Types'; +import { CollectionView } from '../client/views/collections/CollectionView'; +import { InkingStroke } from '../client/views/InkingStroke'; library.add(faTasks, faFolderOpen, faAngleDoubleLeft, faExternalLinkSquareAlt, faMobile, faThLarge, faWindowClose, faEdit, faTrashAlt, faPalette, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus, faTerminal, faToggleOn, fileSolid, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard, @@ -215,6 +217,7 @@ export class MobileInterface extends React.Component { parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} + docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> @@ -236,6 +239,7 @@ export class MobileInterface extends React.Component { * Navigates to the given doc and updates the sidebar. * @param doc: doc for which the method is called */ + @undoBatch handleClick = async (doc: Doc) => { const children = DocListCast(doc.data); if (doc.type !== "collection" && this.sidebarActive) this.openFromSidebar(doc); @@ -508,11 +512,12 @@ export class MobileInterface extends React.Component { } } + undo = () => { if (this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc && this._activeDoc.title !== "WORKSPACES") { return (<> <div className="docButton" - style={{ backgroundColor: "black", color: "white" }} + style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanUndo() ? "1" : "0.4", }} id="undoButton" title="undo" onClick={(e: React.MouseEvent) => { @@ -529,7 +534,7 @@ export class MobileInterface extends React.Component { if (this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc && this._activeDoc.title !== "WORKSPACES") { return (<> <div className="docButton" - style={{ backgroundColor: "black", color: "white" }} + style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanRedo() ? "1" : "0.4", }} id="undoButton" title="redo" onClick={(e: React.MouseEvent) => { @@ -831,8 +836,10 @@ export class MobileInterface extends React.Component { -Scripting.addGlobal(function switchMobileView(doc: (userDoc: Doc) => Doc, renderView?: () => JSX.Element, onSwitch?: () => void) { return MobileInterface.Instance.switchCurrentView(doc, renderView, onSwitch); }); -Scripting.addGlobal(function openMobilePresentation() { return MobileInterface.Instance.setupDefaultPresentation(); }); +Scripting.addGlobal(function switchMobileView(doc: (userDoc: Doc) => Doc, renderView?: () => JSX.Element, onSwitch?: () => void) { return MobileInterface.Instance.switchCurrentView(doc, renderView, onSwitch); }, + "changes the active document displayed on the mobile, (doc: any)"); +Scripting.addGlobal(function openMobilePresentation() { return MobileInterface.Instance.setupDefaultPresentation(); }, + "opens the presentation on mobile"); Scripting.addGlobal(function toggleMobileSidebar() { return MobileInterface.Instance.toggleSidebar(); }); Scripting.addGlobal(function openMobileAudio() { return MobileInterface.Instance.toggleAudio(); }); Scripting.addGlobal(function openMobileSettings() { return SettingsManager.Instance.open(); }); |