aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/components/src/components/Popup/Popup.tsx281
-rw-r--r--src/client/views/nodes/formattedText/EquationView.tsx8
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx47
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx13
4 files changed, 170 insertions, 179 deletions
diff --git a/packages/components/src/components/Popup/Popup.tsx b/packages/components/src/components/Popup/Popup.tsx
index 5a1179c69..92e7227bd 100644
--- a/packages/components/src/components/Popup/Popup.tsx
+++ b/packages/components/src/components/Popup/Popup.tsx
@@ -1,32 +1,32 @@
-import React, { useEffect, useRef, useState } from 'react'
-import { Colors, IGlobalProps, Placement, Size , getFormLabelSize, isDark } from '../../global'
-import { Toggle, ToggleType } from '../Toggle'
-import './Popup.scss'
-import { Popper } from '@mui/material'
+import React, { useEffect, useRef, useState } from 'react';
+import { Colors, IGlobalProps, Placement, Size, getFormLabelSize, isDark } from '../../global';
+import { Toggle, ToggleType } from '../Toggle';
+import './Popup.scss';
+import { Popper } from '@mui/material';
export enum PopupTrigger {
- CLICK = "click",
- HOVER = "hover",
- HOVER_DELAY = "hover_delay"
+ CLICK = 'click',
+ HOVER = 'hover',
+ HOVER_DELAY = 'hover_delay',
}
export interface IPopupProps extends IGlobalProps {
- text?: string
- icon?: JSX.Element | string,
- iconPlacement?: Placement,
- placement?: Placement,
- size?: Size
- height?: number
- toggle?: JSX.Element;
- popup: JSX.Element | string | (() => JSX.Element)
- trigger?: PopupTrigger
- toggleStatus?: boolean;
- isOpen?: boolean;
- setOpen?: (b: boolean) => void;
- background?: string,
- isToggle?: boolean;
- toggleFunc?: () => void;
- popupContainsPt?: (x:number, y:number) => boolean;
+ text?: string;
+ icon?: JSX.Element | string;
+ iconPlacement?: Placement;
+ placement?: Placement;
+ size?: Size;
+ height?: number;
+ toggle?: JSX.Element;
+ popup: JSX.Element | string | (() => JSX.Element);
+ trigger?: PopupTrigger;
+ toggleStatus?: boolean;
+ isOpen?: boolean;
+ setOpen?: (b: boolean) => void;
+ background?: string;
+ isToggle?: boolean;
+ toggleFunc?: () => void;
+ popupContainsPt?: (x: number, y: number) => boolean;
}
/**
@@ -38,130 +38,121 @@ export interface IPopupProps extends IGlobalProps {
* Look at: import Select from "react-select";
*/
export const Popup = (props: IPopupProps) => {
-
- const [locIsOpen, locSetOpen] = useState<boolean>(false)
+ const [locIsOpen, locSetOpen] = useState<boolean>(false);
- const {
- text,
- size,
- icon,
- popup,
- type,
- color,
- isOpen = locIsOpen,
- setOpen = locSetOpen,
- toggle,
- tooltip,
- trigger = PopupTrigger.CLICK,
- placement = 'bottom-start',
- width,
- height,
- fillWidth,
- iconPlacement = 'left',
- background = isDark(color) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY,
- popupContainsPt
- } = props
-
- const triggerRef = useRef(null);
- const popperRef = useRef(null);
+ const {
+ text,
+ size,
+ icon,
+ popup,
+ type,
+ color,
+ isOpen = locIsOpen,
+ setOpen = locSetOpen,
+ toggle,
+ tooltip,
+ trigger = PopupTrigger.CLICK,
+ placement = 'bottom-start',
+ width,
+ height,
+ fillWidth,
+ iconPlacement = 'left',
+ background = isDark(color) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY,
+ popupContainsPt,
+ } = props;
- let timeout = setTimeout(() => {});
+ const triggerRef = useRef(null);
+ const popperRef = useRef(null);
- const handlePointerAwayDown = (e: PointerEvent) => {
- const rect = (popperRef.current as any)?.getBoundingClientRect();
- if (rect && !(rect.left < e.clientX && rect.top < e.clientY && rect.right > e.clientX && rect.bottom > e.clientY) &&
- !popupContainsPt?.(e.clientX, e.clientY)) {
- e.preventDefault();
- setOpen(false);
- }
- }
+ let timeout = setTimeout(() => {});
- useEffect(() => {
- if (isOpen) {
- window.removeEventListener("pointerdown", handlePointerAwayDown, {capture:true})
- window.addEventListener("pointerdown", handlePointerAwayDown, {capture:true});
- return () => {
- window.removeEventListener("pointerdown", handlePointerAwayDown, {capture:true});
- }
- }}, [isOpen, popupContainsPt])
-
- return (
- <div className={`popup-wrapper ${fillWidth && 'fillWidth'}`} >
- <div
- className={`trigger-container ${fillWidth && 'fillWidth'}`}
- ref={triggerRef}
- onClick={() => {
- if (trigger === PopupTrigger.CLICK) setOpen (!isOpen)
- }}
- onPointerEnter={() => {
- if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) {
- clearTimeout(timeout);
- setOpen(true)
- }
- }}
- onPointerLeave={() => {
- if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) {
- timeout = setTimeout(() => setOpen(false), 1000);
- }
- }}
- >
- {toggle
- ?
- toggle
- :
- <Toggle
- tooltip={tooltip}
- size={size}
- type={type}
- color={color}
- background={props.isToggle ? undefined : background}
- toggleType={ToggleType.BUTTON}
- icon={icon}
- iconPlacement={iconPlacement}
- text={text}
- label={props.label}
- toggleStatus={isOpen || props.toggleStatus}
- onClick={() => {
- if (trigger === PopupTrigger.CLICK) {
- if (!props.isToggle || props.toggleStatus) {
- setOpen(!isOpen)
- }
- props.toggleFunc?.();
- }
- }}
- fillWidth={fillWidth}
- />
+ const handlePointerAwayDown = (e: PointerEvent) => {
+ const rect = (popperRef.current as any)?.getBoundingClientRect();
+ if (rect && !(rect.left < e.clientX && rect.top < e.clientY && rect.right > e.clientX && rect.bottom > e.clientY) && !popupContainsPt?.(e.clientX, e.clientY)) {
+ e.preventDefault();
+ setOpen(false);
}
- </div>
- <Popper
- open={isOpen}
- style={{zIndex: 20000}}
- anchorEl={triggerRef.current}
- placement={placement}
- modifiers={[
- ]}
- >
- <div className={`popup-container`} ref={popperRef}
- style={{width, height, background}}
- onPointerDown={(e) => {
- e.stopPropagation();
- }}
- onPointerEnter={() => {
- if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) {
- clearTimeout(timeout);
- setOpen(true);
- }
- }}
- onPointerLeave={() => {
- if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) {
- timeout = setTimeout(() => setOpen(false), 200);
- }
- }}
- >
- {!isOpen ? (null): typeof popup === 'function' ? popup() : popup}
- </div>
- </Popper>
- </div>
- )
-}
+ };
+
+ useEffect(() => {
+ if (isOpen) {
+ window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true });
+ window.addEventListener('pointerdown', handlePointerAwayDown, { capture: true });
+ return () => {
+ window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true });
+ };
+ }
+ }, [isOpen, popupContainsPt]);
+ return (
+ <div className={`popup-wrapper ${fillWidth && 'fillWidth'}`}>
+ <div
+ className={`trigger-container ${fillWidth && 'fillWidth'}`}
+ ref={triggerRef}
+ onClick={() => {
+ if (trigger === PopupTrigger.CLICK) setOpen(!isOpen);
+ }}
+ onPointerEnter={() => {
+ if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) {
+ clearTimeout(timeout);
+ setOpen(true);
+ }
+ }}
+ onPointerLeave={() => {
+ if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) {
+ timeout = setTimeout(() => setOpen(false), 1000);
+ }
+ }}>
+ {toggle ? (
+ toggle
+ ) : (
+ <Toggle
+ tooltip={tooltip}
+ size={size}
+ type={type}
+ color={color}
+ background={props.isToggle ? undefined : background}
+ toggleType={ToggleType.BUTTON}
+ icon={icon}
+ iconPlacement={iconPlacement}
+ text={text}
+ label={props.label}
+ toggleStatus={isOpen || props.toggleStatus}
+ onClick={() => {
+ if (trigger === PopupTrigger.CLICK) {
+ if (!props.isToggle || props.toggleStatus) {
+ setOpen(!isOpen);
+ }
+ props.toggleFunc?.();
+ }
+ }}
+ fillWidth={fillWidth}
+ />
+ )}
+ </div>
+ <Popper open={isOpen} style={{ zIndex: 20000 }} anchorEl={triggerRef.current} placement={placement} modifiers={[]}>
+ <div
+ className={`popup-container`}
+ ref={popperRef}
+ style={{ width, height, background }}
+ tabIndex={-1}
+ onPointerDown={e => {
+ e.stopPropagation();
+ }}
+ onPointerEnter={() => {
+ if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) {
+ clearTimeout(timeout);
+ setOpen(true);
+ }
+ }}
+ onPointerLeave={() => {
+ if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) {
+ timeout = setTimeout(() => setOpen(false), 200);
+ }
+ }}>
+ {!isOpen ? null : typeof popup === 'function' ? popup() : popup}
+ </div>
+ </Popper>
+ </div>
+ );
+};
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index df1421a33..e0450b202 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -110,13 +110,7 @@ export class EquationView {
}
selectNode() {
this.view.dispatch(this.view.state.tr.setSelection(new TextSelection(this.view.state.doc.resolve(this.getPos() ?? 0))));
- this.tbox._applyingChange = this.tbox.fieldKey; // setting focus will make prosemirror lose focus, which will cause it to change its selection to a text selection, which causes this view to get rebuilt but it's no longer node selected, so the equationview won't have focus
- setTimeout(() => {
- this._editor?.mathField.focus();
- setTimeout(() => {
- this.tbox._applyingChange = '';
- });
- });
+ setTimeout(() => this._editor?.mathField.focus());
}
deselectNode() {}
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a6870d65b..eb1f9d07b 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -131,9 +131,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
private _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason. This flags the 2nd click as happening on a selection which allows bullet points to toggle
private _break = true;
- public _applyingChange: string = '';
public ProseRef?: HTMLDivElement;
+ /**
+ * ApplyingChange - Marks whether an interactive text edit is currently in the process of being written to the database.
+ * This is needed to distinguish changes to text fields caused by editing vs those caused by changes to
+ * the prototype or other external edits
+ */
+ public ApplyingChange: string = '';
+
@observable _showSidebar = false;
@computed get fontColor() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore
@@ -359,8 +365,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
let unchanged = true;
const textChange = newText !== prevData?.Text; // the Text string can change even if the RichText doesn't because dashFieldViews may return new strings as the data they reference changes
const rtField = (layoutData !== prevData ? layoutData : undefined) ?? protoData;
- if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
- this._applyingChange = this.fieldKey;
+ if (this.ApplyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
+ this.ApplyingChange = this.fieldKey;
if ((!prevData && !protoData && !layoutData) || newText || (!newText && !protoData && !layoutData)) {
// 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 (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) {
@@ -370,7 +376,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
dataDoc[this.fieldKey] =
numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined;
textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText });
- this._applyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false
+ this.ApplyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false
unchanged = false;
}
} else if (rtField) {
@@ -381,7 +387,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText });
unchanged = false;
}
- this._applyingChange = '';
+ this.ApplyingChange = '';
if (!unchanged) {
this.updateTitle();
this.tryUpdateScrollHeight();
@@ -1012,7 +1018,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (i.className !== 'ProseMirror-separator') this.getImageDesc(i.src);
}
}
- // console.log('HI' + this.ProseRef?.getElementsByTagName('img'));
};
getImageDesc = async (u: string) => {
@@ -1315,7 +1320,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
},
incomingValue => {
- if (this.EditorView && this._applyingChange !== this.fieldKey) {
+ if (this.EditorView && this.ApplyingChange !== this.fieldKey) {
if (incomingValue?.data) {
const updatedState = JSON.parse(incomingValue.data.Data);
if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) {
@@ -1405,7 +1410,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
// } catch (err) {
// console.log('Drop failed', err);
// }
- // console.log('LKSDFLJ');
this.addDocument?.(DocCast(this.Document.image));
}
@@ -1695,10 +1699,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
setTimeout(() => (this.ProseRef?.children?.[0] as HTMLElement).focus(), 200);
};
- IsFocused = false;
@action
onFocused = (e: React.FocusEvent): void => {
- this.IsFocused = true;
// applyDevTools.applyDevTools(this.EditorView);
e.stopPropagation();
};
@@ -1780,18 +1782,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this._undoTyping = undefined;
}
- @action
- onBlur = (e: React.FocusEvent) => {
- let ele: HTMLElement | null = e.relatedTarget instanceof HTMLElement ? e.relatedTarget : null;
- if (ele?.tabIndex === -1) {
- for (; ele; ele = ele?.parentElement) {
- if (ele?.className === 'fonticonbox') {
- setTimeout(() => this._ref.current?.focus());
- break;
- }
+ /**
+ * 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
+ */
+ tryKeepingFocus = (target: Element | null) => {
+ for (let newFocusEle = target instanceof HTMLElement ? target : null; newFocusEle; newFocusEle = newFocusEle?.parentElement) {
+ // test if parent of new focused element is a UI button (should be more specific than testing className)
+ if (newFocusEle?.className === 'fonticonbox' || newFocusEle?.className === 'popup-container') {
+ return this.EditorView?.focus(); // keep focus on text box
}
}
- if (ele?.className !== 'fonticonbox') this.IsFocused = false;
+ };
+
+ @action
+ onBlur = (e: React.FocusEvent) => {
+ this.tryKeepingFocus(e.relatedTarget);
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();
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 09994a889..758b4035e 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -367,20 +367,19 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
if (this.TextView && this.view && fontField !== 'fitBox') {
- if (!this.TextView.IsFocused) {
+ if (this.view.hasFocus()) {
+ const attrs: { [key: string]: string } = {};
+ attrs[fontField] = value;
+ const fmark = this.view.state.schema.marks['pF' + fontField.substring(1)].create(attrs);
+ this.setMark(fmark, this.view.state, (tx: Transaction) => this.view?.dispatch(tx.addStoredMark(fmark)), true);
+ } else {
Array.from(new Set([...DocumentView.Selected(), this.TextView.DocumentView?.()]))
.filter(v => v?.ComponentView instanceof FormattedTextBox && v.ComponentView.EditorView?.TextView)
.map(v => v!.ComponentView as FormattedTextBox)
.forEach(view => {
view.EditorView!.TextView!.dataDoc[(view.EditorView!.TextView!.fieldKey ?? 'text') + `_${fontField}`] = value;
});
- this.view.focus();
}
- const attrs: { [key: string]: string } = {};
- attrs[fontField] = value;
- const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs);
- this.setMark(fmark, this.view.state, (tx: Transaction) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
- this.view.focus();
} else if (this.dataDoc) {
this.dataDoc[`${Doc.LayoutFieldKey(this.dataDoc)}_${fontField}`] = value;
this.updateMenu(undefined, undefined, undefined, this.dataDoc);