aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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);