PageSourceSearch

https://noscrubs.io/assets/controllers/fold/conversation_controlle…0672dd139011ce2c0526e98544d33fee4b996a99.js

js noscrubs.io collected 2026-10-04 17:33:12 UTC 9,172 bytes, 320 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2
3const BOTTOM_THRESHOLD_PX = 48;
4
5export default class extends Controller {
6  static targets = [
7    "thread",
8    "messages",
9    "composer",
10    "newMessagesButton",
11    "confirmDialog",
12    "confirmForm",
13    "confirmMessage",
14    "confirmAttributes",
15  ];
16
17  connect() {
18    this.wasNearBottom = true;
19    this.initialized = false;
20    this.messageObserver = new MutationObserver((mutations) =>
21      this.messagesMutated(mutations),
22    );
23    this.messageObserver.observe(this.messagesTarget, { childList: true });
24
25    this.initializeThread();
26  }
27
28  disconnect() {
29    this.messageObserver?.disconnect();
30  }
31
32  positionMessageMenu(event) {
33    const details = event.currentTarget;
34    if (!details.open) return;
35
36    this.element
37      .querySelectorAll("details[data-message-menu-details][open]")
38      .forEach((openDetails) => {
39        if (openDetails !== details) openDetails.open = false;
40      });
41
42    const menu = details.querySelector("[data-role='message-menu']");
43    if (!menu) return;
44
45    this.positionFloatingElement(details, menu);
46  }
47
48  openTaskHoverCard(event) {
49    const container = event.currentTarget;
50    const card = container.querySelector("[data-role='task-hover-card']");
51    if (!card) return;
52
53    card.classList.remove("hidden");
54    this.positionFloatingElement(container, card, { fixed: true });
55  }
56
57  closeTaskHoverCard(event) {
58    event.currentTarget
59      .closest("[data-task-hover-container]")
60      ?.querySelector("[data-role='task-hover-card']")
61      ?.classList.add("hidden");
62  }
63
64  positionFloatingElement(trigger, element, options = {}) {
65    const gap = 8;
66    const triggerRect = trigger.getBoundingClientRect();
67    const threadRect = this.hasThreadTarget
68      ? this.threadTarget.getBoundingClientRect()
69      : { top: 0, left: 0, right: window.innerWidth };
70    const composerTop = this.hasComposerTarget
71      ? this.composerTarget.getBoundingClientRect().top
72      : window.innerHeight;
73    const elementHeight = element.offsetHeight;
74    const elementWidth = element.offsetWidth;
75
76    if (options.fixed) {
77      const spaceAbove = triggerRect.top - threadRect.top;
78      const spaceBelow =
79        Math.min(composerTop, window.innerHeight) - triggerRect.bottom;
80      const openBelow =
81        spaceBelow >= elementHeight ||
82        (spaceAbove < elementHeight && spaceBelow >= spaceAbove);
83      const top = openBelow
84        ? triggerRect.bottom
85        : triggerRect.top - elementHeight;
86      const left = Math.min(
87        Math.max(triggerRect.left, threadRect.left),
88        Math.min(threadRect.right, window.innerWidth) - elementWidth,
89      );
90
91      element.style.top = `${top}px`;
92      element.style.left = `${left}px`;
93      return;
94    }
95
96    const fitsAbove = triggerRect.top - elementHeight - gap >= threadRect.top;
97    const fitsBelow = triggerRect.bottom + elementHeight + gap <= composerTop;
98    const openBelow = !fitsAbove && fitsBelow;
99    const alignRight = triggerRect.left + elementWidth + gap > threadRect.right;
100
101    element.classList.toggle("top-full", openBelow);
102    element.classList.toggle("pt-2", openBelow);
103    element.classList.toggle("bottom-full", !openBelow);
104    element.classList.toggle("pb-2", !openBelow);
105    element.classList.toggle("right-0", alignRight);
106    element.classList.toggle("left-0", !alignRight);
107  }
108
109  initializeThread() {
110    this.wasNearBottom = this.isNearBottom();
111    this.initialized = true;
112    this.hideNewMessagesButton();
113  }
114
115  threadScrolled() {
116    this.wasNearBottom = this.isNearBottom();
117
118    if (this.wasNearBottom) {
119      this.hideNewMessagesButton();
120    }
121  }
122
123  scrollToBottom() {
124    this.scrollThreadToBottom("smooth");
125    this.hideNewMessagesButton();
126  }
127
128  openConfirmation(event) {
129    if (!this.hasConfirmDialogTarget || !this.hasConfirmFormTarget) return;
130
131    const url = event.currentTarget.dataset.url;
132    if (!url) return;
133
134    event.currentTarget.closest("details")?.removeAttribute("open");
135    this.confirmFormTarget.action = url;
136    this.confirmFormTarget.dataset.method =
137      event.currentTarget.dataset.method || "DELETE";
138    this.confirmAttributesTarget.innerHTML = "";
139
140    const attributeName = event.currentTarget.dataset.attributeName;
141    const attributeValue = event.currentTarget.dataset.attributeValue;
142    if (attributeName && attributeValue != null) {
143      const input = document.createElement("input");
144      input.type = "hidden";
145      input.name = attributeName;
146      input.value = attributeValue;
147      this.confirmAttributesTarget.appendChild(input);
148    }
149
150    const confirmTitle = event.currentTarget.dataset.confirmationTitle;
151    const confirmButtonLabel =
152      event.currentTarget.dataset.confirmationConfirmLabel;
153
154    if (this.hasConfirmMessageTarget) {
155      this.confirmMessageTarget.textContent =
156        event.currentTarget.dataset.confirmationMessage ||
157        "Are you sure you want to continue?";
158    }
159
160    this.confirmDialogTarget.querySelector(
161      "[data-role='confirm-title']",
162    ).textContent = confirmTitle || "Confirm Delete";
163    this.confirmDialogTarget.querySelector(
164      "[data-role='confirm-submit']",
165    ).textContent = confirmButtonLabel || "Delete";
166
167    this.confirmDialogTarget.showModal();
168  }
169
170  closeConfirmation() {
171    if (!this.hasConfirmDialogTarget) return;
172
173    this.confirmDialogTarget.close();
174  }
175
176  openComposeContact(event) {
177    const value = event.currentTarget.dataset.foldComposeContactValue;
178    if (!value) return;
179
180    window.dispatchEvent(
181      new CustomEvent("fold:compose-contact", {
182        detail: {
183          contact: {
184            value,
185            label: event.currentTarget.dataset.foldComposeContactLabel || value,
186          },
187          channelId: event.currentTarget.dataset.foldComposeChannelId,
188          channelType: event.currentTarget.dataset.foldComposeChannelType,
189        },
190      }),
191    );
192
193    event.currentTarget.closest("details")?.removeAttribute("open");
194  }
195
196  orderMediaAttachmentSubmitted(event) {
197    if (!event.detail.success) return;
198
199    event.target.closest("details")?.removeAttribute("open");
200  }
201
202  loadOrderMediaAttachment(event) {
203    const details = event.currentTarget;
204    if (!details.open) return;
205
206    const frame = details.querySelector("turbo-frame[data-src]");
207    if (!frame) return;
208
209    const url = new URL(frame.dataset.src, window.location.origin);
210    url.searchParams.set("_", Date.now().toString());
211    frame.innerHTML =
212      '<div tabindex="0" class="d-dropdown-content z-[100] mb-2 w-[350px] rounded-lg border border-gray-200 bg-white p-3 text-sm text-base-content/60 shadow-sm">Loading orders...</div>';
213    frame.src = url.toString();
214  }
215
216  async submitDelete(event) {
217    event.preventDefault();
218
219    const formData = new FormData(this.confirmFormTarget);
220    formData.delete("_method");
221    const csrfParam = document
222      .querySelector('meta[name="csrf-param"]')
223      ?.getAttribute("content");
224    if (csrfParam) {
225      formData.delete(csrfParam);
226    }
227
228    const response = await fetch(this.confirmFormTarget.action, {
229      method: this.confirmFormTarget.dataset.method || "DELETE",
230      headers: this.requestHeaders("application/json"),
231      body: formData,
232    });
233
234    if (response.ok) {
235      this.closeConfirmation();
236      return;
237    }
238
239    this.closeConfirmation();
240  }
241
242  async retryMessage(event) {
243    const url = event.currentTarget.dataset.url;
244    if (!url) return;
245
246    event.currentTarget.disabled = true;
247
248    try {
249      const response = await fetch(url, {
250        method: "POST",
251        headers: this.requestHeaders("application/json"),
252      });
253
254      if (!response.ok) {
255        this.handleRequestError(response);
256      }
257    } finally {
258      event.currentTarget.disabled = false;
259    }
260  }
261
262  messagesMutated(mutations) {
263    const additions = mutations.reduce((count, mutation) => {
264      return (
265        count +
266        Array.from(mutation.addedNodes).filter((node) => {
267          return node.nodeType === Node.ELEMENT_NODE;
268        }).length
269      );
270    }, 0);
271
272    if (!this.initialized || additions === 0) return;
273
274    if (this.wasNearBottom) {
275      this.scrollThreadToBottom("smooth");
276      this.hideNewMessagesButton();
277      return;
278    }
279
280    this.showNewMessagesButton();
281  }
282
283  isNearBottom() {
284    return Math.abs(this.threadTarget.scrollTop) <= BOTTOM_THRESHOLD_PX;
285  }
286
287  scrollThreadToBottom(behavior) {
288    this.threadTarget.scrollTo({
289      top: 0,
290      behavior,
291    });
292  }
293
294  showNewMessagesButton() {
295    this.newMessagesButtonTarget.classList.remove("hidden");
296    this.newMessagesButtonTarget.classList.add("flex");
297  }
298
299  hideNewMessagesButton() {
300    this.newMessagesButtonTarget.classList.add("hidden");
301    this.newMessagesButtonTarget.classList.remove("flex");
302  }
303
304  requestHeaders(accept = "application/json") {
305    const headers = { Accept: accept };
306    const csrfToken = document
307      .querySelector('meta[name="csrf-token"]')
308      ?.getAttribute("content");
309
310    if (csrfToken) {
311      headers["X-CSRF-Token"] = csrfToken;
312    }
313
314    return headers;
315  }
316
317  handleRequestError(response) {
318    console.error(response);
319  }
320};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.