diff options
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); |