Skip to content

Commit b6a204d

Browse files
authored
Merge pull request #2707 from zsviczian/floatingModalTest
FloatingModal
2 parents e87a936 + 9ef7f61 commit b6a204d

6 files changed

Lines changed: 130 additions & 89 deletions

File tree

package-lock.json

Lines changed: 7 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"license": "MIT",
2626
"dependencies": {
2727
"@popperjs/core": "^2.11.8",
28-
"@zsviczian/excalidraw": "0.18.0-83",
28+
"@zsviczian/excalidraw": "0.18.0-84",
2929
"chroma-js": "^3.1.2",
3030
"clsx": "^2.0.0",
3131
"@zsviczian/colormaster": "^1.2.2",

src/shared/Dialogs/FloatingModal.ts

Lines changed: 44 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { App, Modal } from "obsidian";
2+
import { clamp } from "@radix-ui/number";
23

34
function getClientPoint(e: PointerEvent | TouchEvent) {
45
if (e.type.startsWith("touch")) {
@@ -26,13 +27,13 @@ function isPointOnText(e: PointerEvent | TouchEvent, doc: Document): boolean {
2627
let offset: number | null = null;
2728

2829
// Chromium/Firefox-ish
29-
const caretPos = (doc as any).caretPositionFromPoint?.(pt.x, pt.y);
30+
const caretPos = doc.caretPositionFromPoint?.(pt.x, pt.y);
3031
if (caretPos?.offsetNode) {
3132
offsetNode = caretPos.offsetNode;
3233
offset = typeof caretPos.offset === "number" ? caretPos.offset : null;
3334
} else {
3435
// Safari/WebKit
35-
const caretRange = (doc as any).caretRangeFromPoint?.(pt.x, pt.y);
36+
const caretRange = doc.caretRangeFromPoint?.(pt.x, pt.y);
3637
if (caretRange?.startContainer) {
3738
offsetNode = caretRange.startContainer;
3839
offset = typeof caretRange.startOffset === "number" ? caretRange.startOffset : null;
@@ -67,14 +68,13 @@ export class FloatingModal extends Modal {
6768
private dragging = false;
6869
private offsetX = 0;
6970
private offsetY = 0;
70-
private pointerDownHandler: (e: PointerEvent | TouchEvent) => void;
71-
private pointerMoveHandler: (e: PointerEvent | TouchEvent) => void;
72-
private pointerUpHandler: () => void;
71+
private readonly pointerDownHandler: (e: PointerEvent | TouchEvent) => void;
72+
private readonly pointerMoveHandler: (e: PointerEvent | TouchEvent) => void;
73+
private readonly pointerUpHandler: () => void;
7374

7475
private disableKeyCapture = true; // new flag: when true, let keystrokes pass through to workspace
75-
private previousActive: HTMLElement | null = null; // stores element focused before opening
76-
private escListener: (e: KeyboardEvent) => void;
77-
private modalKeydownStopHandler: (e: KeyboardEvent) => void; // store handler so we can remove it
76+
private previousActive?: HTMLElement = null; // stores element focused before opening
77+
private readonly modalKeydownStopHandler: (e: KeyboardEvent) => void; // store handler so we can remove it
7878
private ownerWindow: Window = window;
7979
private ownerDocument: Document = document;
8080

@@ -84,24 +84,30 @@ export class FloatingModal extends Modal {
8484
this.pointerDownHandler = this.handlePointerDown.bind(this);
8585
this.pointerMoveHandler = this.handlePointerMove.bind(this);
8686
this.pointerUpHandler = this.handlePointerUp.bind(this);
87-
this.escListener = this.handleEscKey.bind(this);
8887
this.modalKeydownStopHandler = (ev: KeyboardEvent) => ev.stopPropagation();
89-
}
9088

91-
private handlePointerDown(e: PointerEvent | TouchEvent): void {
92-
// Get the target element
93-
const target = e.target as HTMLElement;
89+
this.setDimBackground(false);
90+
}
9491

95-
// Ignore if clicking on interactive elements
96-
if (
92+
protected shouldNotStartDrag(target: HTMLElement, event: PointerEvent | TouchEvent): boolean {
93+
return Boolean(
9794
target instanceof HTMLInputElement ||
9895
target instanceof HTMLTextAreaElement ||
9996
target instanceof HTMLSelectElement ||
10097
target instanceof HTMLButtonElement ||
101-
isPointOnText(e, this.ownerDocument) ||
98+
isPointOnText(event, this.ownerDocument) ||
10299
target.closest(".clickable-icon") ||
103-
target.closest(".modal-close-button") // ensure close button never starts drag
104-
) {
100+
// ensure close button never starts drag
101+
target.closest(".modal-close-button")
102+
)
103+
}
104+
105+
private handlePointerDown(e: PointerEvent | TouchEvent): void {
106+
// Get the target element
107+
const target = e.target as HTMLElement;
108+
109+
// Ignore if clicking on interactive elements
110+
if (this.shouldNotStartDrag(target, e)) {
105111
return;
106112
}
107113

@@ -149,25 +155,18 @@ export class FloatingModal extends Modal {
149155
e.preventDefault();
150156
if (e.type === "touchmove") e.stopPropagation();
151157

152-
let clientX, clientY;
158+
const { clientX, clientY } = (e.type === "touchmove") ?
159+
(e as TouchEvent).touches[0] : e as PointerEvent;
153160

154-
if (e.type === "touchmove") {
155-
const touch = (e as TouchEvent).touches[0];
156-
clientX = touch.clientX;
157-
clientY = touch.clientY;
158-
} else {
159-
const pointerEvent = e as PointerEvent;
160-
clientX = pointerEvent.clientX;
161-
clientY = pointerEvent.clientY;
162-
}
163-
164-
const x = clientX - this.offsetX;
165-
const y = clientY - this.offsetY;
161+
// Prevent moving the modal offscreen
162+
const { width, height } = modalEl.getBoundingClientRect();
163+
const margin = 8;
164+
const x = clamp(clientX - this.offsetX, [margin, this.ownerWindow.innerWidth - width - margin]);
165+
const y = clamp(clientY - this.offsetY, [margin, this.ownerWindow.innerHeight - height - margin]);
166166

167167
// Position the modal element
168168
modalEl.style.left = `${x}px`;
169169
modalEl.style.top = `${y}px`;
170-
modalEl.style.transform = "none"; // Remove centering transform
171170
}
172171

173172
private handlePointerUp(): void {
@@ -180,13 +179,6 @@ export class FloatingModal extends Modal {
180179
this.ownerDocument.removeEventListener("touchcancel", this.pointerUpHandler);
181180
}
182181

183-
private handleEscKey(e: KeyboardEvent) {
184-
if (e.key === "Escape") {
185-
e.stopPropagation();
186-
this.close();
187-
}
188-
}
189-
190182
open(): void {
191183
super.open();
192184
this.ownerDocument = this.modalEl.ownerDocument ?? document;
@@ -195,20 +187,17 @@ export class FloatingModal extends Modal {
195187
if (this.disableKeyCapture) {
196188
this.previousActive = this.ownerDocument.activeElement as HTMLElement | null;
197189
try {
198-
// @ts-ignore pop modal's key scope so keys are not intercepted
190+
// Release modal scope so focus and key handling can return to the workspace.
191+
// @ts-ignore
199192
this.app.keymap.popScope(this.scope);
200193
} catch {}
201-
try {
202-
// @ts-ignore prevent automatic selection / focus restoration
203-
this.shouldRestoreSelection = false;
204-
} catch {}
194+
// prevent automatic selection / focus restoration
195+
this.shouldRestoreSelection = false;
205196
}
206197
setTimeout(() => {
207-
//@ts-ignore
208-
const { containerEl, modalEl, bgEl, headerEl } = this;
209-
containerEl.style.pointerEvents = "none";
210-
if (bgEl) bgEl.style.display = "none";
211-
if (headerEl) headerEl.style.pointerEvents = "none";
198+
// @ts-ignore
199+
const { containerEl, modalEl, bgEl } = this;
200+
containerEl.addClass("mod-excalidraw-draggable")
212201

213202
// Set initial position and make modal draggable
214203
if (modalEl) {
@@ -224,9 +213,6 @@ export class FloatingModal extends Modal {
224213
modalEl.style.left = `${centerX}px`;
225214
modalEl.style.top = `${centerY}px`;
226215
modalEl.style.transform = "none";
227-
const modalStyle = this.ownerWindow.getComputedStyle(modalEl);
228-
modalEl.style.borderBottomLeftRadius = modalStyle.borderTopLeftRadius;
229-
modalEl.style.borderBottomRightRadius = modalStyle.borderTopRightRadius;
230216

231217
// Add event listeners for both pointer and touch events
232218
modalEl.addEventListener("pointerdown", this.pointerDownHandler as (e: PointerEvent) => void);
@@ -237,15 +223,19 @@ export class FloatingModal extends Modal {
237223
if (this.disableKeyCapture) {
238224
// Prevent the modal from stealing focus
239225
modalEl.setAttr("tabindex", "-1");
226+
227+
// In order to propagate keypresses, modal container and backdrop
228+
// need to ignore (and thus propagate) any pointerEvents.
229+
containerEl.style.pointerEvents = "none";
230+
if (bgEl) bgEl.style.pointerEvents = "none";
231+
240232
// Refocus previous element (if still in DOM)
241233
if (this.previousActive?.isConnected) {
242234
this.previousActive.focus({ preventScroll: true });
243235
}
244236
// Stop key events originating inside the modal from bubbling back
245237
modalEl.addEventListener("keydown", this.modalKeydownStopHandler, { capture: true });
246238
}
247-
// Add ESC listener (capture to run before underlying workspace)
248-
this.ownerDocument.addEventListener("keydown", this.escListener, { capture: true });
249239

250240
// NEW: re-enable pointer events on the close button so it is tappable on mobile
251241
const closeBtn = containerEl.querySelector(".modal-close-button");
@@ -271,7 +261,6 @@ export class FloatingModal extends Modal {
271261
}
272262
// Remove any remaining document event listeners
273263
this.handlePointerUp();
274-
this.ownerDocument.removeEventListener("keydown", this.escListener, { capture: true });
275264

276265
super.close();
277266
}

src/shared/Dialogs/Prompt.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import {
77
Instruction,
88
TFile,
99
Notice,
10-
TextAreaComponent,
11-
getIcon,
10+
TextAreaComponent
1211
} from "obsidian";
1312
import ExcalidrawView from "../../view/ExcalidrawView";
1413
import ExcalidrawPlugin from "../../core/main";
@@ -93,7 +92,7 @@ export class LaTexPrompt extends FloatingModal {
9392
private resolvePromise: (input: string) => void;
9493
private rejectPromise: (reason?: any) => void;
9594
private editorView : EditorView;
96-
private latexsSuitePlugin : any;
95+
private readonly latexsSuitePlugin : any;
9796

9897
protected constructor(
9998
app: App,
@@ -114,11 +113,15 @@ export class LaTexPrompt extends FloatingModal {
114113
this.editorView.focus();
115114
}
116115

116+
protected shouldNotStartDrag(target: HTMLElement, event: PointerEvent | TouchEvent): boolean {
117+
return target.closest(".cm-editor") != null || super.shouldNotStartDrag(target, event);
118+
}
119+
117120
public static Prompt(app: App,
118121
prompt_text?: string,
119122
default_value?: string,
120123
): Promise<string>{
121-
const latexprompt = new LaTexPrompt(app, prompt_text, default_value);
124+
const latexprompt = new this(app, prompt_text, default_value);
122125

123126
return latexprompt.waitForClose;
124127
}
@@ -138,9 +141,11 @@ export class LaTexPrompt extends FloatingModal {
138141
key:"Mod-Enter",
139142
run : () => {this.submitCallback(); return true;}
140143
}]),
144+
// obsidian class to inherit styling
145+
EditorView.editorAttributes.of({class: "multi-select-container"}),
141146
minimalSetup
142147
]
143-
if (!!this.latexsSuitePlugin) {
148+
if (this.latexsSuitePlugin) {
144149
// the language put eveything in a "math" node
145150
// surrounded by "math-begin" and "math-end"
146151
// so that latex-suite always thinks we are in mathmode

src/types/types.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ declare module "obsidian" {
8383
interface Menu {
8484
items: MenuItem[];
8585
}
86+
interface Modal {
87+
/**
88+
* Whether to *dim* the background behind the modal. If {@link dimmed} is `true`, the
89+
* opacity-value from [setBackgroundOpacity]{@link Modal#setBackgroundOpacity} or
90+
* the default of `0.85` is used.
91+
* @note The hidden backdrop will still catch focus.
92+
*/
93+
setDimBackground(dimmed: boolean): Modal;
94+
/** Sets the opacity of the Modal backdrop. */
95+
setBackgroundOpacity(opacity: number): Modal;
96+
}
8697
interface Keymap {
8798
getRootScope(): Scope;
8899
}

0 commit comments

Comments
 (0)