PageSourceSearch

https://app.replicants.ai/builder/js/embed-block.js?v=20260929wo1

js replicants.ai collected 2026-10-05 12:32:03 UTC 15,413 bytes, 293 lines download raw bytes

1/**
2 * embed-block.js: the website editor's Embed block (vault1, 2026-09-29).
3 *
4 * "Paste a link or embed code". The coach pastes whatever the other site gave
5 * them; POST /api/embeds/resolve recognises the provider and hands back OUR
6 * markup (src/lib/embed-resolve.js): the click-to-play video facade for a
7 * video, a sandboxed frame for everything else. A site that is not on the
8 * trusted list becomes a link card, never raw HTML (Alex: "no raw HTML ever").
9 *
10 * Loaded by grapes-tw.html after grapes-tw-components.js. Double-click an
11 * embed on the canvas to change it.
12 */
13(function () {
14  'use strict';
15
16  var PLACEHOLDER = '<div class="rk-embed rk-embed--empty" data-embed-empty="1" style="max-width:800px;margin:24px auto;padding:36px 20px;border:2px dashed rgba(120,120,140,.45);border-radius:12px;text-align:center;font:500 15px/1.4 Inter,system-ui,sans-serif;color:#6b7280;">Paste a link or embed code to show it here</div>';
17  var providers = null;
18
19  function esc(s) {
20    return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
21  }
22  // The editor has getAuthHeaders(); the app (the Vault library screens,
23  // which reuse this box) has the same token in storage.
24  function headers() {
25    var h = (typeof getAuthHeaders === 'function') ? getAuthHeaders({}) : {};
26    if (!h.Authorization) {
27      var t = null;
28      try { t = (window.SharedAuth && SharedAuth.getToken && SharedAuth.getToken()) || localStorage.getItem('cb_token') || localStorage.getItem('coachbot_token'); } catch (_) { t = null; }
29      if (t) h.Authorization = 'Bearer ' + t;
30    }
31    h['Content-Type'] = 'application/json';
32    return h;
33  }
34
35  function loadProviders() {
36    if (providers) return Promise.resolve(providers);
37    return fetch('/api/embeds/providers', { headers: headers() })
38      .then(function (r) { return r.ok ? r.json() : { groups: [] }; })
39      .then(function (d) { providers = d.groups || []; return providers; })
40      .catch(function () { return []; });
41  }
42
43  function styleOnce() {
44    if (document.getElementById('rk-embed-modal-css')) return;
45    var st = document.createElement('style');
46    st.id = 'rk-embed-modal-css';
47    st.textContent =
48      '.rkem-back{position:fixed;inset:0;background:rgba(10,10,20,.55);z-index:100000;display:flex;align-items:center;justify-content:center;padding:16px}' +
49      '.rkem{background:#fff;color:#111827;width:100%;max-width:560px;max-height:calc(100vh - 32px);overflow:auto;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.35);font:14px/1.5 Inter,system-ui,sans-serif}' +
50      '.rkem-h{display:flex;align-items:center;justify-content:space-between;padding:18px 20px 6px}' +
51      '.rkem-h h2{margin:0;font-size:18px;font-weight:700}' +
52      '.rkem-x{background:none;border:0;font-size:22px;line-height:1;cursor:pointer;color:#6b7280;padding:4px 8px}' +
53      '.rkem-b{padding:6px 20px 20px}' +
54      '.rkem label{display:block;font-weight:600;margin:8px 0 6px}' +
55      '.rkem textarea{width:100%;box-sizing:border-box;min-height:92px;border:1px solid #d1d5db;border-radius:10px;padding:10px 12px;font:14px/1.45 ui-monospace,Menlo,monospace;resize:vertical}' +
56      '.rkem textarea:focus{outline:2px solid #6c63ff;outline-offset:1px;border-color:transparent}' +
57      '.rkem-hint{color:#6b7280;font-size:13px;margin:8px 0 0}' +
58      '.rkem-hint button{background:none;border:0;padding:0;color:#4f46e5;cursor:pointer;font:inherit;text-decoration:underline}' +
59      '.rkem-list{margin:10px 0 0;padding:10px 12px;background:#f9fafb;border-radius:10px;font-size:13px;color:#374151}' +
60      '.rkem-list div{margin:2px 0}.rkem-list b{font-weight:600}' +
61      '.rkem-msg{min-height:20px;margin:12px 0 0;font-size:13px}' +
62      '.rkem-msg.err{color:#b91c1c}' +
63      '.rkem-card{margin-top:12px}' +
64      '.rkem-ask{margin-top:12px;padding:12px 14px;border-radius:10px;background:#f5f3ff}' +
65      '.rkem-ask-t{margin:0 0 8px;font-size:13px;color:#374151}' +
66      '.rkem-ask .rkem-btn{background:#fff;border:1px solid #c7d2fe}' +
67      '.rkem-f{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap;padding:0 20px 20px}' +
68      '.rkem-btn{border:0;border-radius:10px;padding:10px 16px;font:600 14px Inter,system-ui,sans-serif;cursor:pointer}' +
69      '.rkem-btn.pri{background:#6c63ff;color:#fff}.rkem-btn.sec{background:#f3f4f6;color:#111827}' +
70      '.rkem-btn[disabled]{opacity:.6;cursor:default}';
71    document.head.appendChild(st);
72  }
73
74  /**
75   * Open the paste box. onDone(html) receives the markup to put on the page.
76   * Exposed as window.rkEmbedPrompt so the Vault's Embed item can reuse it.
77   */
78  function openEmbedPrompt(opts) {
79    opts = opts || {};
80    styleOnce();
81    var back = document.createElement('div');
82    back.className = 'rkem-back';
83    back.innerHTML =
84      '<div class="rkem" role="dialog" aria-modal="true" aria-labelledby="rkem-title">' +
85        '<div class="rkem-h"><h2 id="rkem-title">Embed something</h2><button type="button" class="rkem-x" aria-label="Close">×</button></div>' +
86        '<div class="rkem-b">' +
87          '<label for="rkem-in">Paste a link or the embed code</label>' +
88          '<textarea id="rkem-in" placeholder="https://www.youtube.com/watch?v=..." spellcheck="false">' + esc(opts.value || '') + '</textarea>' +
89          '<p class="rkem-hint">Works with YouTube, Vimeo, Loom, Calendly, Canva, Typeform, Spotify and more. <button type="button" data-rkem-all>See every site</button></p>' +
90          '<div class="rkem-list" data-rkem-list hidden></div>' +
91          '<div class="rkem-msg" data-rkem-msg role="status" aria-l
91ive="polite"></div>' +
92          '<div class="rkem-card" data-rkem-card hidden></div>' +
93          '<div class="rkem-ask" data-rkem-ask hidden>' +
94            '<p class="rkem-ask-t" data-rkem-ask-t></p>' +
95            '<button type="button" class="rkem-btn sec" data-rkem-askbtn>Ask us to support this site</button>' +
96          '</div>' +
97        '</div>' +
98        '<div class="rkem-f">' +
99          '<button type="button" class="rkem-btn sec" data-rkem-cancel>Cancel</button>' +
100          '<button type="button" class="rkem-btn sec" data-rkem-asacard hidden>Add it as a link</button>' +
101          '<button type="button" class="rkem-btn pri" data-rkem-go>Embed</button>' +
102        '</div>' +
103      '</div>';
104    document.body.appendChild(back);
105    var input = back.querySelector('#rkem-in');
106    var msg = back.querySelector('[data-rkem-msg]');
107    var cardBox = back.querySelector('[data-rkem-card]');
108    var go = back.querySelector('[data-rkem-go]');
109    var asCard = back.querySelector('[data-rkem-asacard]');
110    var lastCardHtml = '';
111    var lastCardUrl = '';
112    var ask = back.querySelector('[data-rkem-ask]');
113    var askT = back.querySelector('[data-rkem-ask-t]');
114    var askBtn = back.querySelector('[data-rkem-askbtn]');
115    var askUrl = '';
116    // "Ask us to support this site" (vault1 phase 3). It lands in the team's
117    // Feedback Desk; the coach is told what happens next, in plain words, and
118    // a second ask for the same site says it is already on its way.
119    function showAsk(d) {
120      askUrl = d.url || '';
121      if (d.request) {
122        askT.textContent = d.request_message || '';
123        askBtn.hidden = true;
124      } else {
125        askT.textContent = 'Want it shown right on the page? Ask us to add it. Requested sites are ' + (d.typical || 'normally added within 48 hours') + '.';
126        askBtn.hidden = false; askBtn.disabled = false; askBtn.textContent = 'Ask us to support this site';
127      }
128      ask.hidden = false;
129    }
130    askBtn.onclick = function () {
131      if (!askUrl) return;
132      askBtn.disabled = true; askBtn.textContent = 'Sending\u2026';
133      fetch('/api/embeds/requests', { method: 'POST', headers: headers(), body: JSON.stringify({ url: askUrl, context: opts.context || 'website' }) })
134        .then(function (r) { return r.json(); })
135        .then(function (d) {
136          if (d && d.ok) { askT.textContent = d.message || ''; askBtn.hidden = true; return; }
137          askBtn.disabled = false; askBtn.textContent = 'Ask us to support this site';
138          askT.textContent = (d && d.message) || 'We could not send that just now. Please try again in a minute.';
139        })
140        .catch(function () {
141          askBtn.disabled = false; askBtn.textContent = 'Ask us to support this site';
142          askT.textContent = 'We could not send that just now. Please try again in a minute.';
143        });
144    };
145    function close() { if (back.parentNode) back.parentNode.removeChild(back); if (opts.onCancel) opts.onCancel(); }
146    function say(t, err) { msg.textContent = t || ''; msg.className = 'rkem-msg' + (err ? ' err' : ''); }
147    back.querySelector('.rkem-x').onclick = close;
148    back.querySelector('[data-rkem-cancel]').onclick = close;
149    back.addEventListener('click', function (e) { if (e.target === back) close(); });
150    back.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
151    back.querySelector('[data-rkem-all]').onclick = function () {
152      var list = back.querySelector('[data-rkem-list]');
153      if (!list.hidden) { list.hidden = true; return; }
154      loadProviders().then(function (groups) {
155        list.innerHTML = groups.map(function (g) {
156          return '<div><b>' + esc(g.label) + ':</b> ' + g.providers.map(function (p) { return esc(p.name); }).join(', ') + '</div>';
157        }).join('') || 'Could not load the list just now.';
158        list.hidden = false;
159      });
160    };
161    asCard.onclick = function () {
162      if (!lastCardHtml) return;
163      if (opts.onDone) opts.onDone(lastCardHtml, { kind: 'card', url: lastCardUrl });
164      opts.onCancel = null; close();
165    };
166    go.onclick = function () {
167      var v = input.value.trim();
168      if (!v) { say('Paste a link first.', true); input.focus(); return; }
169      go.disabled = true; say('Checking the link…');
170      cardBox.hidden = true; asCard.hidden = true; ask.hidden = true; lastCardHtml = ''; lastCardUrl = '';
171      fetch('/api/embeds/resolve', { method: 'POST', headers: headers(), body: JSON.stringify({ input: v }) })
172        .then(function (r) { return r.json().then(function (d) { return { status: r.status, d: d }; }); })
173        .then(function (res) {
174          go.disabled = false;
175          var d = res.d || {};
176          if (d.ok && d.html) {
177            if (opts.onDone) opts.onDone(d.html, { kind: 'embed', provider: d.provider, name: d.name, result: d });
178            opts.onCancel = null; close();
179            return;
180          }
181          if (d.reason === 'unsupported') {
182            say((d.host || 'That site') + ' is not one we can show inside your page yet. You can add it as a link, and it will open in a new tab.');
183            lastCardHtml = d.card_html || '';
184            lastCardUrl = d.url || '';
185            if (lastCardHtml) { cardBox.innerHTML = lastCardHtml; cardBox.hidden = false; asCard.hidden = false; }
186            if (d.can_request) showAsk(d);
187            // Nothing more "Embed" can do with this paste (round 7): the button
188            // rests until the coach pastes something else.
189            go.disabled = true;
190            if (opts.onUnsupported) opts.onUnsupported(d, back);
191            return;
192          }
193          if (d.reason === 'not_a_link' || d.reason === 'empty') {
194            say('That does not look like a link. Paste the address from your browser, or the embed code the site gives you.', true);
195            return;
196          }
197          say('Something went wrong checking that link. Please try again.', true);
198        })
199        .catch(function () { go.disabled = false; say('Something went wrong checking that link. Please try again.', true); });
200    };
201    input.addEventListener('input', function () { go.disabled = false; });
202    setTimeout(function () { input.focus(); }, 30);
203    return back;
204  }
205  window.rkEmbedPrompt = openEmbedPrompt;
206
207  function isEmbedEl(el) {
208    return !!(el && el.classList && (el.classList.contains('rk-embed') || el.classList.contains('rk-embed-card')));
209  }
210
211  window.initEmbedBlock = function (ed) {
212    if (!ed || ed.__rkEmbed) return;
213    ed.__rkEmbed = true;
214    var loaded = false;
215
216    ed.DomComponents.addType('rk-embed', {
217      isComponent: function (el) { return isEmbedEl(el) ? { type: 'rk-embed' } : false; },
218      model: { defaults: { droppable: false, editable: false, copyable: true, name: 'Embed' } },
219    });
220
221    ed.BlockManager.add('embed-block', {
222      label: 'Embed', category: '🔤 Basic',
223      media: '<svg viewBox="0 0 24 24" width="44" height="44" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2.5"/><path d="M9.5 9.5 7 12l2.5 2.5M14.5 9.5 17 12l-2.5 2.5"/></svg>',
224      content: PLACEHOLDER,
225    });
226
227    function edit(comp, value) {
228      openEmbedPrompt({
229        value: value || '',
230        onDone: function (html) {
231          try {
232            var added = comp.replaceWith(html);
233            var first = Array.isArray(added) ? added[0] : added;
234            if (first && ed.select) ed.select(first);
235          } catch (e) { console.error('[embed-block] could not place the embed:', e); }
236        },
237      });
238    }
239
240    function compFrom(el) {
241      for (var n = el; n; n = n.parentNode) {
242        if (isEmbedEl(n)) {
243          var v = n.__gjsv;
244          if (v && v.model) return v.model;
245        }
246      }
247      return null;
248    }
249
250    ed.on('load', function () { loaded = true; wire(); });
251    ed.on('canvas:frame:load', wire);
252    function wire() {
253      try {
254        var doc = ed.Canvas.getDocument();
255        if (!doc || doc.__rkEmbedWired) return;
256        doc.__rkEmbedWired = true;
257        // Capture phase, so an embedded video's double-click opens THIS box
258        // rather than the video library's picker.
259        doc.addEventListener('dblclick', function (e) {
260          var comp = compFrom(e.target);
261          if (!comp) return;
262          e.stopImmediatePropagation(); e.preventDefault();
263          var el = comp.getEl && comp.getEl();
264          var src = el && (el.getAttribute('data-embed-source') || el.getAttribute('data-video-url')) || '';
265          edit(comp, src);
266        }, true);
267      } catch (e) { /* canvas not ready yet: load / frame-load wire it */ }
268    }
269
270    // A freshly dropped Embed block asks for its link straight away.
271    ed.on('component:add', function (comp) {
272      try {
273        if (!loaded) return;
274        var el = comp.getEl && comp.getEl();
275        if (!el || !el.getAttribute || el.getAttribute('data-embed-empty') !== '1') return;
276        // Cancelled before anything was pasted: the empty box goes, so a
277        // dashed "paste here" can never reach a published page.
278        setTimeout(function () {
279          openEmbedPrompt({
280            value: '',
281            onDone: function (html) {
282              try { var added = comp.replaceWith(html); var first = Array.isArray(added) ? added[0] : added; if (first && ed.select) ed.select(first); }
283              catch (e2) { console.error('[embed-block] could not place the embed:', e2); }
284            },
285            onCancel: function () { try { comp.remove(); } catch (_) {} },
286          });
287        }, 120);
288      } catch (e) { /* ignore */ }
289    });
290  };
291
292  if (typeof module !== 'undefined' && module.exports) module.exports = { openEmbedPrompt: openEmbedPrompt, PLACEHOLDER: PLACEHOLDER };
293})();

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.