From 40b8e5c30e7251db6258cac5b4ed5fb4a6a2418d Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 7 Apr 2025 15:20:36 -0400 Subject: added multiToggle option to allow popup to stay up until explicitly untoggled. fixed multitoggle to honor toggleStatus. fixed popup to follow target. fixed setting text highlight background fro popup menu. fixed flashcardui buttons to have background sized properly. --- package-lock.json | 24 ++++++++++++ package.json | 1 + .../src/components/MultiToggle/MultiToggle.tsx | 16 +++++++- packages/components/src/components/Popup/Popup.tsx | 43 +++++++++++++++------- src/client/util/CurrentUserUtils.ts | 5 ++- .../views/collections/CollectionCardDeckView.tsx | 3 +- .../views/collections/FlashcardPracticeUI.tsx | 7 ++-- src/client/views/nodes/FontIconBox/FontIconBox.tsx | 1 + .../views/nodes/formattedText/FormattedTextBox.tsx | 2 +- .../views/nodes/formattedText/RichTextMenu.tsx | 3 +- 10 files changed, 81 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3b11eda7c..9acca90ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@pinecone-database/pinecone": "^2.2.2", "@react-google-maps/api": "^2.19.2", "@react-spring/web": "^9.7.5", + "@thednp/position-observer": "^1.0.7", "@turf/turf": "^7.1.0", "@types/bezier-js": "^4.1.3", "@types/brotli": "^1.3.4", @@ -12081,6 +12082,29 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@thednp/position-observer": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@thednp/position-observer/-/position-observer-1.0.7.tgz", + "integrity": "sha512-MkUAMMgqZPxy71hpcrKr9ZtedMk+oIFbFs5B8uKD857iuYKRJxgJtC1Itus14EEM4qMyeN0x47AUZJmZJQyXbQ==", + "license": "MIT", + "dependencies": { + "@thednp/shorty": "^2.0.10" + }, + "engines": { + "node": ">=16", + "pnpm": ">=8.6.0" + } + }, + "node_modules/@thednp/shorty": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@thednp/shorty/-/shorty-2.0.10.tgz", + "integrity": "sha512-H+hs1lw3Yc1NfwG0b7F7YmVjxQZ31NO2+6zx+I+9XabHxdwPKjvYJnkKKXr7bSItgm2AFrfOn5+3veB6W4iauw==", + "license": "MIT", + "engines": { + "node": ">=16", + "pnpm": ">=8.6.0" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", diff --git a/package.json b/package.json index fe76189bf..1038c470e 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "@pinecone-database/pinecone": "^2.2.2", "@react-google-maps/api": "^2.19.2", "@react-spring/web": "^9.7.5", + "@thednp/position-observer": "^1.0.7", "@turf/turf": "^7.1.0", "@types/bezier-js": "^4.1.3", "@types/brotli": "^1.3.4", diff --git a/packages/components/src/components/MultiToggle/MultiToggle.tsx b/packages/components/src/components/MultiToggle/MultiToggle.tsx index 0a6fb81c9..c1d610c34 100644 --- a/packages/components/src/components/MultiToggle/MultiToggle.tsx +++ b/packages/components/src/components/MultiToggle/MultiToggle.tsx @@ -17,6 +17,7 @@ export interface IMultiToggleProps extends IGlobalProps { selectedItems?: (string | number) | (string | number)[]; onSelectionChange?: (val: (string | number) | (string | number)[], added: boolean) => unknown; toggleStatus?: boolean; + showUntilToggle?: boolean; // whether popup stays open when background is clicked. muyst click toggle button tp close it. } function promoteToArrayOrUndefined(d: (string | number)[] | (string | number) | undefined) { @@ -32,12 +33,23 @@ export const MultiToggle = (props: IMultiToggleProps) => { init = false; const [selectedItemsLocal, setSelectedItemsLocal] = useState(initVal as (string | number) | (string | number)[]); - const { items, selectedItems = selectedItemsLocal, tooltip, tooltipPlacement = 'top', onSelectionChange, color, background } = props; + const { + items, // + selectedItems = selectedItemsLocal, + tooltip, + toggleStatus, + tooltipPlacement = 'top', + onSelectionChange, + color, + background, + } = props; const itemsMap = new Map(); items.forEach(item => itemsMap.set(item.val, item)); return (
{ {promoteToArray(selectedItems).length < 2 ? null :
+
}
} - isToggle={true} + showUntilToggle={props.showUntilToggle ?? true} toggleFunc={() => { const selItem = items.find(item => promoteToArray(selectedItems).includes(item.val)); selItem && setSelectedItemsLocal([selItem.val]); diff --git a/packages/components/src/components/Popup/Popup.tsx b/packages/components/src/components/Popup/Popup.tsx index 9e72ece87..9e91a75cf 100644 --- a/packages/components/src/components/Popup/Popup.tsx +++ b/packages/components/src/components/Popup/Popup.tsx @@ -3,6 +3,7 @@ import { IGlobalProps, Placement, Size } from '../../global'; import { Toggle, ToggleType } from '../Toggle'; import './Popup.scss'; import { Popper } from '@mui/material'; +import PositionObserver from '@thednp/position-observer'; export enum PopupTrigger { CLICK = 'click', @@ -23,9 +24,10 @@ export interface IPopupProps extends IGlobalProps { isOpen?: boolean; setOpen?: (b: boolean) => void; background?: string; - isToggle?: boolean; // whether popup stays open when background is clicked. muyst click toggle button tp close it. + showUntilToggle?: boolean; // whether popup stays open when background is clicked. muyst click toggle button tp close it. toggleFunc?: () => void; popupContainsPt?: (x: number, y: number) => boolean; + multitoggle?: boolean; } /** @@ -57,20 +59,21 @@ export const Popup = (props: IPopupProps) => { fillWidth, iconPlacement = 'left', background, + multitoggle, popupContainsPt, } = props; const triggerRef = useRef(null); const popperRef = useRef(null); - const toggleRef = useRef(null); + const [toggleRef, setToggleRef] = useState(null); let timeout = setTimeout(() => {}); const handlePointerAwayDown = (e: PointerEvent) => { const rect = popperRef.current?.getBoundingClientRect(); - const rect2 = toggleRef.current?.getBoundingClientRect(); + const rect2 = toggleRef?.getBoundingClientRect(); if ( - !props.isToggle && + !props.showUntilToggle && (!rect2 || !(rect2.left < e.clientX && rect2.top < e.clientY && rect2.right > e.clientX && rect2.bottom > e.clientY)) && rect && !(rect.left < e.clientX && rect.top < e.clientY && rect.right > e.clientX && rect.bottom > e.clientY) && @@ -82,22 +85,36 @@ export const Popup = (props: IPopupProps) => { } }; + let observer: PositionObserver | undefined = undefined; + const [previousPosition, setPreviousPosition] = useState(toggleRef?.getBoundingClientRect()); + useEffect(() => { if (isOpen) { window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true }); window.addEventListener('pointerdown', handlePointerAwayDown, { capture: true }); - return () => window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true }); - } - }, [isOpen, popupContainsPt]); - + if (toggleRef && multitoggle) { + (observer = new PositionObserver(entries => { + entries.forEach(entry => { + const currentPosition = entry.boundingClientRect; + if (Math.floor(currentPosition.top) !== Math.floor(previousPosition?.top ?? 0) || Math.floor(currentPosition.left) !== Math.floor(previousPosition?.left ?? 0)) { + // Perform actions when position changes + setPreviousPosition(currentPosition); // Update previous position + } + }); + })).observe(toggleRef); + } + return () => { + window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true }); + observer?.disconnect(); + }; + } else observer?.disconnect(); + }, [isOpen, toggleRef, popupContainsPt]); return (
{ - if (trigger === PopupTrigger.CLICK) setOpen(!isOpen); - }} + className={`trigger-container ${fillWidth && 'fillWidth'}`} + onClick={() => trigger === PopupTrigger.CLICK && setOpen(!isOpen)} onPointerEnter={() => { if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { clearTimeout(timeout); @@ -109,7 +126,7 @@ export const Popup = (props: IPopupProps) => { timeout = setTimeout(() => setOpen(false), 1000); } }}> -
+
setToggleRef(R)}> {toggle ?? ( ; ignoreClick?: boolean; + showUntilToggle?: boolean; // whether the popup should stay open when the background is clicked. buttonText?: string; backgroundColor?: string; waitForDoubleClickToClick?: boolean; @@ -778,7 +779,7 @@ pie title Minerals in my tap water { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType: Gestures.Circle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType: Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType: Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, - { title: "Ink", toolTip: "Ink", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Ink, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }, + { title: "Ink", toolTip: "Ink", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Ink, showUntilToggle: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }, subMenu: [ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: InkInkTool.Pen, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }}, { title: "Highlight",toolTip: "Highlight (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", toolType: InkInkTool.Highlight, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }}, @@ -786,7 +787,7 @@ pie title Minerals in my tap water ]}, { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: InkProperty.StrokeWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}, numBtnMin: 1, linearBtnWidth:40}, { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: InkProperty.StrokeColor,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}}, - { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, + { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, showUntilToggle: true, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, subMenu: [ { title: "Stroke", toolTip: "Eraser complete strokes",btnType: ButtonType.ToggleButton, icon: "eraser", toolType:InkEraserTool.Stroke, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}}, { title: "Segment", toolTip: "Erase between intersections",btnType:ButtonType.ToggleButton,icon:"xmark", toolType:InkEraserTool.Segment, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}}, diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index 756b37f99..50de7c601 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -23,7 +23,8 @@ import { undoable, UndoManager } from '../../util/UndoManager'; import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; import { TagItem } from '../TagsView'; -import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import { DocumentViewProps } from '../nodes/DocumentContentsView'; +import { DocumentView } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import './CollectionCardDeckView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx index 8cd9c5452..f24a8acb7 100644 --- a/src/client/views/collections/FlashcardPracticeUI.tsx +++ b/src/client/views/collections/FlashcardPracticeUI.tsx @@ -63,8 +63,8 @@ export class FlashcardPracticeUI extends ObservableReactComponent doc.title === 'Filter'); } // prettier-ignore @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore - btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale); - btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height)); + btnHeight = () => NumCast(this.filterDoc?.height); + btnWidth = () => (!this.filterDoc ? 1 : NumCast(this.filterDoc._width)); /** * Sets the practice mode answer style for flashcards @@ -135,7 +135,6 @@ export class FlashcardPracticeUI extends ObservableReactComponent= 1 ? undefined : `translateY(${this.btnHeight() / this._props.ScreenToLocalBoxXf().Scale - this.btnHeight()}px)`, }}> +
{!this.filterDoc ? null : (
() { multiSelect={true} onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))} toggleStatus={toggleStatus} + showUntilToggle={BoolCast(this.Document.showUntilToggle)} // allow the toggle to be clickable unless ignoreClick is set on the Document label={selectedItems.length === 1 ? selectedItems[0] : this.label} items={items.map(item => ({ icon: , diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index d90be007f..341340363 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1761,7 +1761,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { 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') { + if (['fonticonbox', 'antimodeMenu-cont', 'popup-container'].includes(newFocusEle?.className ?? '')) { return this.EditorView?.focus(); // keep focus on text box } } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 758b4035e..10c95f1e1 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -367,7 +367,8 @@ export class RichTextMenu extends AntimodeMenu { setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { if (this.TextView && this.view && fontField !== 'fitBox') { - if (this.view.hasFocus()) { + const anchorNode = window.getSelection()?.anchorNode; + if (this.view.hasFocus() || (anchorNode && this.TextView.ProseRef?.contains(anchorNode))) { const attrs: { [key: string]: string } = {}; attrs[fontField] = value; const fmark = this.view.state.schema.marks['pF' + fontField.substring(1)].create(attrs); -- cgit v1.2.3-70-g09d2