aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/formattedText/FormattedTextBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx112
1 files changed, 59 insertions, 53 deletions
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index c51f6c38b..c8df6e50f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -98,7 +98,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
public static PasteOnLoad: ClipboardEvent | undefined;
public static SelectOnLoadChar = '';
- public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
+ public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch request when typing a new text note into a collection
+ private _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection
private static _nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor };
private _curHighlights = new ObservableSet<string>(['Audio Tags']);
@@ -270,13 +271,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
e.preventDefault();
e.stopPropagation();
const targetCreator = (annotationOn?: Doc) => {
- const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn);
+ const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
+ target.layout_fitWidth = true;
DocumentView.SetSelectOnLoad(target);
return target;
};
+ const sourceAnchorCreator = () => this.getAnchor(true);
+
const docView = this.DocumentView?.();
- docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY);
+ docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY);
});
AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => {
@@ -425,10 +429,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const newAutoLinks = new Set<Doc>();
const oldAutoLinks = Doc.Links(this.Document).filter(
link =>
- ((!Doc.isTemplateForField(this.Document) &&
- ((DocCast(link.link_anchor_1) && !Doc.isTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) &&
- ((DocCast(link.link_anchor_2) && !Doc.isTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) ||
- (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
+ ((!Doc.IsTemplateForField(this.Document) &&
+ ((DocCast(link.link_anchor_1) && !Doc.IsTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) &&
+ ((DocCast(link.link_anchor_2) && !Doc.IsTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) ||
+ (Doc.IsTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
link.link_relationship === LinkManager.AutoKeywords
); // prettier-ignore
if (this.EditorView?.state.doc.textContent) {
@@ -1070,15 +1074,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
getView = (doc: Doc, options: FocusViewOptions) => {
if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
- if (!this.SidebarShown) {
- this.toggleSidebar(false);
- options.didMove = true;
- }
- setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc));
+ return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options);
}
- return new Promise<Opt<DocumentView>>(res => {
- DocumentView.addViewRenderedCb(doc, dv => res(dv));
- });
+ return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
};
focus = (textAnchor: Doc, options: FocusViewOptions) => {
const focusSpeed = options.zoomTime ?? 500;
@@ -1145,13 +1143,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return undefined;
};
- // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc.
- // Since we also monitor all component height changes, this will update the document's height.
- resetNativeHeight = action((scrollHeight: number) => {
- this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight;
- if (!this.layoutDoc.isTemplateForField && NumCast(this.layoutDoc._nativeHeight)) this.layoutDoc._nativeHeight = scrollHeight;
- });
-
addPlugin = (plugin: Plugin) => {
const editorView = this.EditorView;
if (editorView) {
@@ -1164,7 +1155,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
@computed get tagsHeight() {
- return this.DocumentView?.().showTags ? Math.max(0, 20 - Math.max(this._props.yPadding ?? 0, NumCast(this.layoutDoc._yMargin))) * this.ScreenToLocalBoxXf().Scale : 0;
+ return this.DocumentView?.().showTags ? Math.max(0, 20 - Math.max(this._props.yMargin ?? 0, NumCast(this.layoutDoc._yMargin))) * this.ScreenToLocalBoxXf().Scale : 0;
}
@computed get contentScaling() {
@@ -1182,21 +1173,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this._disposers.width = reaction(this._props.PanelWidth, this.tryUpdateScrollHeight);
this._disposers.scrollHeight = reaction(
() => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }),
- ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && this.resetNativeHeight(scrollHeight),
+ ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && (this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight),
{ fireImmediately: true }
);
this._disposers.componentHeights = reaction(
// set the document height when one of the component heights changes and layout_autoHeight is on
- () => ({ border: this._props.PanelHeight(), sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
- ({ border, sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
+ () => ({
+ border: this._props.PanelHeight(),
+ scrollHeight: NumCast(this.layoutDoc['_' + this.fieldKey + '_height']),
+ sidebarHeight: this.sidebarHeight,
+ textHeight: this.textHeight,
+ layoutAutoHeight: this.layout_autoHeight,
+ marginsHeight: this.layout_autoHeightMargins,
+ }),
+ ({ border, sidebarHeight, scrollHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
if (
(!Array.from(this._curHighlights).includes('Bold Text') || this._props.isSelected()) && //
layoutAutoHeight &&
newHeight &&
- (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height)) &&
+ (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height) || this.layoutDoc._nativeHeight !== scrollHeight) &&
!this._props.dontRegisterView
) {
+ if (NumCast(this.layoutDoc.nativeHeight)) {
+ this.layoutDoc._nativeHeight = scrollHeight;
+ }
this._props.setHeight?.(newHeight);
}
},
@@ -1219,18 +1220,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)?.[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0;
const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData;
const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData;
- return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
+ return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? NumCast(whichData)?.toString() ?? StrCast(whichData)) };
},
incomingValue => {
if (this.EditorView && this.ApplyingChange !== this.fieldKey) {
if (incomingValue?.data) {
- const updatedState = JSON.parse(incomingValue.data.Data);
+ const updatedState = JSON.parse(incomingValue.data.Data.replace(/\n/g, ''));
if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) {
this.EditorView.updateState(EditorState.fromJSON(this.config, updatedState));
this.tryUpdateScrollHeight();
}
} else if (this.EditorView.state.doc.textContent !== (incomingValue?.str ?? '')) {
selectAll(this.EditorView.state, tx => this.EditorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
+ this.tryUpdateScrollHeight();
}
}
},
@@ -1524,7 +1526,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
const { $from } = this.EditorView.state.selection;
- const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
+ const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() });
const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
if (selLoadChar === 'Enter') {
@@ -1535,6 +1537,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
}
if (selectOnLoad) {
+ this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+ FormattedTextBox.LiveTextUndo = undefined;
this.EditorView!.focus();
}
if (this._props.isContentActive()) this.prepareForTyping();
@@ -1551,7 +1555,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const { text, paragraph } = schema.nodes;
const selNode = this.EditorView.state.selection.$anchor.node();
if (this.EditorView.state.selection.from === 1 && this.EditorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
- const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })];
+ const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() })];
this.EditorView.state.selection.empty && this.EditorView.state.selection.from === 1 && this.EditorView?.dispatch(this.EditorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor));
}
}
@@ -1564,8 +1568,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
removeStyleSheet(this._userStyleSheetElement);
Object.values(this._disposers).forEach(disposer => disposer?.());
this.endUndoTypingBatch();
- FormattedTextBox.LiveTextUndo?.end();
- FormattedTextBox.LiveTextUndo = undefined;
+ this._liveTextUndo?.end();
this.unhighlightSearchTerms();
this.EditorView?.destroy();
RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined);
@@ -1687,7 +1690,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (e.clientX > boundsRect.left && e.clientX < boundsRect.right && e.clientY > boundsRect.bottom) {
// if we clicked below the last prosemirror div, then set the selection to be the end of the document
editorView.focus();
- editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size)));
+ // editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size)));
}
} else if (node && [editorView.state.schema.nodes.ordered_list, editorView.state.schema.nodes.listItem].includes(node.type) && node !== (editorView.state.selection as NodeSelection)?.node && pcords) {
editorView.dispatch(editorView.state.tr.setSelection(NodeSelection.create(editorView.state.doc, pcords.pos)));
@@ -1750,23 +1753,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
* When a text box loses focus, it might be because a text button was clicked (eg, bold, italics) or color picker.
* In these cases, force focus back onto the text box.
* @param target
+ * @returns true if focus was kept on the text box, false otherwise
*/
- tryKeepingFocus = (target: Element | null) => {
+ public static tryKeepingFocus(target: Element | null, refocusFunc?: () => void) {
for (let newFocusEle = target instanceof HTMLElement ? target : null; newFocusEle; newFocusEle = newFocusEle?.parentElement) {
// bcz: HACK!! test if parent of new focused element is a UI button (should be more specific than testing className)
if (['fonticonbox', 'antimodeMenu-cont', 'popup-container'].includes(newFocusEle?.className ?? '')) {
- return this.EditorView?.focus(); // keep focus on text box
+ refocusFunc?.(); // keep focus on text box
+ return true;
}
}
- };
+ return false;
+ }
@action
onBlur = (e: React.FocusEvent) => {
- this.tryKeepingFocus(e.relatedTarget);
+ FormattedTextBox.tryKeepingFocus(e.relatedTarget, () => this.EditorView?.focus());
if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) {
const stordMarks = this.EditorView?.state.storedMarks?.slice();
- if (!(this.EditorView?.state.selection instanceof NodeSelection)) {
+ if (!(this.EditorView?.state.selection instanceof NodeSelection) && typeof this.dataDoc[this.fieldKey] !== 'number') {
this.autoLink();
if (this.EditorView?.state.tr) {
const tr = stordMarks?.reduce((tr2, m) => {
@@ -1791,8 +1797,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this.endUndoTypingBatch();
- FormattedTextBox.LiveTextUndo?.end();
- FormattedTextBox.LiveTextUndo = undefined;
+ this._liveTextUndo?.end();
// if the text box blurs and none of its contents are focused(), then pass the blur along
setTimeout(() => !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.());
@@ -1813,7 +1818,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return;
}
if (this._enteringStyle && 'tix!'.includes(e.key)) {
- const tag = e.key === 't' ? 'todo' : e.key === 'i' ? 'ignore' : e.key === 'x' ? 'disagree' : e.key === '!' ? 'important' : '??';
const node = state.selection.$from.nodeAfter;
const start = state.selection.from;
const end = state.selection.to;
@@ -1822,9 +1826,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
StopEvent(e);
_editorView.dispatch(
state.tr
- .removeMark(start, end, schema.marks.user_mark)
- .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }))
- .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag, modified: Math.round(Date.now() / 1000 / 60) }))
+ .removeMark(start, end, schema.marks.user_mark) //
+ .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }))
);
return;
}
@@ -1857,9 +1860,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
break;
default:
if ([AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document))) {
- const modified = Math.floor(Date.now() / 1000);
- const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark && m.attrs.modified === modified);
- _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified })));
+ const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark);
+ _editorView.dispatch(
+ state.tr
+ .removeStoredMark(schema.marks.user_mark) //
+ .addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }))
+ );
}
break;
}
@@ -1880,17 +1886,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
};
tryUpdateScrollHeight = () => {
- const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
+ const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yMargin || 0);
const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
if (this.EditorView && children && !SnappingManager.IsDragging) {
- const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
+ const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), 0) ?? 0;
const toNum = (val: string) => Number(val.replace('px', ''));
const toHgt = (node: Element): number => {
const { height, marginTop, marginBottom } = getComputedStyle(node);
const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height);
return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom));
};
- const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children);
+ const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children) + margins;
const scrollHeight = this.ProseRef && proseHeight;
if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) {
// if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
@@ -2112,8 +2118,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const rounded = StrCast(this.layoutDoc._layout_borderRounding) === '100%' ? '-rounded' : '';
setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
- const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), 0, this._props.xPadding ?? 0, this._props.screenXPadding?.(this._props.DocumentView?.()) ?? 0);
- const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, this._props.yPadding ?? 0); // prettier-ignore
+ const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), 0, this._props.xMargin ?? 0, this._props.screenXPadding?.(this._props.DocumentView?.()) ?? 0);
+ const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, this._props.yMargin ?? 0); // prettier-ignore
const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' >
return this.isLabel ? (
<LabelBox {...this._props} />