aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-04-07 15:20:36 -0400
committerbobzel <zzzman@gmail.com>2025-04-07 15:20:36 -0400
commit40b8e5c30e7251db6258cac5b4ed5fb4a6a2418d (patch)
tree89ece841da05670fdb4fc429ed0c46b51b539954
parenta3d6fae1482c61ca725d0a103f13b621aa32b3e3 (diff)
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.
-rw-r--r--package-lock.json24
-rw-r--r--package.json1
-rw-r--r--packages/components/src/components/MultiToggle/MultiToggle.tsx16
-rw-r--r--packages/components/src/components/Popup/Popup.tsx43
-rw-r--r--src/client/util/CurrentUserUtils.ts5
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx3
-rw-r--r--src/client/views/collections/FlashcardPracticeUI.tsx7
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx1
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx2
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx3
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 (
<div className="multiToggle-container">
<Popup
+ isOpen={toggleStatus}
+ multitoggle={true} // this is used to indicate that this is a multi toggle, so it can be styled differently in the popup
toggle={
<div style={{ position: 'relative' }}>
<IconButton
@@ -53,7 +65,7 @@ export const MultiToggle = (props: IMultiToggleProps) => {
{promoteToArray(selectedItems).length < 2 ? null : <div style={{ position: 'absolute', top: '0', left: '0', color: color ?? Colors.MEDIUM_BLUE }}>+</div>}
</div>
}
- 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<HTMLDivElement | null>(null);
- const toggleRef = useRef<HTMLDivElement | null>(null);
+ const [toggleRef, setToggleRef] = useState<HTMLDivElement | null>(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<DOMRect | undefined>(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 (
<div className={`popup-wrapper ${fillWidth && 'fillWidth'}`}>
<div
- className={`trigger-container ${fillWidth && 'fillWidth'}`}
ref={triggerRef}
- onClick={() => {
- 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);
}
}}>
- <div ref={toggleRef}>
+ <div className="special" ref={R => setToggleRef(R)}>
{toggle ?? (
<Toggle
tooltip={tooltip}
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f1cc81c88..ca94791d0 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -57,6 +57,7 @@ export interface Button {
expertMode?: boolean;// available only in expert mode
btnList?: List<string>;
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<PracticeUIProp
@computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => 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<PracticeUIProp
className="FlashcardPracticeUI-practiceModes"
style={{
background: SnappingManager.userVariantColor,
- transform: this._props.ScreenToLocalBoxXf().Scale >= 1 ? undefined : `translateY(${this.btnHeight() / this._props.ScreenToLocalBoxXf().Scale - this.btnHeight()}px)`,
}}>
<MultiToggle
tooltip="Practice flashcards one at a time"
@@ -194,7 +193,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
{this.emptyMessage}
{this.practiceButtons}
{this._props.layoutDoc._chromeHidden ? null : (
- <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaling})` }}>
+ <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnWidth(), transform: `scale(${this._props.uiBtnScaling})` }}>
{!this.filterDoc ? null : (
<div style={{ background: SnappingManager.userVariantColor }}>
<DocumentView
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index f83b5e351..d14def9aa 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -302,6 +302,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
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: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />,
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<FormattedTextB
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') {
+ 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<AntimodeMenuProps> {
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);