From 3fe0e0e02a6c9bd717b3df9b000c13d1131f6eb4 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 23 May 2022 12:28:31 -0400 Subject: cleaning up singleLine text box api and TreeView types --- .../formattedText/ProsemirrorExampleTransfer.ts | 62 ++++++---------------- 1 file changed, 15 insertions(+), 47 deletions(-) (limited to 'src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts') diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index c76eda859..0dd0a8411 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -1,20 +1,15 @@ -import { chainCommands, exitCode, joinDown, joinUp, lift, deleteSelection, joinBackward, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn, newlineInCode } from "prosemirror-commands"; -import { liftTarget } from "prosemirror-transform"; +import { chainCommands, deleteSelection, exitCode, joinBackward, joinDown, joinUp, lift, newlineInCode, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from "prosemirror-commands"; import { redo, undo } from "prosemirror-history"; import { Schema } from "prosemirror-model"; -import { liftListItem, sinkListItem } from "./prosemirrorPatches.js"; -import { splitListItem, wrapInList, } from "prosemirror-schema-list"; -import { EditorState, Transaction, TextSelection } from "prosemirror-state"; -import { SelectionManager } from "../../../util/SelectionManager"; -import { NumCast, BoolCast, Cast, StrCast } from "../../../../fields/Types"; -import { Doc, DataSym, DocListCast, AclAugment, AclSelfEdit } from "../../../../fields/Doc"; -import { FormattedTextBox } from "./FormattedTextBox"; -import { Id } from "../../../../fields/FieldSymbols"; -import { Docs } from "../../../documents/Documents"; -import { Utils } from "../../../../Utils"; -import { listSpec } from "../../../../fields/Schema"; -import { List } from "../../../../fields/List"; +import { splitListItem, wrapInList } from "prosemirror-schema-list"; +import { EditorState, TextSelection, Transaction } from "prosemirror-state"; +import { liftTarget } from "prosemirror-transform"; +import { AclAugment, AclSelfEdit, Doc } from "../../../../fields/Doc"; import { GetEffectiveAcl } from "../../../../fields/util"; +import { Utils } from "../../../../Utils"; +import { Docs } from "../../../documents/Documents"; +import { SelectionManager } from "../../../util/SelectionManager"; +import { liftListItem, sinkListItem } from "./prosemirrorPatches.js"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -48,29 +43,6 @@ export function buildKeymap>(schema: S, props: any, mapKey keys[key] = cmd; } - /// bcz; Argh!! replace with an onEnter func that conditionally handles Enter - const addTextBox = (below: boolean, force?: boolean) => { - if (props.Document.treeViewType === "outline") return true; // bcz: Arghh .. need to determine if this is an treeViewOutlineBox in which case Enter's are ignored.. - const layoutDoc = props.Document; - const originalDoc = layoutDoc.rootDocument || layoutDoc; - if (force || props.Document._singleLine) { - const layoutKey = StrCast(originalDoc.layoutKey); - const newDoc = Doc.MakeCopy(originalDoc, true); - const dataField = originalDoc[Doc.LayoutFieldKey(newDoc)]; - newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List([]) : undefined; - if (below) newDoc.y = NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10; - else newDoc.x = NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10; - if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { - newDoc[layoutKey] = originalDoc[layoutKey]; - } - Doc.GetProto(newDoc).text = undefined; - FormattedTextBox.SelectOnLoad = newDoc[Id]; - props.addDocument(newDoc); - return true; - } - return false; - }; - const canEdit = (state: any) => { switch (GetEffectiveAcl(props.Document)) { case AclAugment: return false; @@ -109,11 +81,7 @@ export function buildKeymap>(schema: S, props: any, mapKey bind("Ctrl-i", (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state as any, dispatch as any)); bind("Tab", (state: EditorState, dispatch: (tx: Transaction) => void) => { - /// bcz; Argh!! replace layotuTEmpalteString with a onTab prop conditionally handles Tab); - if (props.Document._singleLine) { - if (!props.LayoutTemplateString) return addTextBox(false, true); - return true; - } + if (props.onKey?.({ key: "Tab" }, props)) return true; if (!canEdit(state)) return true; const ref = state.selection; const range = ref.$from.blockRange(ref.$to); @@ -138,8 +106,7 @@ export function buildKeymap>(schema: S, props: any, mapKey }); bind("Shift-Tab", (state: EditorState, dispatch: (tx: Transaction) => void) => { - /// bcz; Argh!! replace with a onShiftTab prop conditionally handles Tab); - if (props.Document._singleLine) return true; + if (props.onKey?.({ key: "Tab", shiftKey: true })) return true; // single line docs don't process tabs so that their containers can decide what to do. This should be a prop if (!canEdit(state)) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); @@ -188,13 +155,14 @@ export function buildKeymap>(schema: S, props: any, mapKey }; //Command to create a text document to the right of the selected textbox - bind("Alt-Enter", () => addTextBox(false, true)); + bind("Alt-Enter", () => true); //Command to create a text document to the bottom of the selected textbox - bind("Ctrl-Enter", () => addTextBox(true, true)); + bind("Ctrl-Enter", () => true); // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); bind("Backspace", (state: EditorState, dispatch: (tx: Transaction>) => void) => { + if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; if (!deleteSelection(state, (tx: Transaction) => { @@ -216,8 +184,8 @@ export function buildKeymap>(schema: S, props: any, mapKey //newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock //command to break line bind("Enter", (state: EditorState, dispatch: (tx: Transaction>) => void) => { - if (addTextBox(true, false)) return true; + if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; const trange = state.selection.$from.blockRange(state.selection.$to); -- cgit v1.2.3-70-g09d2 From a01cd55030f549b1c4c207d23731a00e689989c3 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 25 May 2022 15:18:46 -0400 Subject: variety of small fixes for text / sharing. made typed text default to 35 height so that when shared it doesn't start big and jump to small. changed permissions to be private until an Acl is set - this prevents private docs from flicking on momentarily when shared since fields are not distributed atomically. added Shift/Alt/Ctrl Enter for freeform and stacking views to create neighboring docs. fixed first typed char of text to have a user_mark. made shared text doc header overlap to prevent scrolling. --- src/client/DocServer.ts | 60 ++++++++++------------ src/client/documents/Documents.ts | 2 +- src/client/util/CurrentUserUtils.ts | 2 +- src/client/views/MainView.tsx | 4 +- src/client/views/StyleProvider.tsx | 5 +- .../views/collections/CollectionStackingView.tsx | 22 ++++++++ .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 16 +++--- .../formattedText/ProsemirrorExampleTransfer.ts | 14 +++-- src/fields/util.ts | 7 ++- 10 files changed, 77 insertions(+), 57 deletions(-) (limited to 'src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index e498a7cca..dbc4783d8 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -364,40 +364,36 @@ export namespace DocServer { const proms: Promise[] = []; runInAction(() => { for (const field of fields) { - if (field !== undefined && field !== null && !_cache[field.id]) { + const cached = _cache[field.id]; + if (!cached) { // deserialize - const cached = _cache[field.id]; - if (!cached) { - const prom = SerializationHelper.Deserialize(field).then(deserialized => { - fieldMap[field.id] = deserialized; - - //overwrite or delete any promises (that we inserted as flags - // to indicate that the field was in the process of being fetched). Now everything - // should be an actual value within or entirely absent from the cache. - if (deserialized !== undefined) { - _cache[field.id] = deserialized; - } else { - delete _cache[field.id]; - } - return deserialized; - }); - // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache) - // we set the value at the field's id to a promise that will resolve to the field. - // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method). - // The mapping in the .then call ensures that when other callers await these promises, they'll - // get the resolved field - _cache[field.id] = prom; - - // adds to a list of promises that will be awaited asynchronously - proms.push(prom); - } else if (cached instanceof Promise) { - proms.push(cached as any); - } - } else if (_cache[field.id] instanceof Promise) { - proms.push(_cache[field.id] as any); - (_cache[field.id] as any).then((f: any) => fieldMap[field.id] = f); + const prom = SerializationHelper.Deserialize(field).then(deserialized => { + fieldMap[field.id] = deserialized; + + //overwrite or delete any promises (that we inserted as flags + // to indicate that the field was in the process of being fetched). Now everything + // should be an actual value within or entirely absent from the cache. + if (deserialized !== undefined) { + _cache[field.id] = deserialized; + } else { + delete _cache[field.id]; + } + return deserialized; + }); + // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache) + // we set the value at the field's id to a promise that will resolve to the field. + // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method). + // The mapping in the .then call ensures that when other callers await these promises, they'll + // get the resolved field + _cache[field.id] = prom; + + // adds to a list of promises that will be awaited asynchronously + proms.push(prom); + } else if (cached instanceof Promise) { + proms.push(cached as any); + cached.then((f: any) => fieldMap[field.id] = f); } else if (field) { - proms.push(_cache[field.id] as any); + proms.push(cached as any); fieldMap[field.id] = DocServer.GetCachedRefField(field.id) || field; } } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index c3b6953b5..ed37c48dc 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -369,7 +369,7 @@ export namespace Docs { [DocumentType.RTF, { layout: { view: FormattedTextBox, dataField: "text" }, options: { - _height: 150, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, treeViewGrowsHorizontally: true, + _height: 35, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, treeViewGrowsHorizontally: true, links: "@links(self)" } }], diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index e4dc175bd..436237d6c 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1184,7 +1184,7 @@ export class CurrentUserUtils { public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, maxHeight?: number, backgroundColor?: string) { const tbox = Docs.Create.TextDocument("", { _xMargin: noMargins ? 0 : undefined, _yMargin: noMargins ? 0 : undefined, annotationOn, docMaxAutoHeight: maxHeight, backgroundColor: backgroundColor, - _width: width || 200, _height: height || 100, x: x, y: y, _fitWidth: true, _autoHeight: true, title + _width: width || 200, _height: 35, x: x, y: y, _fitWidth: true, _autoHeight: true, title }); const template = Doc.UserDoc().defaultTextLayout; if (template instanceof Doc) { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 183efc944..ff2857739 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -120,8 +120,8 @@ export class MainView extends React.Component { } this._sidebarContent.proto = undefined; if (!MainView.Live) { - DocServer.setPlaygroundFields(["dataTransition", "treeViewOpen", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition", - "panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "text-scrollHeight", "text-height", "hideMinimap", + DocServer.setPlaygroundFields(["dataTransition", "treeViewOpen", "showSidebar", "sidebarWidthPercent", "viewTransition", + "panX", "panY", "nativeWidth", "nativeHeight", "text-scrollHeight", "text-height", "hideMinimap", "viewScale", "scrollTop", "hidden", "curPage", "viewType", "chromeHidden", "nativeWidth"]); // can play with these fields on someone else's } DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg))); diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index c44443264..553f84a67 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -70,6 +70,7 @@ export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) { // a preliminary implementation of a dash style sheet for setting rendering properties of documents nested within a Tab // export function DefaultStyleProvider(doc: Opt, props: Opt, property: string): any { + const remoteDocHeader = "author;creationDate;noMargin"; const docProps = testDocProps(props) ? props : undefined; const selected = property.includes(":selected"); const isCaption = property.includes(":caption"); @@ -111,7 +112,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt = StrCast(doc?.[fieldKey + "color"], StrCast(doc?._color)); @@ -126,7 +127,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt (props?.PanelHeight() || 0) ? 5 : 10) : 0; case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) || - doc?.type === DocumentType.RTF || doc?.type === DocumentType.LABEL) && showTitle() && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0; + (doc?.type === DocumentType.RTF && !showTitle()?.includes("noMargin")) || doc?.type === DocumentType.LABEL) && showTitle() && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0; case StyleProp.BackgroundColor: { if (MainView.Instance.LastButton === doc) return Colors.LIGHT_GRAY; let docColor: Opt = diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 4630b3bf2..dddae4a34 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -28,6 +28,8 @@ import "./CollectionStackingView.scss"; import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionViewType } from "./CollectionView"; +import { FieldViewProps } from "../nodes/FieldView"; +import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; const _global = (window /* browser */ || global /* node */) as any; @@ -207,6 +209,25 @@ export class CollectionStackingView extends CollectionSubView { + const docView = fieldProps.DocumentView?.(); + if (docView && ["Enter"].includes(e.key) && e.ctrlKey) { + e.stopPropagation?.(); + const below = !e.altKey && e.key !== "Tab"; + const layoutKey = StrCast(docView.LayoutFieldKey); + const newDoc = Doc.MakeCopy(docView.rootDoc, true); + const dataField = docView.rootDoc[Doc.LayoutFieldKey(newDoc)]; + newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List([]) : undefined; + if (layoutKey !== "layout" && docView.rootDoc[layoutKey] instanceof Doc) { + newDoc[layoutKey] = docView.rootDoc[layoutKey]; + } + Doc.GetProto(newDoc).text = undefined; + FormattedTextBox.SelectOnLoad = newDoc[Id]; + return this.addDocument?.(newDoc); + } + } isContentActive = () => this.props.isSelected() || this.props.isContentActive(); getDisplayDoc(doc: Doc, width: () => number) { const dataDoc = (!doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS) ? undefined : this.props.DataDoc; @@ -225,6 +246,7 @@ export class CollectionStackingView extends CollectionSubView { const docView = fieldProps.DocumentView?.(); - if (docView && docView.rootDoc._singleLine && ["Tab", "Enter"].includes(e.key)) { + if (docView && (e.ctrlKey || e.shiftKey || e.altKey || docView.rootDoc._singleLine) && ["Tab", "Enter"].includes(e.key)) { e.stopPropagation?.(); const below = !e.altKey && e.key !== "Tab"; const layoutKey = StrCast(docView.LayoutFieldKey); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 822bc996b..d468822c0 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -245,8 +245,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const state = this._editorView.state.apply(tx); this._editorView.updateState(state); - const tsel = this._editorView.state.selection.$from; - //tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000))); const curText = state.doc.textBetween(0, state.doc.content.size, " \n"); const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.props.fieldKey], RichTextField) : undefined; // the actual text in the text box const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype @@ -348,7 +346,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } autoLink = () => { - if (this._editorView) { + if (this._editorView?.state.doc.textContent) { const newAutoLinks = new Set(); const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords); const f = this._editorView.state.selection.from; @@ -962,7 +960,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }, { fireImmediately: true } ); quickScroll = undefined; - setTimeout(this.tryUpdateScrollHeight, 10); + this.tryUpdateScrollHeight(); } pushToGoogleDoc = async () => { @@ -1193,19 +1191,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())); if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { + const selLoadChar = FormattedTextBox.SelectOnLoadChar; FormattedTextBox.SelectOnLoad = ""; this.props.select(false); - if (FormattedTextBox.SelectOnLoadChar && this._editorView) { + if (selLoadChar && this._editorView) { const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; const tr = this._editorView.state.tr.setStoredMarks(storedMarks).insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size).setStoredMarks(storedMarks); this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); - FormattedTextBox.SelectOnLoadChar = ""; - } else if (curText && !FormattedTextBox.DontSelectInitialText) { - selectAll(this._editorView!.state, this._editorView?.dispatch); + } else if (this._editorView && curText && !FormattedTextBox.DontSelectInitialText) { + selectAll(this._editorView.state, this._editorView?.dispatch) this.startUndoTypingBatch(); + } else if (this._editorView) { + this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); } FormattedTextBox.DontSelectInitialText = false; } diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 0dd0a8411..e979ae59e 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -80,8 +80,10 @@ export function buildKeymap>(schema: S, props: any, mapKey //Commands for lists bind("Ctrl-i", (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state as any, dispatch as any)); + bind("Ctrl-Tab", () => props.onKey?.(event, props) ? true : true); + bind("Alt-Tab", () => props.onKey?.(event, props) ? true : true); bind("Tab", (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (props.onKey?.({ key: "Tab" }, props)) return true; + if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; const ref = state.selection; const range = ref.$from.blockRange(ref.$to); @@ -106,7 +108,7 @@ export function buildKeymap>(schema: S, props: any, mapKey }); bind("Shift-Tab", (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (props.onKey?.({ key: "Tab", shiftKey: true })) return true; // single line docs don't process tabs so that their containers can decide what to do. This should be a prop + if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); @@ -154,11 +156,9 @@ export function buildKeymap>(schema: S, props: any, mapKey return tx; }; - //Command to create a text document to the right of the selected textbox - bind("Alt-Enter", () => true); - //Command to create a text document to the bottom of the selected textbox - bind("Ctrl-Enter", () => true); + bind("Alt-Enter", () => props.onKey?.(event, props) ? true : true); + bind("Ctrl-Enter", () => props.onKey?.(event, props) ? true : true); // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); bind("Backspace", (state: EditorState, dispatch: (tx: Transaction>) => void) => { @@ -244,8 +244,6 @@ export function buildKeymap>(schema: S, props: any, mapKey return false; }); - // mac && bind("Ctrl-Enter", cmd); - // bind("Mod-Enter", cmd); bind("Shift-Enter", cmd); return keys; diff --git a/src/fields/util.ts b/src/fields/util.ts index c708affe3..ef5ac79b8 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -175,8 +175,11 @@ const getEffectiveAclCache = computedFn(function (target: any, user?: string) { * Calculates the effective access right to a document for the current user. */ export function GetEffectiveAcl(target: any, user?: string): symbol { - return !target ? AclPrivate : - target[UpdatingFromServer] ? AclAdmin : getEffectiveAclCache(target, user);// all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) + if (!target) return AclPrivate; + if (target[UpdatingFromServer]) return AclAdmin; + // authored documents are private until an ACL is set. this also fixes notes that flicker on and off when a remote types to create a private note into a shared collection. + if (!target[AclSym] && target.author && target.author !== Doc.CurrentUserEmail) return AclPrivate; + return getEffectiveAclCache(target, user);// all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) } function getPropAcl(target: any, prop: string | symbol | number) { -- cgit v1.2.3-70-g09d2 From 53cae5e2ab9267295a824ff721c119ada5e5dc20 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 31 May 2022 13:03:19 -0400 Subject: added a pop up menu for viewing dashField view collection. added a menu button for turning off autoLinking to text selection. added ability to add button to top contextMenu buttons without blowing away db. --- src/client/util/CurrentUserUtils.ts | 34 +++++++++ src/client/views/MainView.tsx | 2 + .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/button/FontIconBox.tsx | 8 +++ .../views/nodes/formattedText/DashFieldView.tsx | 81 +++++++++++++++++----- .../views/nodes/formattedText/FormattedTextBox.tsx | 12 ++-- .../formattedText/ProsemirrorExampleTransfer.ts | 2 + .../views/nodes/formattedText/RichTextMenu.tsx | 14 +++- .../views/nodes/formattedText/RichTextRules.ts | 2 +- src/client/views/nodes/formattedText/marks_rts.ts | 14 ++++ 10 files changed, 144 insertions(+), 27 deletions(-) (limited to 'src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 083530ce7..c4e2afd08 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -745,6 +745,7 @@ export class CurrentUserUtils { { title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", click: 'setAlignment("left", _readOnly_)' }, { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", click: 'setAlignment("center", _readOnly_)' }, { title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", click: 'setAlignment("right", _readOnly_)' }, + { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", click: 'toggleNoAutoLinkAnchor(_readOnly_)' }, ]; return tools; } @@ -834,11 +835,44 @@ export class CurrentUserUtils { !params.subMenu ? btnFunc(params) : CurrentUserUtils.linearButtonList({ + title: params.title, linearViewSubMenu: true, flexGap: 0, ignoreClick: true, linearViewExpandable: true, icon: params.title, _height: 30, linearViewIsExpanded: params.expanded ? !(ComputedField.MakeFunction(params.expanded) as any) : undefined, hidden: params.hidden ? ComputedField.MakeFunction(params.hidden) as any : undefined, }, params.subMenu.map(btnFunc)))); + } else { + const menuBtnList = DocListCast((doc.contextMenuBtns as Doc).data); + let prev = ""; + CurrentUserUtils.contextMenuTools(doc).forEach(params => { + const menuBtnDoc = menuBtnList.find(doc => doc.title === params.title); + if (!menuBtnDoc) { + const newMenuBtnDoc = !params.subMenu ? + btnFunc(params) : + CurrentUserUtils.linearButtonList({ + title: params.title, + linearViewSubMenu: true, flexGap: 0, ignoreClick: true, + linearViewExpandable: true, icon: params.title, _height: 30, + linearViewIsExpanded: params.expanded ? !(ComputedField.MakeFunction(params.expanded) as any) : undefined, + hidden: params.hidden ? ComputedField.MakeFunction(params.hidden) as any : undefined, + }, params.subMenu.map(btnFunc)); + const after = menuBtnList.find(doc => doc.title === prev); + Doc.AddDocToList(doc.contextMenuBtns as Doc, "data", newMenuBtnDoc, after, false, !after); + } + const subMenuBtnList = menuBtnDoc?.data ? DocListCast(menuBtnDoc.data) : undefined; + if (menuBtnDoc && subMenuBtnList && params.subMenu && DocListCast(doc.data).length !== subMenuBtnList.length) { + let prevSub = ""; + params.subMenu.forEach(sub => { + if (!subMenuBtnList.find(doc => doc.title === sub.title)) { + const newSubMenuBtnDoc = btnFunc(sub); + const after = subMenuBtnList.find(doc => doc.title === prevSub); + Doc.AddDocToList(menuBtnDoc, "data", newSubMenuBtnDoc, after, false, !prevSub); + } + prevSub = params.title; + }) + } + prev = params.title; + }); } } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ff2857739..55190001b 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -50,6 +50,7 @@ import { AudioBox } from './nodes/AudioBox'; import { ButtonType } from './nodes/button/FontIconBox'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView } from './nodes/DocumentView'; +import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from './nodes/formattedText/RichTextMenu'; import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; @@ -686,6 +687,7 @@ export class MainView extends React.Component { + diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f4cb08f8f..259cc1ee5 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1166,7 +1166,7 @@ export class CollectionFreeFormView extends CollectionSubView { const docView = fieldProps.DocumentView?.(); - if (docView && (e.ctrlKey || e.shiftKey || e.altKey || docView.rootDoc._singleLine) && ["Tab", "Enter"].includes(e.key)) { + if (docView && (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || docView.rootDoc._singleLine) && ["Tab", "Enter"].includes(e.key)) { e.stopPropagation?.(); const below = !e.altKey && e.key !== "Tab"; const layoutKey = StrCast(docView.LayoutFieldKey); diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index e6d8fe88d..625f5d14a 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -666,6 +666,14 @@ ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: b if (editorView) RichTextMenu.Instance.setFontSize(size); else Doc.UserDoc()._fontSize = size; }); +ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; + if (checkResult) { + return (editorView ? RichTextMenu.Instance.noAutoLink : Doc.UserDoc().noAutoLink) ? Colors.MEDIUM_BLUE : "transparent"; + } + if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor(); + else Doc.UserDoc().noAutoLink = Doc.UserDoc().noAutoLink ? true : false; +}); ScriptingGlobals.add(function toggleBold(checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 1a8352b72..6a3f9ed00 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -8,35 +8,41 @@ import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; import { ComputedField } from "../../../../fields/ScriptField"; import { Cast, StrCast } from "../../../../fields/Types"; import { DocServer } from "../../../DocServer"; -import { DocUtils } from "../../../documents/Documents"; import { CollectionViewType } from "../../collections/CollectionView"; import "./DashFieldView.scss"; import { FormattedTextBox } from "./FormattedTextBox"; import React = require("react"); +import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../../Utils"; +import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu"; +import { Tooltip } from "@material-ui/core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; export class DashFieldView { _fieldWrapper: HTMLDivElement; // container for label and value constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + const { boolVal, strVal } = DashFieldViewInternal.fieldContent(tbox.props.Document, tbox.rootDoc, node.attrs.fieldKey); + this._fieldWrapper = document.createElement("div"); this._fieldWrapper.style.width = node.attrs.width; this._fieldWrapper.style.height = node.attrs.height; this._fieldWrapper.style.fontWeight = "bold"; this._fieldWrapper.style.position = "relative"; this._fieldWrapper.style.display = "inline-block"; + this._fieldWrapper.textContent = node.attrs.fieldKey.startsWith("#") ? node.attrs.fieldKey : node.attrs.fieldKey + " " + strVal; this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); }; this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); }; this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); }; this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); }; - ReactDOM.render( ReactDOM.render(, this._fieldWrapper); + />, this._fieldWrapper)); (this as any).dom = this._fieldWrapper; } destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); } @@ -76,16 +82,17 @@ export class DashFieldViewInternal extends React.Component { r?.addEventListener("keydown", e => this.fieldSpanKeyDown(e, r)); r?.addEventListener("blur", e => r && this.updateText(r.textContent!, false)); - r?.addEventListener("pointerdown", action((e) => { - e.stopPropagation(); - })); + r?.addEventListener("pointerdown", action(e => e.stopPropagation())); }} > {strVal} ; @@ -161,7 +166,7 @@ export class DashFieldViewInternal extends React.Component 1 ? new List(splits) : newText; if (this._fieldKey.startsWith("_")) Doc.Layout(this._textBoxDoc)[this._fieldKey] = strVal; @@ -173,11 +178,7 @@ export class DashFieldViewInternal extends React.Component { - e.stopPropagation(); + createPivotForField = (e: React.MouseEvent) => { let container = this.props.tbox.props.ContainingCollectionView; while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) { container = container.props.ContainingCollectionView; @@ -196,6 +197,16 @@ export class DashFieldViewInternal extends React.Component { + setupMoveUpEvents(this, e, returnFalse, returnFalse, (e) => { + DashFieldViewMenu.createFieldView = this.createPivotForField; + DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16); + }); + } + render() { return
; } +} +@observer +export class DashFieldViewMenu extends AntimodeMenu { + static Instance: DashFieldViewMenu; + static createFieldView: (e: React.MouseEvent) => void = emptyFunction; + constructor(props: any) { + super(props); + DashFieldViewMenu.Instance = this; + } + @action + showFields = (e: React.MouseEvent) => { + DashFieldViewMenu.createFieldView(e); + DashFieldViewMenu.Instance.fadeOut(true); + } + + public show = (x: number, y: number) => { + this.jumpTo(x, y, true); + const hideMenu = () => { + this.fadeOut(true); + document.removeEventListener("pointerdown", hideMenu); + } + document.addEventListener("pointerdown", hideMenu) + } + render() { + const buttons = [ + {"Remove Link Anchor"}
}> + + , + ]; + + return this.getElement(buttons); + } } \ No newline at end of file diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index d468822c0..ce82821b6 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -391,16 +391,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp flattened1.forEach((flat, i) => { const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, autoLinkTerm); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; - alink = alink ?? (DocListCast(this.Document.links).find(link => - Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && - Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, - LinkManager.AutoKeywords)!); - newAutoLinks.add(alink); const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); const sel = flattened[i]; tr = tr.addMark(sel.from, sel.to, splitter); tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { - if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { + if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { + alink = alink ?? (DocListCast(this.Document.links).find(link => + Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && + Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, + LinkManager.AutoKeywords)!); + newAutoLinks.add(alink); const allAnchors = [{ href: Doc.localServerPath(target), title: "a link", anchorId: this.props.Document[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: "auto term", location: "add:right" }); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index e979ae59e..fb49b0698 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -82,6 +82,8 @@ export function buildKeymap>(schema: S, props: any, mapKey bind("Ctrl-Tab", () => props.onKey?.(event, props) ? true : true); bind("Alt-Tab", () => props.onKey?.(event, props) ? true : true); + bind("Meta-Tab", () => props.onKey?.(event, props) ? true : true); + bind("Meta-Enter", () => props.onKey?.(event, props) ? true : true); bind("Tab", (state: EditorState, dispatch: (tx: Transaction) => void) => { if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index c5f76cc8d..3df1e45a5 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -35,6 +35,7 @@ export class RichTextMenu extends AntimodeMenu { public _brushMap: Map> = new Map(); @observable private collapsed: boolean = false; + @observable private _noLinkActive: boolean = false; @observable private _boldActive: boolean = false; @observable private _italicsActive: boolean = false; @observable private _underlineActive: boolean = false; @@ -79,6 +80,7 @@ export class RichTextMenu extends AntimodeMenu { this._reaction?.(); } + @computed get noAutoLink() { return this._noLinkActive; } @computed get bold() { return this._boldActive; } @computed get underline() { return this._underlineActive; } @computed get italics() { return this._italicsActive; } @@ -220,7 +222,7 @@ export class RichTextMenu extends AntimodeMenu { let activeMarks: MarkType[] = []; if (!this.view || !this.TextView.props.isSelected(true)) return activeMarks; - const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; + const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type); //current selection const { empty, ranges, $to } = this.view.state.selection as TextSelection; @@ -264,6 +266,7 @@ export class RichTextMenu extends AntimodeMenu { setActiveMarkButtons(activeMarks: MarkType[] | undefined) { if (!activeMarks) return; + this._noLinkActive = false; this._boldActive = false; this._italicsActive = false; this._underlineActive = false; @@ -273,6 +276,7 @@ export class RichTextMenu extends AntimodeMenu { activeMarks.forEach(mark => { switch (mark.name) { + case "noAutoLinkAnchor": this._noLinkActive = true; break; case "strong": this._boldActive = true; break; case "em": this._italicsActive = true; break; case "underline": this._underlineActive = true; break; @@ -283,6 +287,14 @@ export class RichTextMenu extends AntimodeMenu { }); } + toggleNoAutoLinkAnchor = () => { + if (this.view) { + const mark = this.view.state.schema.mark(this.view.state.schema.marks.noAutoLinkAnchor); + this.setMark(mark, this.view.state, this.view.dispatch, false); + this.TextView.autoLink(); + this.view.focus(); + } + } toggleBold = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong); diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 427e05edb..8851d52e4 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -348,7 +348,7 @@ export class RichTextRules { this.Document[DataSym].tags = `${tags + "#" + tag + ':'}`; } const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" + tag }); - return state.tr.deleteRange(start, end).insert(start, fieldView); + return state.tr.deleteRange(start, end).insert(start, fieldView).insertText(" "); }), diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 1f6ce014f..2fde5c7ba 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -48,6 +48,20 @@ export const marks: { [index: string]: MarkSpec } = { return ["a", { class: anchorids, "data-targethrefs": targethrefs, "data-linkdoc": node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0]; } }, + noAutoLinkAnchor: { + attrs: {}, + inclusive: false, + parseDOM: [{ + tag: "div", getAttrs(dom: any) { + return { + noAutoLink: dom.getAttribute("data-noAutoLink"), + }; + } + }], + toDOM(node: any) { + return ["span", { "data-noAutoLink": "true" }, 0]; + } + }, // :: MarkSpec A linkAnchor. The anchor can have multiple links, where each linkAnchor specifies an href to the URL of the source selection Marker text, // and a title for use in menus and hover. `title` // defaults to the empty string. Rendered and parsed as an `` -- cgit v1.2.3-70-g09d2