@brightspot/ui 6.4.2 → 6.4.3-pr.366.2026.10.5

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.
Files changed (91) hide show
  1. package/dist/custom-elements.json +499 -499
  2. package/dist/storybook/assets/{ActionBar.stories-S00Ek6J4.js → ActionBar.stories-0ScNWOC4.js} +1 -1
  3. package/dist/storybook/assets/{ActionItem.stories-D4F9yCyV.js → ActionItem.stories-DXJ_yKW3.js} +1 -1
  4. package/dist/storybook/assets/{AiSparkle.stories-vC8jum9o.js → AiSparkle.stories-Z_5khRM7.js} +1 -1
  5. package/dist/storybook/assets/{Avatar.stories-BvCzPIfr.js → Avatar.stories-C9XcQNHg.js} +1 -1
  6. package/dist/storybook/assets/{AvatarGroup.stories-VRTsctUo.js → AvatarGroup.stories-Cdy_mkeq.js} +1 -1
  7. package/dist/storybook/assets/{Badge.stories-DPaTwNDd.js → Badge.stories-BUdXSprE.js} +1 -1
  8. package/dist/storybook/assets/{Banner.stories-Ki6o6Qnz.js → Banner.stories-D71fgNb5.js} +1 -1
  9. package/dist/storybook/assets/{Button-BVkUpjw-.js → Button-D7pK-2at.js} +1 -1
  10. package/dist/storybook/assets/{Button.stories-B4Cajufa.js → Button.stories-B6WKuxXx.js} +1 -1
  11. package/dist/storybook/assets/{ButtonGroup.stories--XdEhkoR.js → ButtonGroup.stories-CwiRfEal.js} +1 -1
  12. package/dist/storybook/assets/{Card.stories-DdaTXloZ.js → Card.stories-CxCcO9k6.js} +1 -1
  13. package/dist/storybook/assets/{Celebrate.stories-5krJLBnV.js → Celebrate.stories-DNVFUrhg.js} +1 -1
  14. package/dist/storybook/assets/{Checkbox.stories-DJiir-4S.js → Checkbox.stories-SuP4ltMI.js} +1 -1
  15. package/dist/storybook/assets/{CircularProgress.stories-FoLbJ02D.js → CircularProgress.stories-BB0twcGv.js} +1 -1
  16. package/dist/storybook/assets/{ClipboardMixin.stories-C_Y93Vvc.js → ClipboardMixin.stories-DaPV19jG.js} +1 -1
  17. package/dist/storybook/assets/{Color-6BZIO3FS-Cal-f26G.js → Color-6BZIO3FS-DQ34tmQu.js} +1 -1
  18. package/dist/storybook/assets/{Colors.stories-BIOYpAZL.js → Colors.stories-CMFJai_a.js} +1 -1
  19. package/dist/storybook/assets/{CombinedEffects.stories-z1_xU5i7.js → CombinedEffects.stories-BIHfDsN0.js} +1 -1
  20. package/dist/storybook/assets/{ComponentStatesMixin-DCnHVb7T.js → ComponentStatesMixin-Bq-OGuus.js} +1 -1
  21. package/dist/storybook/assets/{ComponentStatesMixin.stories-Crysvxtr.js → ComponentStatesMixin.stories-DMxDqidh.js} +1 -1
  22. package/dist/storybook/assets/{CopyToClipboard.stories-CMpzwyh6.js → CopyToClipboard.stories-CkdYHm8U.js} +1 -1
  23. package/dist/storybook/assets/{Debounce.stories-C2FgBZXP.js → Debounce.stories-CZSMfS-h.js} +1 -1
  24. package/dist/storybook/assets/{DocsRenderer-LL677BLK-CxHYlXfz.js → DocsRenderer-LL677BLK-CCAvPHmY.js} +3 -3
  25. package/dist/storybook/assets/{Dropdown.stories-DhBn1nVs.js → Dropdown.stories-BTqSRQiC.js} +1 -1
  26. package/dist/storybook/assets/{EmptyState.stories-CBIvJ1Tk.js → EmptyState.stories-CjIkNVv4.js} +1 -1
  27. package/dist/storybook/assets/{EscaAvatar.stories-DKPhkhvA.js → EscaAvatar.stories-ORFCzxQw.js} +1 -1
  28. package/dist/storybook/assets/{Events.stories-DLbfdSXr.js → Events.stories-DHFT6dZM.js} +1 -1
  29. package/dist/storybook/assets/{Heading.stories-B6CIwwiJ.js → Heading.stories-CRgKl3Ve.js} +1 -1
  30. package/dist/storybook/assets/{HueRipple.stories-C8E72qX_.js → HueRipple.stories-YiQHrWPS.js} +1 -1
  31. package/dist/storybook/assets/{Icon.stories-YUubIb9i.js → Icon.stories-Bv4QRZNL.js} +1 -1
  32. package/dist/storybook/assets/{IconButton.stories-07ADapHQ.js → IconButton.stories-ErFc860x.js} +1 -1
  33. package/dist/storybook/assets/{LinearProgress.stories-DZk0i6LC.js → LinearProgress.stories-DUAAMzdt.js} +1 -1
  34. package/dist/storybook/assets/{Message.stories-BaTncCPY.js → Message.stories-DxJEEd1I.js} +1 -1
  35. package/dist/storybook/assets/{OnFindMixin.stories-BKgCI6zb.js → OnFindMixin.stories-CoKUEhb4.js} +1 -1
  36. package/dist/storybook/assets/{Pagination.stories-BrD-TcnC.js → Pagination.stories-6k02-Zb5.js} +1 -1
  37. package/dist/storybook/assets/{Popover.stories-CUe375fR.js → Popover.stories-Cd7P1nw_.js} +1 -1
  38. package/dist/storybook/assets/{ReadyMixin-XQ_tHzRU.js → ReadyMixin-BL65bbm_.js} +1 -1
  39. package/dist/storybook/assets/{RovingTabindexMixin.stories-BBc7zdjl.js → RovingTabindexMixin.stories-CvtQ9Om6.js} +1 -1
  40. package/dist/storybook/assets/{Rtc.stories-CkEgMrqd.js → Rtc.stories-BBmO6Ses.js} +1 -1
  41. package/dist/storybook/assets/{ScrollShadow.stories-DdOKLFJ5.js → ScrollShadow.stories-BUiypzkp.js} +1 -1
  42. package/dist/storybook/assets/{Switch.stories-Dw9Jic7Y.js → Switch.stories-BndbX0N7.js} +1 -1
  43. package/dist/storybook/assets/{Tab.stories-CUAvvpwI.js → Tab.stories-DVMyUFjq.js} +1 -1
  44. package/dist/storybook/assets/{Tabs.stories--iRj2mWu.js → Tabs.stories-BQPYcQ6j.js} +1 -1
  45. package/dist/storybook/assets/{Throttle.stories-BDuNJMQg.js → Throttle.stories-CKYOCVnj.js} +1 -1
  46. package/dist/storybook/assets/{Tooltip.stories-RaSZ71_I.js → Tooltip.stories-B3DmBl8X.js} +1 -1
  47. package/dist/storybook/assets/{Upload.stories-hhhR_V85.js → Upload.stories-nzkKN-_d.js} +1 -1
  48. package/dist/storybook/assets/{UploadItem.stories-B-1lkPzO.js → UploadItem.stories-CL3oKT8D.js} +1 -1
  49. package/dist/storybook/assets/{Welcome.stories-BycQWbzd.js → Welcome.stories-DfHnb5cA.js} +1 -1
  50. package/dist/storybook/assets/{Widget.stories-BDcpbQXU.js → Widget.stories-CcVwc0wv.js} +1 -1
  51. package/dist/storybook/assets/{WithTooltip-65CFNBJE-CHL0JiUq.js → WithTooltip-65CFNBJE-CxSJYFKf.js} +1 -1
  52. package/dist/storybook/assets/{blocks-DllEBdtP.js → blocks-A7XjcsrR.js} +5 -5
  53. package/dist/storybook/assets/{formatter-EIJCOSYU-LrYXshB9.js → formatter-EIJCOSYU-BJ6Z7p9H.js} +1 -1
  54. package/dist/storybook/assets/if-defined-C9mNa1xD.js +1 -0
  55. package/dist/storybook/assets/{iframe-Bp7-_nUZ.js → iframe-ChxxW2FZ.js} +4 -4
  56. package/dist/storybook/assets/iframe-DI_vgyKl.css +1 -0
  57. package/dist/storybook/assets/{index-Cc3M657Z.js → index-ChSJHRK6.js} +1 -1
  58. package/dist/storybook/assets/{onFind-Du4z1ObO.js → onFind-C-gvwlyW.js} +1 -1
  59. package/dist/storybook/assets/{onFind.stories-BCUiC6a9.js → onFind.stories-8tPEGWhS.js} +1 -1
  60. package/dist/storybook/assets/{onRemove.stories-B-ICGMhl.js → onRemove.stories-CSuoNbAk.js} +1 -1
  61. package/dist/storybook/assets/{onVisible.stories-DijYge1X.js → onVisible.stories-LFpM1FnE.js} +1 -1
  62. package/dist/storybook/assets/{style-map-DaohL-ne.js → style-map-4fqj8G4H.js} +1 -1
  63. package/dist/storybook/assets/{syntaxhighlighter-ED5Y7EFY-CH5_k0Po.js → syntaxhighlighter-ED5Y7EFY-DCcLcHdT.js} +1 -1
  64. package/dist/storybook/iframe.html +2 -2
  65. package/dist/storybook/project.json +1 -1
  66. package/dist/tailwind-plugin-badge.js +2 -1
  67. package/dist/tailwind-plugin-badge.js.map +1 -1
  68. package/dist/tailwind-plugin-badge.ts +2 -1
  69. package/dist/tailwind-plugin-message.js +3 -0
  70. package/dist/tailwind-plugin-message.js.map +1 -1
  71. package/dist/tailwind-plugin-message.ts +3 -0
  72. package/package.json +1 -1
  73. package/src/legacy/tool-ui/src/Conversation.css +14 -3
  74. package/src/legacy/tool-ui/src/Conversation.ts +3 -0
  75. package/src/legacy/tool-ui/src/Entry.ts +4 -3
  76. package/src/legacy/tool-ui/src/SharePreview.css +209 -4
  77. package/src/legacy/tool-ui/src/SharePreviewCommenting.ts +659 -0
  78. package/src/legacy/tool-ui/src/main/webapp/dist/LocationMap.8c328ae48113c22fec31.js +1 -0
  79. package/src/legacy/tool-ui/src/main/webapp/dist/{RTEProseMirror.ef21fd86df209b8f772b.js → RTEProseMirror.e4cbe3b9e2e8f938cf1a.js} +5 -5
  80. package/src/legacy/tool-ui/src/main/webapp/dist/RegionMap.17d7cd9f92c923b593d5.js +1 -0
  81. package/src/legacy/tool-ui/src/main/webapp/dist/{v4.83bc23585a616647b793.js → v4.56e25b6f00e10789ff37.js} +8 -8
  82. package/src/legacy/tool-ui/src/main/webapp/dist/v5.22ce1a2b135f2c813878.css +5 -0
  83. package/src/legacy/tool-ui/src/main/webapp/dist/{v5.dae3cbc9fb1335ca0f1f.js → v5.85bedd855e1e6f2c2caa.js} +6 -6
  84. package/dist/storybook/assets/if-defined-DONC-k_n.js +0 -1
  85. package/dist/storybook/assets/iframe-D7jBrFjV.css +0 -1
  86. package/src/legacy/tool-ui/src/main/webapp/dist/LocationMap.72a510c0f5ff236f596e.js +0 -1
  87. package/src/legacy/tool-ui/src/main/webapp/dist/RegionMap.f6f963eda2cb616b9661.js +0 -1
  88. package/src/legacy/tool-ui/src/main/webapp/dist/v5.d87bbcc711f912ae68db.css +0 -5
  89. /package/src/legacy/tool-ui/src/main/webapp/dist/{RTEProseMirror.ef21fd86df209b8f772b.js.LICENSE.txt → RTEProseMirror.e4cbe3b9e2e8f938cf1a.js.LICENSE.txt} +0 -0
  90. /package/src/legacy/tool-ui/src/main/webapp/dist/{v4.83bc23585a616647b793.js.LICENSE.txt → v4.56e25b6f00e10789ff37.js.LICENSE.txt} +0 -0
  91. /package/src/legacy/tool-ui/src/main/webapp/dist/{v5.dae3cbc9fb1335ca0f1f.js.LICENSE.txt → v5.85bedd855e1e6f2c2caa.js.LICENSE.txt} +0 -0
@@ -0,0 +1,659 @@
1
+ import onFind from './dom/onFind'
2
+ import onRTEReady from './dom/onRTEReady'
3
+ import Cookies from 'js-cookie'
4
+ import rtc from './rtc'
5
+ import Tether from './dom/Tether'
6
+
7
+ interface PinData {
8
+ id: string
9
+ xPercent: number
10
+ yPercent: number
11
+ initials: string
12
+ colorIndex: number
13
+ }
14
+
15
+ interface PostBroadcastData {
16
+ contentId: string
17
+ }
18
+
19
+ interface ScrollState {
20
+ scrollX: number
21
+ scrollY: number
22
+ docWidth: number
23
+ docHeight: number
24
+ }
25
+
26
+ interface PendingClick {
27
+ clientX: number
28
+ clientY: number
29
+ overlayRect: DOMRect
30
+ previewId: string
31
+ contentId: string
32
+ overlayId: string
33
+ }
34
+
35
+ interface PinPopoverOptions {
36
+ clientX: number
37
+ clientY: number
38
+ xPercent: number
39
+ yPercent: number
40
+ contentId: string
41
+ overlayId: string
42
+ previewId: string
43
+ overlay: HTMLElement
44
+ onSuccess: () => void
45
+ }
46
+
47
+ // If the preview never reports scroll data (e.g. its CSP blocks the injected
48
+ // script), fall back to a same-origin read after this delay so pins still render.
49
+ const SCROLL_DATA_FALLBACK_MS = 1500
50
+
51
+ // ─── Panel toggle ────────────────────────────────────────────────────────────
52
+
53
+ onFind('.SharePreview-toggle', (toggle: HTMLElement) => {
54
+ const sharePreview = toggle.closest<HTMLElement>('.SharePreview')
55
+ if (!sharePreview) return
56
+
57
+ // Lazy-init the pin overlay on the first open. The toggle button lives inside
58
+ // the XHR-loaded frame, so onFind fires only after that frame is fully loaded —
59
+ // meaning the preview iframe and pinsOverlay are already in the DOM.
60
+ let overlayReady = false
61
+
62
+ function maybeInit() {
63
+ if (overlayReady) return
64
+ overlayReady = true
65
+ initCommentingOverlay(sharePreview!)
66
+ }
67
+
68
+ // If the page loaded with commenting already open (e.g. from a shared preview
69
+ // comment link), init now — the frame is already loaded at this point.
70
+ if (sharePreview.classList.contains('is-commenting-open')) {
71
+ maybeInit()
72
+ }
73
+
74
+ toggle.addEventListener('click', () => {
75
+ const isOpen = sharePreview.classList.toggle('is-commenting-open')
76
+ toggle.setAttribute('aria-pressed', String(isOpen))
77
+
78
+ if (isOpen) {
79
+ maybeInit()
80
+ requestAnimationFrame(() => {
81
+ const postsContainer = sharePreview.querySelector<HTMLElement>(
82
+ '.Conversation-posts',
83
+ )
84
+ if (postsContainer) {
85
+ postsContainer.scrollTop = postsContainer.scrollHeight
86
+ }
87
+ })
88
+ }
89
+ })
90
+ })
91
+
92
+ onFind('.SharePreview-panel-close', (closeButton: HTMLElement) => {
93
+ const sharePreview = closeButton.closest<HTMLElement>('.SharePreview')
94
+ if (!sharePreview) return
95
+
96
+ closeButton.addEventListener('click', () => {
97
+ sharePreview.classList.remove('is-commenting-open')
98
+ const toggle = sharePreview.querySelector<HTMLElement>(
99
+ '.SharePreview-toggle',
100
+ )
101
+ toggle?.setAttribute('aria-pressed', 'false')
102
+ toggle?.focus()
103
+ })
104
+ })
105
+
106
+ // When opened via a Shared Preview comment link (commentingOpen=true + #postId hash),
107
+ // wait for the conversation to load then scroll the target post into view.
108
+ onFind('.SharePreview.is-commenting-open', () => {
109
+ const hash = window.location.hash.slice(1)
110
+ if (!hash) return
111
+
112
+ // Guard against CSS selector injection — post IDs are UUIDs (hex + hyphens only)
113
+ if (!/^[a-f0-9-]+$/i.test(hash)) return
114
+
115
+ onFind(`[id="${hash}"]`, (post: HTMLElement) => {
116
+ post.scrollIntoView({ behavior: 'smooth', block: 'center' })
117
+ })
118
+ })
119
+
120
+ // ─── Pin overlay setup (lazy, called on first toggle open) ───────────────────
121
+ //
122
+ // The preview iframe may be cross-origin, so we cannot read its scroll position
123
+ // or intercept pointer events inside it. A same-origin click overlay sits above
124
+ // it to capture wheel/click, and PageFilter injects a script into the preview
125
+ // page that postMessages its scroll position to us.
126
+
127
+ function initCommentingOverlay(sharePreview: HTMLElement) {
128
+ const iframeEl = sharePreview.querySelector<HTMLIFrameElement>(
129
+ 'iframe[name^="PreviewFrame-preview"]',
130
+ )
131
+ if (!iframeEl) return
132
+ const iframe: HTMLIFrameElement = iframeEl
133
+
134
+ const overlayEl = sharePreview.querySelector<HTMLElement>(
135
+ '.SharePreview-pinsOverlay',
136
+ )
137
+ if (!overlayEl) return
138
+ const overlay: HTMLElement = overlayEl
139
+
140
+ let cachedPins: PinData[] = []
141
+
142
+ // previewId from the page URL — always available on /preview/share-view?previewId=...
143
+ const urlPreviewId =
144
+ new URLSearchParams(window.location.search).get('previewId') || ''
145
+
146
+ // Scroll state of the preview document, reported by the injected script via
147
+ // postMessage. Pin coordinates are stored as fractions of the document size, so
148
+ // we wait for the first real ScrollData (hasScrollData) before rendering to
149
+ // avoid scaling against the wrong dimensions.
150
+ let postMessageScrollX = 0
151
+ let postMessageScrollY = 0
152
+ let postMessageDocWidth = 0
153
+ let postMessageDocHeight = 0
154
+ let hasScrollData = false
155
+ let pendingClick: PendingClick | null = null
156
+
157
+ // Resolve the preview origin once — iframe.src is stable.
158
+ let iframeOrigin = '*'
159
+ try {
160
+ iframeOrigin = new URL(iframe.src).origin
161
+ } catch {
162
+ /* keep '*' */
163
+ }
164
+ const postToPreview = (message: unknown) =>
165
+ iframe.contentWindow?.postMessage(message, iframeOrigin)
166
+
167
+ // Pin position helpers — single source of truth for the coordinate model.
168
+ const pinAbsoluteX = (pin: PinData) =>
169
+ (pin.xPercent / 100) * postMessageDocWidth
170
+ const pinAbsoluteY = (pin: PinData) =>
171
+ (pin.yPercent / 100) * postMessageDocHeight
172
+ const pinLeftPercent = (pin: PinData) =>
173
+ ((pinAbsoluteX(pin) - postMessageScrollX) / (overlay.clientWidth || 1)) *
174
+ 100
175
+ const pinTopPercent = (pin: PinData) =>
176
+ ((pinAbsoluteY(pin) - postMessageScrollY) / (overlay.clientHeight || 1)) *
177
+ 100
178
+
179
+ const setActivePin = (postId: string | null) => {
180
+ overlay.querySelectorAll<HTMLElement>('.SharePreview-pin').forEach((p) => {
181
+ p.classList.toggle('is-active', !!postId && p.dataset.postId === postId)
182
+ })
183
+ }
184
+
185
+ const messageHandler = (e: MessageEvent) => {
186
+ if (!e.data || e.data.type !== 'BSP.SharedPreview.ScrollData') return
187
+ if (e.source !== iframe.contentWindow) return
188
+ const firstMessage = !hasScrollData
189
+ postMessageScrollX = e.data.scrollX as number
190
+ postMessageScrollY = e.data.scrollY as number
191
+ postMessageDocWidth = e.data.docWidth as number
192
+ postMessageDocHeight = e.data.docHeight as number
193
+ hasScrollData = true
194
+ // First message: need a full render to create pin elements.
195
+ // Subsequent scroll events: just reposition existing elements.
196
+ if (firstMessage) {
197
+ renderPins(cachedPins)
198
+ flushPendingClick()
199
+ } else {
200
+ repositionPins()
201
+ }
202
+ }
203
+ // This and the rtc.receive subscription and onFind('.Conversation-post') below
204
+ // are page-level and intentionally never torn down: a share-view page hosts a
205
+ // single .SharePreview for its lifetime, so initCommentingOverlay runs once.
206
+ window.addEventListener('message', messageHandler)
207
+
208
+ function getConversationEl(): HTMLElement | null {
209
+ return sharePreview.querySelector<HTMLElement>(
210
+ '.Conversation.frame[data-preview-id]',
211
+ )
212
+ }
213
+
214
+ const resolvePreviewId = (): string =>
215
+ getConversationEl()?.dataset.previewId || urlPreviewId
216
+
217
+ // Current scroll state, used for pin placement. Every caller runs after the first ScrollData
218
+ // has arrived (hasScrollData === true), so this just returns the postMessage-backed state.
219
+ // The same-origin/overlay seeding for the script-blocked case lives in ensureScrollData().
220
+ function getScrollInfo(): ScrollState {
221
+ return {
222
+ scrollX: postMessageScrollX,
223
+ scrollY: postMessageScrollY,
224
+ docWidth: postMessageDocWidth,
225
+ docHeight: postMessageDocHeight,
226
+ }
227
+ }
228
+
229
+ // Fallback when the injected ScrollData script never reports (blocked/absent): seed scroll state
230
+ // from a same-origin read so pins render against true document coordinates, degrading to
231
+ // overlay-relative dimensions only when the preview is cross-origin.
232
+ function ensureScrollData() {
233
+ if (hasScrollData) return
234
+ let scrollX = 0
235
+ let scrollY = 0
236
+ let docWidth = overlay.clientWidth || 1
237
+ let docHeight = overlay.clientHeight || 1
238
+ try {
239
+ const win = iframe.contentWindow
240
+ const doc = iframe.contentDocument
241
+ if (win && doc) {
242
+ scrollX = win.scrollX
243
+ scrollY = win.scrollY
244
+ docWidth = doc.documentElement.scrollWidth || docWidth
245
+ docHeight = doc.documentElement.scrollHeight || docHeight
246
+ }
247
+ } catch {
248
+ /* cross-origin — keep overlay-relative defaults */
249
+ }
250
+ postMessageScrollX = scrollX
251
+ postMessageScrollY = scrollY
252
+ postMessageDocWidth = docWidth
253
+ postMessageDocHeight = docHeight
254
+ hasScrollData = true
255
+ renderPins(cachedPins)
256
+ flushPendingClick()
257
+ }
258
+
259
+ function flushPendingClick() {
260
+ if (!pendingClick) return
261
+ const pc = pendingClick
262
+ pendingClick = null
263
+ const { scrollX, scrollY, docWidth, docHeight } = getScrollInfo()
264
+ const xPercent =
265
+ ((scrollX + (pc.clientX - pc.overlayRect.left)) / docWidth) * 100
266
+ const yPercent =
267
+ ((scrollY + (pc.clientY - pc.overlayRect.top)) / docHeight) * 100
268
+ closeActivePinPopover?.()
269
+ closeActivePinPopover = showPinPopover({
270
+ clientX: pc.clientX,
271
+ clientY: pc.clientY,
272
+ xPercent,
273
+ yPercent,
274
+ contentId: pc.contentId,
275
+ overlayId: pc.overlayId,
276
+ previewId: pc.previewId,
277
+ overlay,
278
+ onSuccess: fetchAndRenderPins,
279
+ })
280
+ }
281
+
282
+ // Full re-render: recreates pin DOM elements. Call only when the pin set changes
283
+ // (initial fetch, new/deleted pin via RTC). Use repositionPins() for scroll updates.
284
+ function renderPins(pins: PinData[]) {
285
+ cachedPins = pins
286
+ if (!hasScrollData) return
287
+
288
+ overlay.querySelectorAll('.SharePreview-pin').forEach((p) => p.remove())
289
+
290
+ pins.forEach((pin) => {
291
+ const pinEl = document.createElement('button')
292
+ pinEl.className = `SharePreview-pin SharePreview-pin--color${pin.colorIndex}`
293
+ pinEl.dataset.postId = pin.id
294
+ pinEl.textContent = pin.initials
295
+ pinEl.type = 'button'
296
+ pinEl.setAttribute('aria-label', `Comment by ${pin.initials}`)
297
+
298
+ pinEl.style.left = `${pinLeftPercent(pin)}%`
299
+ pinEl.style.top = `${pinTopPercent(pin)}%`
300
+
301
+ pinEl.addEventListener('click', (e) => {
302
+ e.stopPropagation()
303
+ highlightPost(pin.id)
304
+ })
305
+
306
+ overlay.appendChild(pinEl)
307
+ })
308
+ }
309
+
310
+ // Lightweight scroll update: updates only style.left/top on existing pin elements.
311
+ function repositionPins() {
312
+ if (!hasScrollData) return
313
+ overlay
314
+ .querySelectorAll<HTMLElement>('.SharePreview-pin')
315
+ .forEach((pinEl) => {
316
+ const pin = cachedPins.find((p) => p.id === pinEl.dataset.postId)
317
+ if (!pin) return
318
+ pinEl.style.left = `${pinLeftPercent(pin)}%`
319
+ pinEl.style.top = `${pinTopPercent(pin)}%`
320
+ })
321
+ }
322
+
323
+ // Coalesce reposition work to one update per frame so a burst of wheel/scroll
324
+ // events doesn't trigger a layout write per event.
325
+ let repositionScheduled = false
326
+ function scheduleReposition() {
327
+ if (repositionScheduled) return
328
+ repositionScheduled = true
329
+ requestAnimationFrame(() => {
330
+ repositionScheduled = false
331
+ repositionPins()
332
+ })
333
+ }
334
+
335
+ async function fetchAndRenderPins() {
336
+ const previewId = resolvePreviewId()
337
+ if (!previewId) return
338
+
339
+ try {
340
+ const response = await fetch(
341
+ `/cms/preview/share-view-conversation?previewId=${encodeURIComponent(previewId)}&pinsOnly=true`,
342
+ { credentials: 'include' },
343
+ )
344
+ if (!response.ok) return
345
+ const pins: PinData[] = await response.json()
346
+ renderPins(pins)
347
+ } catch {
348
+ // Network or parse error — silently ignore
349
+ }
350
+ }
351
+
352
+ function highlightPost(postId: string) {
353
+ const post = document.getElementById(postId)
354
+ if (post) {
355
+ post.scrollIntoView({ behavior: 'smooth', block: 'center' })
356
+ post.classList.add('is-pin-active')
357
+ setTimeout(() => post.classList.remove('is-pin-active'), 2000)
358
+ }
359
+ setActivePin(postId)
360
+ }
361
+
362
+ // ── Click overlay iframe ──────────────────────────────────────────────────
363
+ // A same-origin click overlay sits above the cross-origin preview iframe and,
364
+ // when commenting is open, captures wheel (forwarded as ScrollBy) and click
365
+ // (opens the pin placement popover).
366
+
367
+ const clickOverlayEl = sharePreview.querySelector<HTMLElement>(
368
+ '.SharePreview-clickOverlay',
369
+ )
370
+
371
+ // Teardown for the currently-open pin popover, if any. Tracked so re-clicking
372
+ // the overlay closes the prior popover through its full close path.
373
+ let closeActivePinPopover: (() => void) | null = null
374
+
375
+ if (clickOverlayEl) {
376
+ clickOverlayEl.addEventListener(
377
+ 'wheel',
378
+ (e: WheelEvent) => {
379
+ e.preventDefault()
380
+ // Optimistic: update local scroll for immediate pin repositioning.
381
+ postMessageScrollX = Math.max(0, postMessageScrollX + e.deltaX)
382
+ postMessageScrollY = Math.max(0, postMessageScrollY + e.deltaY)
383
+ scheduleReposition()
384
+ postToPreview({
385
+ type: 'BSP.SharedPreview.ScrollBy',
386
+ deltaY: e.deltaY,
387
+ deltaX: e.deltaX,
388
+ })
389
+ },
390
+ { passive: false },
391
+ )
392
+
393
+ // Click → place a pin at the clicked position
394
+ clickOverlayEl.addEventListener('click', (e: MouseEvent) => {
395
+ const conv = getConversationEl()
396
+ const previewId = resolvePreviewId()
397
+ if (!previewId) return
398
+
399
+ const contentId = conv?.dataset.contentId || ''
400
+ const overlayId = conv?.dataset.overlayId || ''
401
+
402
+ // If real scroll dimensions haven't arrived yet, defer coordinate
403
+ // computation until the first ScrollData message. Sending a zero-delta
404
+ // ScrollBy prompts the preview iframe to report its state immediately so
405
+ // the wait is typically only a few frames rather than the full fallback
406
+ // timeout.
407
+ if (!hasScrollData) {
408
+ pendingClick = {
409
+ clientX: e.clientX,
410
+ clientY: e.clientY,
411
+ overlayRect: clickOverlayEl.getBoundingClientRect(),
412
+ previewId,
413
+ contentId,
414
+ overlayId,
415
+ }
416
+ postToPreview({
417
+ type: 'BSP.SharedPreview.ScrollBy',
418
+ deltaX: 0,
419
+ deltaY: 0,
420
+ })
421
+ return
422
+ }
423
+
424
+ const overlayRect = clickOverlayEl.getBoundingClientRect()
425
+ const { scrollX, scrollY, docWidth, docHeight } = getScrollInfo()
426
+ const xPercent =
427
+ ((scrollX + (e.clientX - overlayRect.left)) / docWidth) * 100
428
+ const yPercent =
429
+ ((scrollY + (e.clientY - overlayRect.top)) / docHeight) * 100
430
+
431
+ closeActivePinPopover?.()
432
+
433
+ closeActivePinPopover = showPinPopover({
434
+ clientX: e.clientX,
435
+ clientY: e.clientY,
436
+ xPercent,
437
+ yPercent,
438
+ contentId,
439
+ overlayId,
440
+ previewId,
441
+ overlay,
442
+ onSuccess: fetchAndRenderPins,
443
+ })
444
+ })
445
+ }
446
+
447
+ // Fetch pins when conversation panel finishes loading
448
+ onFind('.Conversation.frame.loaded', (conversation: HTMLElement) => {
449
+ if (!sharePreview.contains(conversation)) return
450
+ if (!conversation.dataset.previewId) return
451
+ fetchAndRenderPins()
452
+ })
453
+
454
+ // Real-time: refresh pins when any post is added/deleted for this preview
455
+ rtc.receive('com.psddev.cms.conversation.PostBroadcast', (data: unknown) => {
456
+ const postData = data as PostBroadcastData
457
+ const conv = getConversationEl()
458
+ const contentId = conv?.dataset.contentId
459
+ if (contentId && postData.contentId === contentId) {
460
+ fetchAndRenderPins()
461
+ }
462
+ })
463
+
464
+ // Bidirectional: hovering a panel post highlights its pin;
465
+ // clicking a panel post scrolls the preview iframe to the pin's location.
466
+ onFind('.Conversation-post', (post: HTMLElement) => {
467
+ const postId = post.id
468
+ if (!postId) return
469
+
470
+ post.addEventListener('mouseenter', () => setActivePin(postId))
471
+ post.addEventListener('mouseleave', () => setActivePin(null))
472
+
473
+ post.addEventListener('click', (e: MouseEvent) => {
474
+ // Don't intercept clicks on interactive elements within the post
475
+ if ((e.target as HTMLElement).closest('a, button, textarea, input'))
476
+ return
477
+
478
+ const pin = cachedPins.find((p) => p.id === postId)
479
+ if (!pin || !hasScrollData) return
480
+
481
+ // Center the pin vertically in the viewport
482
+ const targetScrollY = Math.max(
483
+ 0,
484
+ pinAbsoluteY(pin) - (overlay.clientHeight || 1) / 2,
485
+ )
486
+ postToPreview({ type: 'BSP.SharedPreview.ScrollTo', top: targetScrollY })
487
+
488
+ // Optimistic update so pins reposition immediately
489
+ postMessageScrollY = targetScrollY
490
+ repositionPins()
491
+ })
492
+ })
493
+
494
+ // When commenting is turned off (panel close button or toggle), tear down any open pin popover.
495
+ // The pins themselves are hidden via CSS while commenting is closed.
496
+ new MutationObserver(() => {
497
+ if (!sharePreview.classList.contains('is-commenting-open')) {
498
+ closeActivePinPopover?.()
499
+ closeActivePinPopover = null
500
+ }
501
+ }).observe(sharePreview, { attributes: true, attributeFilter: ['class'] })
502
+
503
+ // Render even if the preview never reports its scroll position.
504
+ setTimeout(ensureScrollData, SCROLL_DATA_FALLBACK_MS)
505
+ }
506
+
507
+ // ─── Pin placement popover ───────────────────────────────────────────────────
508
+
509
+ function showPinPopover(options: PinPopoverOptions): () => void {
510
+ const {
511
+ clientX,
512
+ clientY,
513
+ xPercent,
514
+ yPercent,
515
+ contentId,
516
+ overlayId,
517
+ previewId,
518
+ overlay,
519
+ onSuccess,
520
+ } = options
521
+
522
+ // Placeholder pin — shown while the user types, removed on submit or cancel
523
+ const placeholder = document.createElement('div')
524
+ placeholder.className = 'SharePreview-pin SharePreview-pin--placeholder'
525
+ placeholder.setAttribute('aria-hidden', 'true')
526
+ const overlayRect = overlay.getBoundingClientRect()
527
+ placeholder.style.left = `${((clientX - overlayRect.left) / overlayRect.width) * 100}%`
528
+ placeholder.style.top = `${((clientY - overlayRect.top) / overlayRect.height) * 100}%`
529
+ overlay.appendChild(placeholder)
530
+
531
+ const popover = document.createElement('div')
532
+ popover.className = 'SharePreview-pinPopover'
533
+ popover.setAttribute('role', 'dialog')
534
+ popover.setAttribute('aria-label', 'Add a comment')
535
+
536
+ // Position is handled by Tether (below), anchored to the placeholder pin.
537
+
538
+ // Clone the server-rendered rich-text editor template (formatting + @mentions); fall back to a plain textarea.
539
+ const editorTemplate = document.getElementById('sharePreview-pinEditor')
540
+ let editorEl: HTMLElement
541
+ let getValue: () => string
542
+ let focusEditor: () => void
543
+ let rteCleanup: (() => void) | null = null
544
+
545
+ if (editorTemplate instanceof HTMLTemplateElement) {
546
+ editorEl = document.createElement('div')
547
+ editorEl.className = 'SharePreview-pinPopover-editor'
548
+ editorEl.appendChild(editorTemplate.content.cloneNode(true))
549
+ const textarea = editorEl.querySelector<HTMLTextAreaElement>(
550
+ 'textarea[name="text"]',
551
+ )
552
+ // Drop the cloned id so it can't collide with the panel's own editor.
553
+ textarea?.removeAttribute('id')
554
+ getValue = () => textarea?.value.trim() ?? ''
555
+ focusEditor = () =>
556
+ (editorEl.querySelector<HTMLElement>('.ProseMirror') ?? textarea)?.focus()
557
+ // Focus deterministically once the editor finishes enhancing, rather than racing the
558
+ // global .richtext watcher; cleaned up in close() if it never fires.
559
+ if (textarea) {
560
+ rteCleanup = onRTEReady((rte) => rte.view.dom.focus(), textarea)
561
+ }
562
+ } else {
563
+ const textarea = document.createElement('textarea')
564
+ textarea.className = 'SharePreview-pinPopover-input'
565
+ textarea.placeholder = 'Add a comment…'
566
+ textarea.rows = 3
567
+ editorEl = textarea
568
+ getValue = () => textarea.value.trim()
569
+ focusEditor = () => textarea.focus()
570
+ }
571
+
572
+ const actions = document.createElement('div')
573
+ actions.className = 'SharePreview-pinPopover-actions'
574
+
575
+ const cancelBtn = document.createElement('button')
576
+ cancelBtn.type = 'button'
577
+ cancelBtn.className = 'SharePreview-pinPopover-cancel Button Button--border'
578
+ cancelBtn.textContent = 'Cancel'
579
+
580
+ const submitBtn = document.createElement('button')
581
+ submitBtn.type = 'button'
582
+ submitBtn.className = 'SharePreview-pinPopover-submit Button'
583
+ submitBtn.textContent = 'Post'
584
+
585
+ actions.append(cancelBtn, submitBtn)
586
+ popover.append(editorEl, actions)
587
+ document.body.appendChild(popover)
588
+ // Tether the popover to the placeholder pin: RTL-aware positioning that tracks scroll/resize.
589
+ // The plain constructor positions only (no outside-click close), so the @mention dropdown can
590
+ // tether outside the popover without dismissing it.
591
+ const tether = new Tether(placeholder, popover)
592
+ focusEditor()
593
+
594
+ // Dismiss is Cancel/Escape/placing another pin only — no outside-click close, since the
595
+ // @mention combo tethers its dropdown outside the popover and would dismiss it mid-mention.
596
+ let closed = false
597
+ const close = () => {
598
+ if (closed) return
599
+ closed = true
600
+ rteCleanup?.()
601
+ tether.destroy()
602
+ popover.remove()
603
+ placeholder.remove()
604
+ document.removeEventListener('keydown', escHandler)
605
+ }
606
+ const escHandler = (e: KeyboardEvent) => {
607
+ if (e.key === 'Escape') close()
608
+ }
609
+
610
+ cancelBtn.addEventListener('click', close)
611
+
612
+ submitBtn.addEventListener('click', async () => {
613
+ const text = getValue()
614
+ if (!text) {
615
+ focusEditor()
616
+ return
617
+ }
618
+
619
+ submitBtn.disabled = true
620
+
621
+ const headers = new Headers()
622
+ const cookie = Cookies.get('bsp.csrf')
623
+ if (cookie) headers.append('Brightspot-CSRF', cookie)
624
+
625
+ const body = new FormData()
626
+ body.append('postText', text)
627
+ body.append('contentId', contentId)
628
+ body.append('overlayId', overlayId)
629
+ body.append('sharedPreviewId', previewId)
630
+ body.append('xPercent', String(xPercent))
631
+ body.append('yPercent', String(yPercent))
632
+ body.append('postsOnly', 'true')
633
+
634
+ try {
635
+ const response = await fetch(
636
+ window.BRIGHTSPOT?.web?.pagePaths?.[
637
+ 'com.psddev.cms.conversation.Posts'
638
+ ],
639
+ { method: 'post', credentials: 'include', headers, body },
640
+ )
641
+ // fetch only rejects on network errors, so a 4xx/5xx (e.g. invalid/expired
642
+ // one-time token, or auth required) would otherwise close the popover as if
643
+ // the comment saved and drop the user's text. Keep the popover open instead.
644
+ if (!response.ok) {
645
+ submitBtn.disabled = false
646
+ return
647
+ }
648
+ close()
649
+ onSuccess()
650
+ } catch {
651
+ submitBtn.disabled = false
652
+ }
653
+ })
654
+
655
+ document.addEventListener('keydown', escHandler)
656
+
657
+ // Return the teardown so callers close via the single path (element + listener + placeholder).
658
+ return close
659
+ }
@@ -0,0 +1 @@
1
+ "use strict";(self.webpackChunkbrightspot_cms=self.webpackChunkbrightspot_cms||[]).push([[1888],{95628(t,a,e){function o(t,a){return t?(Array.isArray(a)&&(a=a.join(",")),t.querySelector(a)):null}e.d(a,{A:()=>o}),Object.defineProperty(o,"name",{value:"default",configurable:!0})},60021(t,a,e){function o(t,a){return t?(Array.isArray(a)&&(a=a.join(",")),t.querySelector(a)):null}e.d(a,{A:()=>o}),Object.defineProperty(o,"name",{value:"default",configurable:!0})},83814(t,a,e){e.d(a,{default:()=>l}),e(84978),e(91672),e(92191),e(68690),e(45299);var o=e(53608),r=e.n(o),n=e(80907);if(7717==e.j)var i=e(95628);class l{constructor(t){if(this._container=t,this._latInput=(0,i.A)(t,".locationMapLatitude"),this._lngInput=(0,i.A)(t,".locationMapLongitude"),this._zoomInput=(0,i.A)(t,".locationMapZoom"),!this._latInput||!this._lngInput||!this._zoomInput)return;const{tileUrl:a,tileAttribution:e,geocoderSearchUrl:o,geocoderReverseUrl:l}=t.dataset,s=this._latInput.value,p=this._lngInput.value,c=""!==s&&""!==p,u=c?new(r().LatLng)(parseFloat(s),parseFloat(p)):new(r().LatLng)(38.95498,-77.365057);this._map=r().map(t,{center:u,zoom:parseFloat(this._zoomInput.value)||6,layers:[r().tileLayer(a||"https://tile.openstreetmap.org/{z}/{x}/{y}.png",{attribution:e||'© <a href="https://openstreetmap.org">OpenStreetMap</a> contributors',referrerPolicy:"strict-origin-when-cross-origin"})]}),this._map.on("click",t=>{t.originalEvent.srcElement.classList.contains("locationMap")&&this.updateMarker(t.latlng)}),this._map.on("zoomend",()=>{this._zoomInput.value=this._map.getZoom()}),c&&this.updateMarker(u,""),new n.Vn({provider:new n.F6({...o&&{searchUrl:o},...l&&{reverseUrl:l}}),retainZoomLevel:!0,showMarker:!1}).addTo(this._map),this._map.on("geosearch/showlocation",t=>{t.location&&this.updateMarker(new(r().LatLng)(parseFloat(t.location.y),parseFloat(t.location.x)),t.location.label)}),r().control.locate({drawCircle:!1,markerStyle:{weight:2,radius:6},keepCurrentZoomLevel:!0,icon:"LocationMap-locateControl",iconLoading:"LocationMap-locateControl is-loading",strings:{title:"Set marker to your current location",popup:"<b>Current location</b><br/>(within {distance} {unit})"}}).addTo(this._map),this._map.on("locationfound",t=>{this.updateMarker(t.latlng,"")}),new(r().Control.extend({onAdd:()=>{const t=r().DomUtil.create("div");t.className="leaflet-bar leaflet-control";const a=r().DomUtil.create("a");return a.className="LocationMap-removeMarker",a.onclick=t=>{t.preventDefault(),t.stopPropagation(),this._marker&&(this._map.removeLayer(this._marker),this._marker=null,this._latInput.value=null,this._lngInput.value=null,this._container.classList.remove("is-marked"))},t.appendChild(a),t}}))({position:"topleft"}).addTo(this._map)}updateValue(t){this._latInput.value=t.lat,this._lngInput.value=t.lng}updateMarker(t,a){if(this._marker)this._marker.setLatLng(t),this._marker.bindPopup(a||t.toString());else{const e=r().divIcon({className:"LocationMap-marker",iconSize:[36,36],iconAnchor:[18,36],popupAnchor:[0,-30]});this._marker=r().marker(t,{draggable:!0,autoPan:!0,icon:e}).bindPopup(a||t.toString()),this._marker.on("moveend",t=>{this.updateValue(t.target._latlng),this._marker.bindPopup(t.target._latlng.toString())}),this._marker.addTo(this._map)}this._container.classList.add("is-marked"),this.updateValue(t)}}},48207(t,a,e){e.d(a,{default:()=>l}),e(49371),e(56065),e(79382),e(81499),e(80489);var o=e(66417),r=e.n(o),n=e(68098);if(3324==e.j)var i=e(60021);class l{constructor(t){if(this._container=t,this._latInput=(0,i.A)(t,".locationMapLatitude"),this._lngInput=(0,i.A)(t,".locationMapLongitude"),this._zoomInput=(0,i.A)(t,".locationMapZoom"),!this._latInput||!this._lngInput||!this._zoomInput)return;const{tileUrl:a,tileAttribution:e,geocoderSearchUrl:o,geocoderReverseUrl:l}=t.dataset,s=this._latInput.value,p=this._lngInput.value,c=""!==s&&""!==p,u=c?new(r().LatLng)(parseFloat(s),parseFloat(p)):new(r().LatLng)(38.95498,-77.365057);this._map=r().map(t,{center:u,zoom:parseFloat(this._zoomInput.value)||6,layers:[r().tileLayer(a||"https://tile.openstreetmap.org/{z}/{x}/{y}.png",{attribution:e||'© <a href="https://openstreetmap.org">OpenStreetMap</a> contributors',referrerPolicy:"strict-origin-when-cross-origin"})]}),this._map.on("click",t=>{t.originalEvent.srcElement.classList.contains("locationMap")&&this.updateMarker(t.latlng)}),this._map.on("zoomend",()=>{this._zoomInput.value=this._map.getZoom()}),c&&this.updateMarker(u,""),new n.Vn({provider:new n.F6({...o&&{searchUrl:o},...l&&{reverseUrl:l}}),retainZoomLevel:!0,showMarker:!1}).addTo(this._map),this._map.on("geosearch/showlocation",t=>{t.location&&this.updateMarker(new(r().LatLng)(parseFloat(t.location.y),parseFloat(t.location.x)),t.location.label)}),r().control.locate({drawCircle:!1,markerStyle:{weight:2,radius:6},keepCurrentZoomLevel:!0,icon:"LocationMap-locateControl",iconLoading:"LocationMap-locateControl is-loading",strings:{title:"Set marker to your current location",popup:"<b>Current location</b><br/>(within {distance} {unit})"}}).addTo(this._map),this._map.on("locationfound",t=>{this.updateMarker(t.latlng,"")}),new(r().Control.extend({onAdd:()=>{const t=r().DomUtil.create("div");t.className="leaflet-bar leaflet-control";const a=r().DomUtil.create("a");return a.className="LocationMap-removeMarker",a.onclick=t=>{t.preventDefault(),t.stopPropagation(),this._marker&&(this._map.removeLayer(this._marker),this._marker=null,this._latInput.value=null,this._lngInput.value=null,this._container.classList.remove("is-marked"))},t.appendChild(a),t}}))({position:"topleft"}).addTo(this._map)}updateValue(t){this._latInput.value=t.lat,this._lngInput.value=t.lng}updateMarker(t,a){if(this._marker)this._marker.setLatLng(t),this._marker.bindPopup(a||t.toString());else{const e=r().divIcon({className:"LocationMap-marker",iconSize:[36,36],iconAnchor:[18,36],popupAnchor:[0,-30]});this._marker=r().marker(t,{draggable:!0,autoPan:!0,icon:e}).bindPopup(a||t.toString()),this._marker.on("moveend",t=>{this.updateValue(t.target._latlng),this._marker.bindPopup(t.target._latlng.toString())}),this._marker.addTo(this._map)}this._container.classList.add("is-marked"),this.updateValue(t)}}}}]);