@proto-labs-inc/rig-core 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ /** Stable bootstrap only. Selection behavior is deployed with the Frame app. */
2
+ export declare function loadFrameTools(frameOrigin?: string): () => void;
@@ -0,0 +1,47 @@
1
+ /** Stable bootstrap only. Selection behavior is deployed with the Frame app. */
2
+ export function loadFrameTools(frameOrigin) {
3
+ if (window.parent === window)
4
+ return () => { };
5
+ let disposed = false;
6
+ let unmount;
7
+ const report = (status, origin = "*") => {
8
+ if (!disposed)
9
+ window.parent.postMessage({ type: "frame-tools-status", status }, origin);
10
+ };
11
+ const dispose = () => { disposed = true; unmount?.(); };
12
+ if (!frameOrigin) {
13
+ report("unavailable");
14
+ return dispose;
15
+ }
16
+ let origin;
17
+ try {
18
+ const url = new URL(frameOrigin);
19
+ const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
20
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
21
+ throw new Error("Invalid Frame origin");
22
+ if (url.username || url.password || url.origin !== frameOrigin)
23
+ throw new Error("Expected an origin");
24
+ origin = url.origin;
25
+ // The origin is pinned in the prototype, never taken from a message or URL parameter.
26
+ if (!document.referrer || new URL(document.referrer).origin !== origin)
27
+ return dispose;
28
+ }
29
+ catch {
30
+ report("unavailable");
31
+ return dispose;
32
+ }
33
+ report("loading", origin);
34
+ const url = new URL("/frame-tools/v1.js", origin).href;
35
+ void import(/* @vite-ignore */ url).then((runtime) => {
36
+ if (disposed)
37
+ return;
38
+ unmount = runtime.mountFrameTools({ frameOrigin: origin });
39
+ report("ready", origin);
40
+ }).catch((error) => {
41
+ if (disposed)
42
+ return;
43
+ console.warn("Proto selection tools failed to load", error);
44
+ report("unavailable", origin);
45
+ });
46
+ return dispose;
47
+ }
package/dist/index.d.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  /**
2
- * The rig's framework-free core: URL-state sync, the bridge, and comment
3
- * mode. Everything here is plain DOM + history + postMessage — no framework.
2
+ * The rig's framework-free core: URL-state sync, the bridge, and the Frame tools
3
+ * loader. Everything here is plain DOM + history + postMessage — no framework.
4
4
  * Framework adapters (@proto-labs-inc/rig for React, @proto-labs-inc/rig-vue for Vue) wrap
5
5
  * this with their own idioms and nothing else.
6
6
  */
7
7
  import { type Manifest, type ResolvedPreviewTheme, type RigToFrame } from "@proto-labs-inc/wire";
8
8
  export { STATE_PARAM, variantParam } from "@proto-labs-inc/wire";
9
9
  export declare const INTERNAL_PREVIEW_THEME_PARAM = "__protoTheme";
10
+ /** Mark the page light or dark (data-proto-theme and the "dark" class) for
11
+ * a prototype that styles both. The page's own CSS decides color-scheme:
12
+ * forcing it here gave pages that only style light mode dark form fields
13
+ * on a light page whenever the viewer's system was dark. */
10
14
  export declare function applyPreviewTheme(theme: ResolvedPreviewTheme): void;
11
15
  /** All addressable state, mirrored to the URL. Keys: "state" | "v.<component>". */
12
16
  export declare class RigStore {
package/dist/index.js CHANGED
@@ -1,21 +1,24 @@
1
1
  /**
2
- * The rig's framework-free core: URL-state sync, the bridge, and comment
3
- * mode. Everything here is plain DOM + history + postMessage — no framework.
2
+ * The rig's framework-free core: URL-state sync, the bridge, and the Frame tools
3
+ * loader. Everything here is plain DOM + history + postMessage — no framework.
4
4
  * Framework adapters (@proto-labs-inc/rig for React, @proto-labs-inc/rig-vue for Vue) wrap
5
5
  * this with their own idioms and nothing else.
6
6
  */
7
7
  import { PROTOCOL_VERSION, STATE_PARAM, variantParam, applyVariantSelection, isFrameToRig, } from "@proto-labs-inc/wire";
8
- import { ElementPreview } from "./element-preview.js";
8
+ import { loadFrameTools } from "./frame-tools-loader.js";
9
9
  export { STATE_PARAM, variantParam } from "@proto-labs-inc/wire";
10
- const RIG_VERSION = "0.1.1";
10
+ const RIG_VERSION = "0.2.1";
11
11
  export const INTERNAL_PREVIEW_THEME_PARAM = "__protoTheme";
12
+ /** Mark the page light or dark (data-proto-theme and the "dark" class) for
13
+ * a prototype that styles both. The page's own CSS decides color-scheme:
14
+ * forcing it here gave pages that only style light mode dark form fields
15
+ * on a light page whenever the viewer's system was dark. */
12
16
  export function applyPreviewTheme(theme) {
13
17
  if (typeof document === "undefined")
14
18
  return;
15
19
  const root = document.documentElement;
16
20
  root.dataset.protoTheme = theme;
17
21
  root.classList.toggle("dark", theme === "dark");
18
- root.style.colorScheme = theme;
19
22
  }
20
23
  function resolvedSystemTheme() {
21
24
  return window.matchMedia("(prefers-color-scheme: dark)").matches
@@ -131,48 +134,6 @@ export function postToFrame(msg) {
131
134
  if (typeof window !== "undefined" && window.parent !== window)
132
135
  window.parent.postMessage(msg, "*");
133
136
  }
134
- // ─── comment mode ──────────────────────────────────────────────────
135
- // The Frame can't touch this document (cross-origin iframe), so the blue
136
- // selector runs here: hit-test [data-proto-id] under the cursor, draw the
137
- // highlight, and on click capture the view with the highlight baked in.
138
- // The highlight is Proto blue (#0088FF): 2px solid at 55%, a 6% fill,
139
- // radius 4px, 0.12s ease-out entry; the picker layer sits at z-index 99997.
140
- const COMMENT_BLUE = "#0088FF";
141
- const TARGET_ATTR = "data-proto-id";
142
- function selectorFor(element) {
143
- if (element === document.documentElement)
144
- return "html";
145
- if (element === document.body)
146
- return "body";
147
- const protoId = element.getAttribute(TARGET_ATTR);
148
- if (protoId)
149
- return `[${TARGET_ATTR}="${CSS.escape(protoId)}"]`;
150
- if (element.id) {
151
- const idSelector = `#${CSS.escape(element.id)}`;
152
- if (document.querySelectorAll(idSelector).length === 1)
153
- return idSelector;
154
- }
155
- const owner = element.closest(`[${TARGET_ATTR}]`);
156
- const boundary = owner ?? document.body;
157
- const segments = [];
158
- let current = element;
159
- while (current && current !== boundary) {
160
- const tag = current.tagName.toLowerCase();
161
- const currentTagName = current.tagName;
162
- const siblings = current.parentElement
163
- ? [...current.parentElement.children].filter((sibling) => sibling.tagName === currentTagName)
164
- : [];
165
- const index = siblings.indexOf(current);
166
- segments.unshift(siblings.length > 1 && index >= 0 ? `${tag}:nth-of-type(${index + 1})` : tag);
167
- current = current.parentElement;
168
- }
169
- const base = owner
170
- ? `[${TARGET_ATTR}="${CSS.escape(owner.getAttribute(TARGET_ATTR) ?? "")}"]`
171
- : boundary === element
172
- ? element.tagName.toLowerCase()
173
- : "body";
174
- return segments.length > 0 ? `${base} > ${segments.join(" > ")}` : base;
175
- }
176
137
  async function waitForCaptureReady() {
177
138
  const pendingImages = [...document.images]
178
139
  .filter((image) => !image.complete)
@@ -190,47 +151,6 @@ async function waitForCaptureReady() {
190
151
  // resting state before the gallery captures them.
191
152
  await new Promise((resolve) => window.setTimeout(resolve, 1500));
192
153
  }
193
- async function captureViewport(scale = Math.min(window.devicePixelRatio || 1, 2)) {
194
- try {
195
- // Optional peer provided by the prototype, not the rig.
196
- const mod = (await import("modern-screenshot"));
197
- const root = document.body;
198
- const viewportWidth = document.documentElement.clientWidth;
199
- const viewportHeight = document.documentElement.clientHeight;
200
- const scrollX = window.scrollX || document.documentElement.scrollLeft;
201
- const scrollY = window.scrollY || document.documentElement.scrollTop;
202
- const contentWidth = Math.max(viewportWidth, document.documentElement.scrollWidth, root.scrollWidth);
203
- const contentHeight = Math.max(viewportHeight, document.documentElement.scrollHeight, root.scrollHeight);
204
- return await mod.domToDataUrl(root, {
205
- scale,
206
- width: viewportWidth,
207
- height: viewportHeight,
208
- features: { restoreScrollPosition: true },
209
- style: {
210
- margin: "0",
211
- width: `${contentWidth}px`,
212
- height: `${contentHeight}px`,
213
- transform: `translate(${-scrollX}px, ${-scrollY}px)`,
214
- transformOrigin: "top left",
215
- overflow: "visible",
216
- },
217
- onCloneNode: (cloned) => {
218
- if (!(cloned instanceof HTMLElement) || (!scrollX && !scrollY))
219
- return;
220
- const fixed = [cloned, ...cloned.querySelectorAll("*")].filter((element) => element.style.position === "fixed");
221
- for (const element of fixed) {
222
- const existing = element.style.transform;
223
- element.style.transform =
224
- `translate(${scrollX}px, ${scrollY}px)` +
225
- (existing && existing !== "none" ? ` ${existing}` : "");
226
- }
227
- },
228
- });
229
- }
230
- catch {
231
- return null;
232
- }
233
- }
234
154
  async function captureThumbnail() {
235
155
  const restingState = document.createElement("style");
236
156
  restingState.textContent = `
@@ -277,229 +197,13 @@ async function captureThumbnail() {
277
197
  restingState.remove();
278
198
  }
279
199
  }
280
- const elementPreview = new ElementPreview(postToFrame);
281
- class ElementPicker {
282
- overlay = null;
283
- highlight = null;
284
- cursorStyle = null;
285
- target = null;
286
- active = false;
287
- mode = null;
288
- enable(mode) {
289
- if (typeof document === "undefined")
290
- return;
291
- elementPreview.release();
292
- if (this.active || this.overlay)
293
- this.disable();
294
- this.active = true;
295
- this.mode = mode;
296
- this.cursorStyle = document.createElement("style");
297
- this.cursorStyle.setAttribute("data-proto-picker-cursor", "");
298
- this.cursorStyle.textContent =
299
- "html, body, body * { cursor: crosshair !important; }";
300
- document.head.appendChild(this.cursorStyle);
301
- const blue = COMMENT_BLUE;
302
- this.overlay = document.createElement("div");
303
- this.overlay.setAttribute("data-proto-rig-overlay", "");
304
- Object.assign(this.overlay.style, {
305
- position: "fixed",
306
- inset: "0",
307
- zIndex: "99997",
308
- pointerEvents: "none",
309
- });
310
- this.highlight = document.createElement("div");
311
- Object.assign(this.highlight.style, {
312
- position: "absolute",
313
- border: `2px solid color-mix(in srgb, ${blue} 55%, transparent)`,
314
- borderRadius: "4px",
315
- backgroundColor: `color-mix(in srgb, ${blue} 6%, transparent)`,
316
- pointerEvents: "none",
317
- boxSizing: "border-box",
318
- opacity: "0",
319
- transition: "opacity 0.12s ease-out",
320
- willChange: "opacity",
321
- });
322
- this.overlay.appendChild(this.highlight);
323
- document.body.appendChild(this.overlay);
324
- document.addEventListener("pointermove", this.onMove, true);
325
- document.addEventListener("click", this.onClick, true);
326
- document.addEventListener("keydown", this.onKey, true);
327
- }
328
- disable() {
329
- if (!this.active && !this.overlay && !this.cursorStyle)
330
- return;
331
- this.active = false;
332
- document.removeEventListener("pointermove", this.onMove, true);
333
- document.removeEventListener("click", this.onClick, true);
334
- document.removeEventListener("keydown", this.onKey, true);
335
- this.overlay?.remove();
336
- this.cursorStyle?.remove();
337
- this.overlay = null;
338
- this.highlight = null;
339
- this.cursorStyle = null;
340
- this.target = null;
341
- this.mode = null;
342
- }
343
- freezeSelection() {
344
- this.active = false;
345
- document.removeEventListener("pointermove", this.onMove, true);
346
- document.removeEventListener("click", this.onClick, true);
347
- document.removeEventListener("keydown", this.onKey, true);
348
- this.cursorStyle?.remove();
349
- this.cursorStyle = null;
350
- }
351
- onMove = (e) => {
352
- const hit = this.targetAt(e.clientX, e.clientY);
353
- if (hit === this.target)
354
- return;
355
- this.target = hit;
356
- if (!this.highlight)
357
- return;
358
- const rect = this.rectFor(hit);
359
- Object.assign(this.highlight.style, {
360
- left: `${rect.left}px`,
361
- top: `${rect.top}px`,
362
- width: `${rect.width}px`,
363
- height: `${rect.height}px`,
364
- opacity: "1",
365
- });
366
- };
367
- onClick = (e) => {
368
- e.preventDefault();
369
- e.stopPropagation();
370
- const clickedTarget = this.targetAt(e.clientX, e.clientY);
371
- this.target = clickedTarget;
372
- const targetId = clickedTarget?.getAttribute(TARGET_ATTR) ?? null;
373
- if (this.mode === "element") {
374
- const rect = this.rectFor(clickedTarget);
375
- const text = clickedTarget.textContent?.replace(/\s+/g, " ").trim() || null;
376
- const inspection = elementPreview.inspect(clickedTarget);
377
- this.disable();
378
- postToFrame({
379
- type: "element-target-picked",
380
- inspection,
381
- selector: selectorFor(clickedTarget),
382
- tagName: clickedTarget.tagName.toLowerCase(),
383
- text: text?.slice(0, 160) ?? null,
384
- anchor: {
385
- x: rect.left,
386
- y: rect.top,
387
- width: rect.width,
388
- height: rect.height,
389
- },
390
- });
391
- return;
392
- }
393
- if (this.mode === "variantSet") {
394
- if (!targetId)
395
- return;
396
- const rect = this.rectFor(clickedTarget);
397
- this.disable();
398
- postToFrame({
399
- type: "variant-set-target-picked",
400
- targetId,
401
- anchor: {
402
- x: rect.left,
403
- y: rect.top,
404
- width: rect.width,
405
- height: rect.height,
406
- },
407
- });
408
- return;
409
- }
410
- const rect = this.rectFor(clickedTarget);
411
- if (this.highlight) {
412
- Object.assign(this.highlight.style, {
413
- left: `${rect.left}px`,
414
- top: `${rect.top}px`,
415
- width: `${rect.width}px`,
416
- height: `${rect.height}px`,
417
- opacity: "1",
418
- });
419
- }
420
- this.freezeSelection();
421
- postToFrame({
422
- type: "comment-target-picked",
423
- targetId,
424
- anchor: {
425
- x: rect.left,
426
- y: rect.top,
427
- width: rect.width,
428
- height: rect.height,
429
- },
430
- });
431
- };
432
- targetAt(x, y) {
433
- const hit = document.elementFromPoint(x, y);
434
- if (this.mode === "element")
435
- return hit ?? document.body;
436
- return hit?.closest(`[${TARGET_ATTR}]`) ?? document.body;
437
- }
438
- rectFor(target) {
439
- if (target === document.body || target === document.documentElement) {
440
- return {
441
- left: 0,
442
- top: 0,
443
- width: document.documentElement.clientWidth,
444
- height: document.documentElement.clientHeight,
445
- };
446
- }
447
- return target.getBoundingClientRect();
448
- }
449
- onKey = (e) => {
450
- if (e.key !== "Escape")
451
- return;
452
- e.preventDefault();
453
- const mode = this.mode;
454
- this.disable();
455
- postToFrame({
456
- type: mode === "variantSet"
457
- ? "variant-set-picker-exited"
458
- : mode === "element"
459
- ? "element-picker-exited"
460
- : "comment-mode-exited",
461
- });
462
- };
463
- async captureSelection(requestId) {
464
- if (!this.target)
465
- return;
466
- const targetId = this.target.getAttribute(TARGET_ATTR);
467
- const rect = this.rectFor(this.target);
468
- const anchor = {
469
- x: rect.left,
470
- y: rect.top,
471
- width: rect.width,
472
- height: rect.height,
473
- };
474
- if (this.highlight) {
475
- Object.assign(this.highlight.style, {
476
- left: `${rect.left}px`,
477
- top: `${rect.top}px`,
478
- width: `${rect.width}px`,
479
- height: `${rect.height}px`,
480
- });
481
- }
482
- // The page is already interactive when the user picks a comment target.
483
- // Capture immediately: thumbnail readiness delays can consume the Frame's
484
- // request timeout on image-heavy prototypes.
485
- const image = await captureViewport();
486
- this.disable();
487
- postToFrame({
488
- type: "comment-capture",
489
- targetId,
490
- image,
491
- requestId,
492
- anchor,
493
- });
494
- }
495
- }
496
- const elementPicker = typeof window === "undefined" ? null : new ElementPicker();
497
200
  /**
498
201
  * Wire a store to the bridge: listen for Frame messages, announce the rig,
499
202
  * and report the boot-time state (the deep-link fix). Returns a disposer.
500
203
  * Call client-side only.
501
204
  */
502
205
  export function wireRig(store, manifest) {
206
+ const disposeTools = loadFrameTools(manifest.frameOrigin);
503
207
  let controlledTheme = false;
504
208
  const media = window.matchMedia("(prefers-color-scheme: dark)");
505
209
  const onSystemTheme = () => {
@@ -512,16 +216,6 @@ export function wireRig(store, manifest) {
512
216
  return;
513
217
  if (!isFrameToRig(e.data))
514
218
  return; // tolerate unknown messages silently
515
- if (e.data.type === "preview-element-styles") {
516
- elementPreview.preview(e.data.selectionId, e.data.styles);
517
- return;
518
- }
519
- if (e.data.type === "release-element") {
520
- elementPreview.release(e.data.selectionId);
521
- return;
522
- }
523
- if (["set-state", "set-variant", "set-theme"].includes(e.data.type))
524
- elementPreview.release();
525
219
  if (e.data.type === "set-state") {
526
220
  const state = manifest.states.find((s) => s.id === e.data.id);
527
221
  store.setState(e.data.id, state?.variants ?? {});
@@ -532,42 +226,6 @@ export function wireRig(store, manifest) {
532
226
  }
533
227
  else if (e.data.type === "set-variant")
534
228
  store.set(variantParam(e.data.component), e.data.variant);
535
- else if (e.data.type === "measure-target") {
536
- const target = Array.from(document.querySelectorAll(`[${TARGET_ATTR}]`)).find((candidate) => candidate.getAttribute(TARGET_ATTR) === e.data.id);
537
- const rect = target?.getBoundingClientRect();
538
- postToFrame({
539
- type: "target-measured",
540
- targetId: e.data.id,
541
- anchor: rect
542
- ? {
543
- x: rect.left,
544
- y: rect.top,
545
- width: rect.width,
546
- height: rect.height,
547
- }
548
- : null,
549
- });
550
- }
551
- else if (e.data.type === "set-comment-mode") {
552
- if (e.data.on)
553
- elementPicker?.enable("comment");
554
- else
555
- elementPicker?.disable();
556
- }
557
- else if (e.data.type === "capture-comment")
558
- void elementPicker?.captureSelection(e.data.requestId);
559
- else if (e.data.type === "set-variant-set-picker") {
560
- if (e.data.on)
561
- elementPicker?.enable("variantSet");
562
- else
563
- elementPicker?.disable();
564
- }
565
- else if (e.data.type === "set-element-picker") {
566
- if (e.data.on)
567
- elementPicker?.enable("element");
568
- else
569
- elementPicker?.disable();
570
- }
571
229
  };
572
230
  window.addEventListener("message", onMessage);
573
231
  postToFrame({
@@ -590,7 +248,6 @@ export function wireRig(store, manifest) {
590
248
  return () => {
591
249
  media.removeEventListener("change", onSystemTheme);
592
250
  window.removeEventListener("message", onMessage);
593
- elementPicker?.disable();
594
- elementPreview.release();
251
+ disposeTools();
595
252
  };
596
253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proto-labs-inc/rig-core",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Proto's rig, framework-free: URL-state sync, the postMessage bridge to the Frame, and comment mode.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  }
29
29
  },
30
30
  "dependencies": {
31
- "@proto-labs-inc/wire": "^0.1.1"
31
+ "@proto-labs-inc/wire": "^0.2.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "typescript": "~6.0.2",
@@ -1,20 +0,0 @@
1
- import { type ElementStyles, type RigToFrame } from "@proto-labs-inc/wire";
2
- /** Owns one temporary edit. Never resolves a selector back to a different element. */
3
- export declare class ElementPreview {
4
- private post;
5
- private session;
6
- private observer;
7
- private resize;
8
- private animationFrame;
9
- private lastBounds;
10
- constructor(post: (message: RigToFrame) => void);
11
- inspect(target: Element): {
12
- selectionId: `${string}-${string}-${string}-${string}-${string}`;
13
- styles: Partial<Record<"color" | "background-color" | "opacity" | "font-family" | "font-size" | "font-weight" | "line-height" | "letter-spacing" | "padding-top" | "padding-right" | "padding-bottom" | "padding-left" | "border-radius" | "gap", string>>;
14
- } | undefined;
15
- preview(id: string, styles: ElementStyles): void;
16
- private restore;
17
- release(id?: string): void;
18
- private scheduleBounds;
19
- private measure;
20
- }
@@ -1,95 +0,0 @@
1
- import { ELEMENT_STYLE_PROPERTIES } from "@proto-labs-inc/wire";
2
- /** Owns one temporary edit. Never resolves a selector back to a different element. */
3
- export class ElementPreview {
4
- post;
5
- session = null;
6
- observer = null;
7
- resize = null;
8
- animationFrame = 0;
9
- lastBounds = "";
10
- constructor(post) {
11
- this.post = post;
12
- }
13
- inspect(target) {
14
- this.release();
15
- if (!(target instanceof HTMLElement || target instanceof SVGElement))
16
- return undefined;
17
- const computed = getComputedStyle(target);
18
- const hasText = Array.from(target.childNodes).some((node) => node.nodeType === Node.TEXT_NODE && Boolean(node.textContent?.trim())) || target.matches("input, textarea, select");
19
- const typography = new Set(["color", "font-family", "font-size", "font-weight", "line-height", "letter-spacing"]);
20
- const styles = {};
21
- for (const property of ELEMENT_STYLE_PROPERTIES) {
22
- if (typography.has(property) && !hasText)
23
- continue;
24
- if (property === "gap" && !/flex|grid/.test(computed.display))
25
- continue;
26
- styles[property] = computed.getPropertyValue(property);
27
- }
28
- const selectionId = crypto.randomUUID();
29
- this.session = { id: selectionId, target, original: target.getAttribute("style") };
30
- this.lastBounds = "";
31
- this.observer = new MutationObserver(this.scheduleBounds);
32
- this.observer.observe(document.body, { childList: true, subtree: true, attributes: true });
33
- this.resize = new ResizeObserver(this.scheduleBounds);
34
- this.resize.observe(target);
35
- window.addEventListener("scroll", this.scheduleBounds, true);
36
- window.addEventListener("resize", this.scheduleBounds);
37
- return { selectionId, styles };
38
- }
39
- preview(id, styles) {
40
- const session = this.session;
41
- if (!session || session.id !== id)
42
- return;
43
- this.restore();
44
- for (const property of ELEMENT_STYLE_PROPERTIES) {
45
- const value = styles[property];
46
- if (value && CSS.supports(property, value))
47
- session.target.style.setProperty(property, value, "important");
48
- }
49
- this.scheduleBounds();
50
- }
51
- restore() {
52
- if (!this.session)
53
- return;
54
- const { target, original } = this.session;
55
- if (original === null)
56
- target.removeAttribute("style");
57
- else
58
- target.setAttribute("style", original);
59
- }
60
- release(id) {
61
- if (!this.session || (id && id !== this.session.id))
62
- return;
63
- const selectionId = this.session.id;
64
- this.observer?.disconnect();
65
- this.resize?.disconnect();
66
- window.removeEventListener("scroll", this.scheduleBounds, true);
67
- window.removeEventListener("resize", this.scheduleBounds);
68
- cancelAnimationFrame(this.animationFrame);
69
- this.animationFrame = 0;
70
- this.restore();
71
- this.session = null;
72
- this.post({ type: "element-preview-bounds", selectionId, anchor: null });
73
- }
74
- scheduleBounds = () => {
75
- if (!this.animationFrame)
76
- this.animationFrame = requestAnimationFrame(this.measure);
77
- };
78
- measure = () => {
79
- this.animationFrame = 0;
80
- if (!this.session)
81
- return;
82
- const { target, id } = this.session;
83
- if (!target.isConnected) {
84
- this.release();
85
- return;
86
- }
87
- const rect = target.getBoundingClientRect();
88
- const anchor = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
89
- const bounds = JSON.stringify(anchor);
90
- if (bounds === this.lastBounds)
91
- return;
92
- this.lastBounds = bounds;
93
- this.post({ type: "element-preview-bounds", selectionId: id, anchor });
94
- };
95
- }