PageSourceSearch

https://sacobaynews.com/sys-live-editor.js?v=1790302613677

js sacobaynews.com collected 2026-09-25 19:10:40 UTC 79,006 bytes, 1,790 lines download raw bytes

1// sys-live-editor.js - Admin Live Builder (Popup-based Overlay Editing)
2// Loads only for logged-in admins (detected via _cms_admin hint cookie)
3// Pattern: hover overlays on editable elements → open dashboard forms in popup iframe
4// Matches legacy Live Builder UX: header bar + element overlays with CRUD buttons
5(function() {
6  'use strict';
7  var _bp = (typeof __CMS_BASE_PATH !== 'undefined' ? __CMS_BASE_PATH : '');
8  console.log('[LiveEditor] _bp =', JSON.stringify(_bp), '| __CMS_BASE_PATH =', typeof __CMS_BASE_PATH !== 'undefined' ? JSON.stringify(__CMS_BASE_PATH) : 'UNDEFINED');
9
10  // Force-reload after navigating away from a deleted detail page
11  try {
12    if (sessionStorage.getItem('cms-lb-force-reload')) {
13      sessionStorage.removeItem('cms-lb-force-reload');
14      location.reload();
15      return;
16    }
17  } catch(e) {}
18
19  // Gate: only run when admin has enabled Live Editor via dashboard toggle
20  if (document.cookie.indexOf('_cms_live_editor=1') === -1) return;
21
22  // Desktop only (same as legacy Live Builder)
23  if (window.innerWidth < 1024) return;
24
25  var P = 'cms-lb'; // CSS prefix
26  var popupVisible = false;
27  var popupEditUrl = ''; // Track which URL was opened (to detect navigation away after save)
28
29  // ── Module path configuration ─────────────────────────────────────
30  // These must match the 'link' column in the modules database table.
31  // Adjust if your site uses different module paths.
32  var MOD = {
33    articles:     'articles',
34    ads_groups:   'customersadsgroups',
35    ads:          'ads',
36    menus:        'menus',
37    menus_subs:   'menus_subs',
38    polls:        'polls',
39    logo:         'settings',
40    sections:     'sections',
41    galleries:    'galleries',
42    events:       'events',
43    classifieds:  'classifieds',
44    subscribers:  'subscribers'
45  };
46
47  // ── Menu lookup (for presetting menu dropdown on New Article) ──
48  // Fetches an existing article's data to get its menu string
49  var _menuByArticle = {};
50  function getMenuFromArticle(articleId, cb) {
51    var cached = _menuByArticle[articleId];
52    if (cached !== undefined) return cb(cached);
53    fetch(_bp + '/api/data/articles/' + articleId).then(function(r) { return r.json(); }).then(function(data) {
54      var menuVal = data && data.menu ? data.menu : null;
55      _menuByArticle[articleId] = menuVal;
56      cb(menuVal);
57    }).catch(function() { cb(null); });
58  }
59
60  // ── CSS Styles ────────────────────────────────────────────────────
61
62  function injectStyles() {
63    var css = '';
64
65    // ---- Header Bar ----
66    css += '#'+P+'-hdr{position:fixed;top:0;left:0;width:100%;height:48px;background:#2d1b69;' +
67      'display:flex;align-items:center;justify-content:space-between;padding:0 20px;z-index:99990;' +
68      'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-size:13px;' +
69      'box-shadow:0 2px 12px rgba(0,0,0,0.3);color:#fff;box-sizing:border-box}';
70    css += '#'+P+'-hdr *{box-sizing:border-box}';
71    css += '#'+P+'-hdr .lbl{font-size:17px;font-weight:700;letter-spacing:0.5px;white-space:nowrap}';
72    css += '#'+P+'-hdr .lnks{display:flex;align-items:center;gap:2px}';
73    css += '#'+P+'-hdr .lnks a,#'+P+'-hdr .lnks>button{display:flex;align-items:center;gap:6px;' +
74      'padding:8px 14px;color:#e2e8f0;background:transparent;border:none;cursor:pointer;' +
75      'text-decoration:none;font-size:13px;border-radius:6px;transition:background .15s;' +
76      'font-family:inherit;white-space:nowrap}';
77    css += '#'+P+'-hdr .lnks a:hover,#'+P+'-hdr .lnks>button:hover{background:rgba(255,255,255,0.15);color:#fff;text-decoration:none}';
78
79    // Push body down for header
80    css += 'body.'+P+'-active{padding-top:48px !important}';
81
82    // ---- Element Overlay ----
83    css += '.'+P+'-el{position:relative !important;transition:outline .15s}';
84    css += '.'+P+'-ov{position:absolute;top:0;left:0;right:0;bottom:0;' +
85      'background:rgba(0,0,0,0.55);display:flex;flex-direction:column;align-items:center;' +
86      'justify-content:center;gap:8px;opacity:0;pointer-events:none;transition:opacity .2s;z-index:100;' +
87      'border:2px dashed rgba(255,255,255,0.5)}';
88    css += '.'+P+'-el:hover>.'+P+'-ov{opacity:1;pointer-events:auto}';
89
90    // Overlay label (top badge)
91    css += '.'+P+'-ov-lbl{position:absolute;top:8px;left:50%;transform:translateX(-50%);' +
92      'background:rgba(0,0,0,0.75);color:#fff;padding:4px 14px;border-radius:4px;font-size:12px;' +
93      'font-weight:700;white-space:nowrap;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' +
94      'letter-spacing:0.3px}';
95
96    // Overlay action buttons
97    css += '.'+P+'-ov-btns{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:center}';
98    css += '.'+P+'-ov-btn{display:inline-flex;align-items:center;gap:5px;padding:9px 18px;border:none;' +
99      'border-radius:6px;cursor:pointer;font-size:12px;font-weight:700;text-transform:uppercase;' +
100      'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' +
101      'transition:background .15s,transform .1s;letter-spacing:0.5px;text-decoration:none;color:#fff}';
102    css += '.'+P+'-ov-btn:hover{transform:scale(1.05);text-decoration:none;color:#fff}';
103    css += '.'+P+'-ov-btn.edit{background:#4a90d9}';
104    css += '.'+P+'-ov-btn.edit:hover{background:#3a7bc8}';
105    css += '.'+P+'-ov-btn.new{background:#27ae60}';
106    css += '.'+P+'-ov-btn.new:hover{background:#1e8449}';
107    css += '.'+P+'-ov-btn.del{background:#e74c3c}';
108    css += '.'+P+'-ov-btn.del:hover{background:#c0392b}';
109    css += '.'+P+'-ov-btn.help{background:#95a5a6}';
110    css += '.'+P+'-ov-btn.help:hover{background:#7f8c8d}';
111    css += '.'+P+'-ov-btn.view{background:#8e44ad}';
112    css += '.'+P+'-ov-btn.view:hover{background:#7d3c98}';
113
114    // Overlay info bar (bottom)
115    css += '.'+P+'-ov-info{position:absolute;bottom:6px;left:8px;right:8px;display:flex;' +
116      'justify-content:space-between;font-size:11px;color:#fff;' +
117      'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}';
118    css += '.'+P+'-ov-info span{background:rgba(0,0,0,0.6);padding:3px 8px;border-radius:3px}';
119
120    // ---- Menu inline edit icons (inline approach — avoids overflow clipping) ----
121    css += '.'+P+'-mi-icons{display:none;margin-left:3px;vertical-align:middle;white-space:nowrap;gap:2px}';
122    css += '.'+P+'-mi:hover .'+P+'-mi-icons{display:inline-flex}';
123    css += '.'+P+'-mi-btn{width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;' +
124      'border:none;border-radius:4px;cursor:pointer;font-size:15px;transition:background .15s;color:#fff;' +
125      'vertical-align:middle;line-height:1;padding:0}';
126    css += '.'+P+'-mi-btn.add{font-size:22px;font-weight:bold}';
127    css += '.'+P+'-mi-btn.view{font-size:18px}';
128    css += '.'+P+'-mi-btn.edit{background:#4a90d9}';
129    css += '.'+P+'-mi-btn.edit:hover{background:#3a7bc8}';
130    css += '.'+P+'-mi-btn.add{background:#27ae60}';
131    css += '.'+P+'-mi-btn.add:hover{background:#1e8449}';
132    css += '.'+P+'-mi-btn.del{background:#e74c3c}';
133    css += '.'+P+'-mi-btn.del:hover{background:#c0392b}';
134    css += '.'+P+'-mi-btn.view{background:#8e44ad}';
135    css += '.'+P+'-mi-btn.view:hover{background:#7d3c98}';
136
137    // ---- Article card icons ----
138    css += '.'+P+'-card{position:relative !important}';
139
140    // ---- Popup ----
141    css += '#'+P+'-bkdrop{position:fixed;top:0;left:0;width:100%;height:100%;' +
142      'background:rgba(0,0,0,0.5);z-index:99991;display:none}';
143    css += '#'+P+'-popup{position:fixed;top:2%;left:2%;width:96%;height:96%;z-index:99992;' +
144      'display:none;flex-direction:column;background:#fff;border-radius:10px;overflow:hidden;' +
145      'box-shadow:0 20px 60px rgba(0,0,0,0.4)}';
146    css += '#'+P+'-pop-hdr{display:none}';
147    css += '#'+P+'-pop-title{display:none}';
148    css += '#'+P+'-pop-acts{display:none}';
149    css += '#'+P+'-pop-close{position:absolute;top:6px;right:6px;z-index:10;font-size:28px;' +
150      'background:rgba(0,0,0,0.55);border:none;color:#fff;cursor:pointer;padding:0;' +
151      'border-radius:50%;line-height:1;transition:background .15s;' +
152      'width:36px;height:36px;display:flex;align-items:center;justify-content:center}';
153    css += '#'+P+'-pop-close:hover{background:rgba(0,0,0,0.75)}';
154    css += '#'+P+'-pop-frame{flex:1;width:100%;border:none;background:#f8fafc}';
155
156    // Rebuilding overlay
157    css += '#'+P+'-rebuilding{position:fixed;top:0;left:0;width:100%;height:100%;' +
158      'background:rgba(255,255,255,0.92);z-index:99998;display:none;flex-direction:column;' +
159      'align-items:center;justify-content:center;gap:12px;' +
160      'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}';
161    css += '#'+P+'-rebuilding .spinner{width:40px;height:40px;border:4px solid #e5e7eb;' +
162      'border-top-color:#2d1b69;border-radius:50%;animation:'+P+'-spin 0.8s linear infinite}';
163    css += '@keyframes '+P+'-spin{to{transform:rotate(360deg)}}';
164    css += '#'+P+'-rebuilding .text{font-size:16px;font-weight:600;color:#1e293b}';
165    css += '#'+P+'-rebuilding .sub{font-size:13px;color:#64748b}';
166
167    var s = document.createElement('style');
168    s.id = P + '-css';
169    s.textContent = css;
170    document.head.appendChild(s);
171  }
172
173  // ── Popup System ──────────────────────────────────────────────────
174
175  var bkdrop, popup, popFrame, popTitle, popupFieldFilter, popupPresetFields;
176
177  function createPopup() {
178    bkdrop = document.createElement('div');
179    bkdrop.id = P + '-bkdrop';
180    bkdrop.addEventListener('click', function() { closePopup(false); });
181    document.body.appendChild(bkdrop);
182
183    popup = document.createElement('div');
184    popup.id = P + '-popup';
185
186    var hdr = document.createElement('div');
187    hdr.id = P + '-pop-hdr';
188
189    popTitle = document.createElement('div');
190    popTitle.id = P + '-pop-title';
191    popTitle.innerHTML = '<i class="fa fa-pencil"></i> <span>Edit</span>';
192
193    var acts = document.createElement('div');
194    acts.id = P + '-pop-acts';
195
196    var closeBtn = document.createElement('button');
197    closeBtn.id = P + '-pop-close';
198    closeBtn.innerHTML = '&times;';
199    closeBtn.title = 'Close';
200    closeBtn.addEventListener('click', function() { closePopup(false); });
201
202    acts.appendChild(closeBtn);
203    hdr.appendChild(popTitle);
204    hdr.appendChild(acts);
205    popup.appendChild(hdr);
206    popup.appendChild(closeBtn);
207
208    popFrame = document.createElement('iframe');
209    popFrame.id = P + '-pop-frame';
210    popFrame.src = 'about:blank';
211
212    // Detect when iframe navigates away from edit/new/delete page
213    // (save completed → dashboard redirects to listing → auto-close & refresh)
214    popFrame.addEventListener('load', function() {
215      if (!popupVisible || !popupEditUrl) return;
216      popFrame._loadCount = (popFrame._loadCount || 0) + 1;
217      try {
218        var iframeDoc = popFrame.contentWindow.document;
219        // Hide nav icons (back/home/settings/user) inside live editor popup
220        var _leStyle = iframeDoc.createElement('style');
221        _leStyle.textContent = '.form-nav-top{display:none!important}';
222        iframeDoc.head.appendChild(_leStyle);
223        // Hook Cancel buttons to close popup instantly without refresh
224        var cancelSel = '.btn-cancel, .header-btn-cancel, .popup-header-cancel-btn, a[href*="javascript:history"]';
225        var cancelBtns = iframeDoc.querySelectorAll(cancelSel);
226        for (var c = 0; c < cancelBtns.length; c++) {
227          (function(btn) {
228            btn.addEventListener('click', function(e) {
229              e.preventDefault(); e.stopPropagation();
230              closePopup(false);
231            });
232          })(cancelBtns[c]);
233        }
234        // Also hook any link/button with text "Cancel" or "CANCEL"
235        var allEls = iframeDoc.querySelectorAll('a, button');
236        for (var d = 0; d < allEls.length; d++) {
237          var txt = allEls[d].textContent.trim().toLowerCase();
238          if (txt === 'cancel' || txt === 'no, cancel') {
239            (function(btn) {
240              btn.addEventListener('click', function(e) {
241                e.preventDefault(); e.stopPropagation();
242                closePopup(false);
243              });
244            })(allEls[d]);
245          }
246        }
247        // Filter fields if popupFieldFilter is set
248        if (popupFieldFilter && popupFieldFilter.length) {
249          var allRows = iframeDoc.querySelectorAll('.form-group, .field-row, tr');
250          for (var f = 0; f < allRows.length; f++) {
251            var row = allRows[f];
252            // Check if this row contains a field we want to keep
253            var keepRow = false;
254            for (var g = 0; g < popupFieldFilter.length; g++) {
255              var fieldName = popupFieldFilter[g];
256              // Match by: input name, data-column, label text, or field class
257              var input = row.querySelector('[name="' + fieldName + '"], [data-column="' + fieldName + '"]');
258              var label = row.querySelector('label');
259              var labelText = label ? label.textContent.trim().toLowerCase() : '';
260              if (input || labelText.indexOf(fieldName.toLowerCase()) !== -1 ||
261                  row.className.indexOf(fieldName) !== -1) {
262                keepRow = true;
263                break;
264              }
265            }
266            if (!keepRow && row.querySelector('input, select, textarea, .file-upload, [data-column]')) {
267              row.style.display = 'none';
268            }
269          }
270        }
271        // Pre-set field values (e.g. highlight flag = Yes for new articles)
272        if (popupPresetFields) {
273          for (var pKey in popupPresetFields) {
274            if (!popupPresetFields.hasOwnProperty(pKey)) continue;
275            var pVal = popupPresetFields[pKey];
276            // Try by name, then data-column
277            var pField = iframeDoc.querySelector('[name="' + pKey + '"]') ||
278                         iframeDoc.querySelector('[data-column="' + pKey + '"]');
279            if (pField) {
280              pField.value = pVal;
281              // Trigger change event so any JS listeners update
282              try { pField.dispatchEvent(new Event('change', { bubbles: true })); } catch (ev) {}
283            }
284          }
285        }
286        // (delete is now handled by our own modal, not via iframe)
287      } catch (e) {}
288      // Skip the first load (initial page load of the edit form itself)
289      if (popFrame._loadCount <= 1) return;
290      try {
291        var iframePath = popFrame.contentWindow.location.pathname;
292        // After save, dashboard redirects to listing (no /edit/, /new, /delete/ in path)
293        var isActionPage = iframePath.indexOf('/edit/') !== -1 ||
294                           iframePath.indexOf('/new') !== -1 ||
295                           iframePath.indexOf('/delete/') !== -1;
296        if (!isActionPage) {
297          closePopup(true);
298        }
299      } catch (e) {
300        // Cross-origin or other error — ignore
301      }
302    });
303
304    popup.appendChild(popFrame);
305
306    document.body.appendChild(popup);
307  }
308
309  function openPopup(url, title, opts) {
310    // Navigate directly to the CRUD edit page instead of opening an iframe popup
311    try { sessionStorage.setItem('_le_ref', window.location.href); } catch(e) {}
312    // Prefix basePath for folder-based sites
313    if (_bp && url.charAt(0) === '/' && url.indexOf(_bp) !== 0) url = _bp + url;
314    var sep = url.indexOf('?') === -1 ? '?' : '&';
315    window.location.href = url + sep + '_le=1';
316  }
317
318  function closePopup(refresh) {
319    var editUrl = popupEditUrl;
320    popupVisible = false;
321    bkdrop.style.display = 'none';
322    popup.style.display = 'none';
323    document.body.style.overflow = '';
324    popFrame.src = 'about:blank';
325    popupEditUrl = '';
326    if (refresh) {
327      // If we edited an article while on its detail page, the heading (and URL slug)
328      // may have changed. Fetch the new filename and navigate to it.
329      var artMatch = editUrl && editUrl.match(/\/articles\/edit\/(\d+)/);
330      var pageMatch = location.pathname.match(/-p(\d+)-\d+\.htm$/i);
331      if (artMatch && pageMatch && artMatch[1] === pageMatch[1]) {
332        var artId = artMatch[1];
333        setTimeout(function() {
334          fetch(_bp + '/api/live-editor/lookup?table=articles&pk=article_id&id=' + artId + '&field=file_html', { credentials: 'same-origin' })
335            .then(function(r) { return r.json(); })
336            .then(function(j) {
337              if (j.value) {
338                location.href = _bp + '/' + j.value;
339              } else {
340                location.reload();
341              }
342            })
343            .catch(function() { location.reload(); });
344        }, 2000);
345        return;
346      }
347      // Default: reload current page after short delay
348      setTimeout(function() { location.reload(); }, 1500);
349    }
350  }
351
352  // Delete confirmation modal (standalone, no iframe)
353  var delModal = null;
354  function ensureDeleteModal() {
355    if (delModal) return;
356    var overlay = document.createElement('div');
357    overlay.id = P + '-del-overlay';
358    overlay.innerHTML =
359      '<div id="' + P + '-del-box">' +
360        '<div style="text-align:center;margin:0 auto 16px;width:80px;height:80px">' +
361          '<svg id="' + P + '-del-icon" class="' + P + '-del-icon-animated" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:80px;height:80px">' +
362            '<path class="' + P + '-del-lid" d="M3 6h18" stroke="#dc3545" stroke-width="2" stroke-linecap="round"/>' +
363            '<path class="' + P + '-del-handle" d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="#dc3545" stroke-width="2" stroke-linecap="round"/>' +
364            '<path class="' + P + '-del-body" d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" stroke="#dc3545" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
365            '<path class="' + P + '-del-lines" d="M10 11v6M14 11v6" stroke="#dc3545" stroke-width="2" stroke-linecap="round"/>' +
366          '</svg>' +
367        '</div>' +
368        '<p style="font-size:16px;font-weight:600;margin:0 0 6px;text-align:center">Are you sure you want to delete this item?</p>' +
369        '<p style="font-size:13px;color:#888;margin:0 0 20px;text-align:center;font-style:italic">This action cannot be undone.</p>' +
370        '<div style="display:flex;gap:12px;justify-content:center">' +
371          '<button id="' + P + '-del-cancel" style="padding:10px 24px;border:1px solid #ddd;border-radius:6px;background:#fff;cursor:pointer;font-size:14px;font-weight:500;transition:all .2s">Cancel</button>' +
372          '<button id="' + P + '-del-confirm" style="padding:10px 24px;border:none;border-radius:6px;background:#dc3545;color:#fff;cursor:pointer;font-size:14px;font-weight:500;transition:all .2s">Delete</button>' +
373        '</div>' +
374      '</div>';
375    var dp = P;
376    var style = document.createElement('style');
377    style.textContent =
378      '#' + dp + '-del-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;' +
379        'background:rgba(0,0,0,0.5);z-index:999999;align-items:center;justify-content:center}' +
380      '#' + dp + '-del-box{background:#fff;border-radius:12px;padding:32px 40px;max-width:420px;width:90%;' +
381        'box-shadow:0 8px 32px rgba(0,0,0,0.3);animation:' + dp + 'DelSlideIn .3s ease-out}' +
382      '@keyframes ' + dp + 'DelSlideIn{from{transform:translateY(-50px);opacity:0}
382to{transform:translateY(0);opacity:1}}' +
383      '#' + dp + '-del-confirm:hover{background:#b02a37;transform:translateY(-1px);box-shadow:0 4px 8px rgba(220,53,69,0.3)}' +
384      '#' + dp + '-del-cancel:hover{background:#e9ecef;color:#495057}' +
385      /* Draw animation for each SVG path */
386      '.' + dp + '-del-body{stroke-dasharray:140;stroke-dashoffset:140;animation:' + dp + 'DrawBody .5s ease-out .1s forwards}' +
387      '.' + dp + '-del-lid{stroke-dasharray:32;stroke-dashoffset:32;animation:' + dp + 'DrawLid .3s ease-out forwards}' +
388      '.' + dp + '-del-handle{stroke-dasharray:40;stroke-dashoffset:40;animation:' + dp + 'DrawHandle .3s ease-out .3s forwards}' +
389      '.' + dp + '-del-lines{stroke-dasharray:54;stroke-dashoffset:54;animation:' + dp + 'DrawLines .4s ease-out .6s forwards}' +
390      '@keyframes ' + dp + 'DrawBody{to{stroke-dashoffset:0}}' +
391      '@keyframes ' + dp + 'DrawLid{to{stroke-dashoffset:0}}' +
392      '@keyframes ' + dp + 'DrawHandle{to{stroke-dashoffset:0}}' +
393      '@keyframes ' + dp + 'DrawLines{to{stroke-dashoffset:0}}' +
394      /* Shake animation during deleting */
395      '.' + dp + '-del-icon-animated.' + dp + '-del-shaking{animation:' + dp + 'TrashShake .6s ease-in-out infinite !important;transform-origin:center center !important}' +
396      '.' + dp + '-del-icon-animated.' + dp + '-del-shaking path{animation:none !important;stroke-dashoffset:0 !important}' +
397      '@keyframes ' + dp + 'TrashShake{' +
398        '0%{transform:rotate(0deg) scale(1)}' +
399        '10%{transform:rotate(-10deg) scale(1.05)}' +
400        '20%{transform:rotate(10deg) scale(1.05)}' +
401        '30%{transform:rotate(-10deg) scale(1.05)}' +
402        '40%{transform:rotate(10deg) scale(1.05)}' +
403        '50%{transform:rotate(-10deg) scale(1.05)}' +
404        '60%{transform:rotate(10deg) scale(1.05)}' +
405        '70%{transform:rotate(-10deg) scale(1.05)}' +
406        '80%{transform:rotate(10deg) scale(1.05)}' +
407        '90%{transform:rotate(-10deg) scale(1.05)}' +
408        '100%{transform:rotate(0deg) scale(1)}}';
409    document.head.appendChild(style);
410    document.body.appendChild(overlay);
411    delModal = overlay;
412    document.getElementById(P + '-del-cancel').addEventListener('click', hideDeleteModal);
413    overlay.addEventListener('click', function(e) { if (e.target === overlay) hideDeleteModal(); });
414  }
415  function hideDeleteModal() {
416    if (delModal) delModal.style.display = 'none';
417  }
418  function resetDeleteIcon() {
419    var icon = document.getElementById(P + '-del-icon');
420    if (!icon) return;
421    icon.classList.remove(P + '-del-shaking');
422    // Reset stroke-dashoffset to restart draw animation
423    var paths = icon.querySelectorAll('path');
424    for (var i = 0; i < paths.length; i++) {
425      var p = paths[i];
426      var da = p.style.strokeDasharray || getComputedStyle(p).strokeDasharray;
427      p.style.strokeDashoffset = da;
428      p.style.animation = 'none';
429      p.offsetHeight; // trigger reflow
430      p.style.animation = '';
431    }
432  }
433  function shakeDeleteIcon() {
434    var icon = document.getElementById(P + '-del-icon');
435    if (!icon) return;
436    // Ensure paths are fully drawn before shaking
437    var paths = icon.querySelectorAll('path');
438    for (var i = 0; i < paths.length; i++) {
439      paths[i].style.strokeDashoffset = '0';
440      paths[i].style.animation = 'none';
441    }
442    icon.classList.add(P + '-del-shaking');
443  }
444  function performDelete(modulePath, itemId) {
445    ensureDeleteModal();
446    delModal.style.display = 'flex';
447    resetDeleteIcon();
448    var confirmBtn = document.getElementById(P + '-del-confirm');
449    confirmBtn.textContent = 'Delete';
450    confirmBtn.disabled = false;
451    var icon = document.getElementById(P + '-del-icon');
452    if (icon) icon.classList.remove(P + '-del-shaking');
453    confirmBtn.onclick = function() {
454      confirmBtn.textContent = 'Deleting...';
455      confirmBtn.disabled = true;
456      // Start shake animation
457      setTimeout(shakeDeleteIcon, 100);
458      fetch(_bp + '/api/live-editor/delete', {
459        method: 'POST',
460        headers: { 'Content-Type': 'application/json' },
461        credentials: 'same-origin',
462        body: JSON.stringify({ modulePath: modulePath, itemId: itemId })
463      }).then(function(resp) {
464        return resp.text().then(function(txt) {
465          if (resp.ok) {
466            // Show success in the modal
467            var box = document.getElementById(P + '-del-box');
468            if (box) {
469              box.innerHTML = '<div style="text-align:center;padding:20px">' +
470                '<div style="font-size:48px;color:#28a745;margin-bottom:12px">&#10003;</div>' +
471                '<p style="font-size:16px;font-weight:600;margin:0 0 8px">Deleted successfully!</p>' +
472                '<p style="font-size:13px;color:#888;margin:0">Rebuilding page... refreshing in 5 seconds</p>' +
473                '</div>';
474            }
475            setTimeout(function() {
476              hideDeleteModal();
477              // If on an article detail page, redirect to homepage since the page no longer exists
478              if (location.pathname.match(/-p\d+-\d+\.htm$/i)) {
479                try { sessionStorage.setItem(P + '-force-reload', '1'); } catch(e) {}
480                window.location.href = _bp + '/';
481              } else {
482                location.reload();
483              }
484            }, 5000);
485          } else {
486            throw new Error(resp.status + ': ' + txt);
487          }
488        });
489      }).catch(function(err) {
490        if (icon) icon.classList.remove(P + '-del-shaking');
491        confirmBtn.textContent = 'Delete';
492        confirmBtn.disabled = false;
493        alert('Delete failed:\n' + err.message);
494      });
495    };
496  }
497
498  // Show a "Rebuilding..." overlay with countdown, then reload
499  function showRebuildingOverlay() {
500    var ov = document.getElementById(P + '-rebuilding');
501    if (!ov) {
502      ov = document.createElement('div');
503      ov.id = P + '-rebuilding';
504      ov.innerHTML = '<div class="spinner"></div>' +
505        '<div class="text">Page is rebuilding...</div>' +
506        '<div class="sub">Refreshing in <span id="'+P+'-countdown">5</span> seconds</div>';
507      document.body.appendChild(ov);
508    }
509    ov.style.display = 'flex';
510
511    var seconds = 5;
512    var countdownEl = document.getElementById(P + '-countdown');
513    var timer = setInterval(function() {
514      seconds--;
515      if (countdownEl) countdownEl.textContent = seconds;
516      if (seconds <= 0) {
517        clearInterval(timer);
518        // Force reload from server (HTML has no-cache headers so this gets fresh content)
519        location.reload();
520      }
521    }, 1000);
522  }
523
524  // Global functions for iframe communication
525  window.cms_lb_reload = function() { closePopup(true); };
526  window.cms_lb_close = function() { closePopup(false); };
527
528  // ── Header Bar ────────────────────────────────────────────────────
529
530  function createHeaderBar() {
531    var hdr = document.createElement('div');
532    hdr.id = P + '-hdr';
533
534    // Left: label
535    var lbl = document.createElement('div');
536    lbl.className = 'lbl';
537    lbl.textContent = 'LIVE EDITOR';
538
539    // Right: links
540    var lnks = document.createElement('div');
541    lnks.className = 'lnks';
542
543    // Dashboard (open in new tab)
544    var dashBtn = document.createElement('a');
545    dashBtn.href = _bp + '/dashboard';
546    dashBtn.target = '_blank';
547    dashBtn.innerHTML = '<i class="fa fa-tachometer"></i> Dashboard';
548    lnks.appendChild(dashBtn);
549
550    // Refresh
551    var refreshBtn = document.createElement('button');
552    refreshBtn.innerHTML = '<i class="fa fa-refresh"></i> Refresh';
553    refreshBtn.addEventListener('click', function() { location.reload(true); });
554    lnks.appendChild(refreshBtn);
555
556    // Turn Off live editor
557    var offBtn = document.createElement('button');
558    offBtn.innerHTML = '<i class="fa fa-power-off"></i> Turn Off';
559    offBtn.addEventListener('click', function() {
560      document.cookie = '_cms_live_editor=; path=/; max-age=0';
561      location.reload();
562    });
563    lnks.appendChild(offBtn);
564
565    // Logout
566    var logoutBtn = document.createElement('a');
567    logoutBtn.href = _bp + '/auth/logout';
568    logoutBtn.innerHTML = '<i class="fa fa-sign-out"></i> Logout';
569    lnks.appendChild(logoutBtn);
570
571    hdr.appendChild(lbl);
572    hdr.appendChild(lnks);
573    document.body.appendChild(hdr);
574    document.body.classList.add(P + '-active');
575  }
576
577  // ── Overlay Builder ───────────────────────────────────────────────
578
579  // Add a hover overlay with label + action buttons to an element.
580  // config: { label, buttons: [{ text, icon, cls, action }], info: { left, right } }
581  function addOverlay(el, config) {
582    el.classList.add(P + '-el');
583
584    var ov = document.createElement('div');
585    ov.className = P + '-ov';
586
587    // Label badge
588    if (config.label) {
589      var lbl = document.createElement('div');
590      lbl.className = P + '-ov-lbl';
591      lbl.textContent = config.label;
592      ov.appendChild(lbl);
593    }
594
595    // Action buttons
596    if (config.buttons && config.buttons.length) {
597      var btns = document.createElement('div');
598      btns.className = P + '-ov-btns';
599      for (var i = 0; i < config.buttons.length; i++) {
600        var b = config.buttons[i];
601        var btn = document.createElement('button');
602        btn.className = P + '-ov-btn ' + (b.cls || 'edit');
603        btn.innerHTML = '<i class="fa fa-' + (b.icon || 'pencil') + '"></i> ' + b.text;
604        (function(action) {
605          btn.addEventListener('click', function(e) {
606            e.preventDefault();
607            e.stopPropagation();
608            action();
609          });
610        })(b.action);
611        btns.appendChild(btn);
612      }
613      ov.appendChild(btns);
614    }
615
616    // Info bar (bottom)
617    if (config.info) {
618      var info = document.createElement('div');
619      info.className = P + '-ov-info';
620      if (config.info.left) {
621        var sl = document.createElement('span');
622        sl.textContent = config.info.left;
623        info.appendChild(sl);
624      }
625      if (config.info.right) {
626        var sr = document.createElement('span');
627        sr.textContent = config.info.right;
628        info.appendChild(sr);
629      }
630      ov.appendChild(info);
631    }
632
633    el.appendChild(ov);
634  }
635
636  // ── Detect & Overlay: Ad Groups ───────────────────────────────────
637
638  function detectAds() {
639    var found = {};
640
641    // Strategy 1: data-cms-el="adgroup" markers (from LayoutGenerator)
642    var cmsEls = document.querySelectorAll('[data-cms-el="adgroup"]');
643    for (var i = 0; i < cmsEls.length; i++) {
644      var groupId = cmsEls[i].getAttribute('data-cms-group');
645      // The parent of the data-cms-el span is the container from the layout template
646      var container = cmsEls[i].parentElement;
647      if (container && groupId && !found['g' + groupId]) {
648        found['g' + groupId] = true;
649        addAdOverlay(container, groupId);
650      }
651    }
652
653    // Strategy 2: data-adgroup attribute (legacy layout templates)
654    var adgEls = document.querySelectorAll('[data-adgroup]');
655    for (var j = 0; j < adgEls.length; j++) {
656      var gid = adgEls[j].getAttribute('data-adgroup');
657      if (gid && !found['g' + gid]) {
658        found['g' + gid] = true;
659        addAdOverlay(adgEls[j], gid);
660      }
661    }
662
663    // Strategy 3: .hpe-i-ad cells → find parent containers
664    var adCells = document.querySelectorAll('.hpe-i-ad[data-adnum]');
665    for (var k = 0; k < adCells.length; k++) {
666      var gnum = adCells[k].getAttribute('data-adnum');
667      if (gnum && !found['g' + gnum]) {
668        found['g' + gnum] = true;
669        // Walk up to find a reasonable container (div with AdGroup class or data-adgroup)
670        var cont = findAdContainer(adCells[k]);
671        if (cont) addAdOverlay(cont, gnum);
672      }
673    }
674  }
675
676  function findAdContainer(el) {
677    var node = el;
678    for (var i = 0; i < 8; i++) {
679      node = node.parentElement;
680      if (!node || node === document.body) return el.closest('table') || el;
681      if (node.getAttribute('data-adgroup')) return node;
682      if (node.className && /AdGroup/i.test(node.className)) return node;
683      if (node.tagName === 'DIV' && node.offsetWidth > 50) return node;
684    }
685    return el.closest('table') || el;
686  }
687
688  function addAdOverlay(container, groupId) {
689    // Find individual ad cells within the group
690    var adCells = container.querySelectorAll('.hpe-i-ad[data-adid][data-adnum="' + groupId + '"]');
691
692    if (adCells.length > 1) {
693      // Multiple ads: per-ad icons on each cell, group label badge on container
694      container.classList.add(P + '-card');
695      container.style.position = 'relative';
696      var badge = document.createElement('div');
697      badge.className = P + '-ov-lbl';
698      badge.style.cssText = 'position:absolute;top:4px;left:50%;transform:translateX(-50%);z-index:50;opacity:0;transition:opacity .2s;pointer-events:none;' +
699        'background:rgba(0,0,0,0.75);color:#fff;padding:4px 14px;border-radius:4px;font-size:12px;white-space:nowrap';
700      badge.textContent = 'AdGroup - ' + groupId;
701      container.appendChild(badge);
702      container.addEventListener('mouseenter', function() { badge.style.opacity = '1'; });
703      container.addEventListener('mouseleave', function() { badge.style.opacity = '0'; });
704
705      for (var i = 0; i < adCells.length; i++) {
706        var adId = adCells[i].getAttribute('data-adid');
707        if (adId) addAdItemIcons(adCells[i], adId, groupId);
708      }
709    } else if (adCells.length === 1) {
710      // Single ad: overlay on the ad cell itself
711      addAdItemIcons(adCells[0], adCells[0].getAttribute('data-adid'), groupId);
712    } else {
713      // No individual ad cells found (rotation type showing one at a time)
714      // Use standard overlay with ad-level CRUD only
715      addOverlay(container, {
716        label: 'AdGroup - ' + groupId,
717        buttons: [
718          { text: 'NEW', icon: 'plus', cls: 'new', action: function() {
719            openPopup('/' + MOD.ads_groups + '/details/new/' + groupId, 'New Ad');
720          }}
721        ]
722      });
723    }
724  }
725
726  function addAdItemIcons(cell, adId, groupId) {
727    if (cell.querySelector('.' + P + '-ad-icons')) return;
728    cell.classList.add(P + '-card');
729    cell.style.position = 'relative';
730
731    var icons = document.createElement('span');
732    icons.className = P + '-ad-icons';
733    icons.style.cssText = 'position:absolute;top:4px;right:4px;z-index:50;display:inline-flex;gap:3px;opacity:0;transition:opacity .2s;pointer-events:none';
734
735    var btnStyle = 'width:28px;height:28px;font-size:14px';
736
737    // Edit this ad
738    var editBtn = document.createElement('button');
739    editBtn.className = P + '-mi-btn edit';
740    editBtn.style.cssText = btnStyle;
741    editBtn.innerHTML = '&#9998;';
742    editBtn.title = 'Edit Ad #' + adId;
743    (function(aid, gid) {
744      editBtn.addEventListener('click', function(e) {
745        e.preventDefault(); e.stopPropagation();
746        openPopup('/' + MOD.ads_groups + '/details/edit/' + gid + '/' + aid, 'Edit Ad #' + aid);
747      });
748    })(adId, groupId);
749
750    // New ad in this group
751    var newBtn = document.createElement('button');
752    newBtn.className = P + '-mi-btn add';
753    newBtn.style.cssText = btnStyle;
754    newBtn.innerHTML = '+';
755    newBtn.title = 'New Ad in Group ' + groupId;
756    (function(gid) {
757      newBtn.addEventListener('click', function(e) {
758        e.preventDefault(); e.stopPropagation();
759        openPopup('/' + MOD.ads_groups + '/details/new/' + gid, 'New Ad');
760      });
761    })(groupId);
762
763    // Delete this ad
764    var delBtn = document.createElement('button');
765    delBtn.className = P + '-mi-btn del';
766    delBtn.style.cssText = btnStyle;
767    delBtn.innerHTML = '&#10005;';
768    delBtn.title = 'Delete Ad #' + adId;
769    (function(aid) {
770      delBtn.addEventListener('click', function(e) {
771        e.preventDefault(); e.stopPropagation();
772        performDelete('ads', aid);
773      });
774    })(adId);
775
776    icons.appendChild(editBtn);
777    icons.appendChild(newBtn);
778    icons.appendChild(delBtn);
779    cell.appendChild(icons);
780
781    cell.addEventListener('mouseenter', function() { icons.style.opacity = '1'; icons.style.pointerEvents = 'auto'; });
782    cell.addEventListener('mouseleave', function() { icons.style.opacity = '0'; icons.style.pointerEvents = 'none'; });
783  }
784
785  // ── Detect & Overlay: Logo ────────────────────────────────────────
786
787  function detectLogo() {
788    // Strategy 1: data-cms-el="logo" marker
789    var cmsLogo = document.querySelector('[data-cms-el="logo"]');
790    if (cmsLogo) {
791      var logoContainer = cmsLogo.parentElement || cmsLogo;
792      addLogoOverlay(logoContainer);
793      return;
794    }
795
796    // Strategy 2: .logo or .hpe-logo class
797    var logoEl = document.querySelector('.hpe-logo') || document.querySelector('.logo');
798    if (logoEl) {
799      addLogoOverlay(logoEl);
800      return;
801    }
802  }
803
804  function addLogoOverlay(container) {
805    container.classList.add(P + '-card');
806    container.style.position = 'relative';
807
808    var icons = document.createElement('span');
809    icons.className = P + '-hl-icons';
810    icons.style.cssText = 'position:absolute;top:4px;right:4px;z-index:50;display:inline-flex;gap:3px;opacity:0;transition:opacity .2s;pointer-events:none';
811
812    var btnStyle = 'width:28px;height:28px;font-size:14px';
813
814    // Edit
815    var editBtn = document.createElement('button');
816    editBtn.className = P + '-mi-btn edit';
817    editBtn.style.cssText = btnStyle;
818    editBtn.innerHTML = '&#9998;';
819    editBtn.title = 'Edit Logo';
820    editBtn.addEventListener('click', function(e) {
821      e.preventDefault(); e.stopPropagation();
822      openPopup('/' + MOD.logo, 'Edit Logo', { fields: ['logo'] });
823    });
824
825    icons.appendChild(editBtn);
826    container.appendChild(icons);
827
828    container.addEventListener('mouseenter', function() { icons.style.opacity = '1'; icons.style.pointerEvents = 'auto'; container.style.backgroundColor = 'rgba(74,144,217,0.08)'; });
829    container.addEventListener('mouseleave', function() { icons.style.opacity = '0'; icons.style.pointerEvents = 'none'; container.style.backgroundColor = ''; });
830  }
831
832  // ── Detect & Overlay: Menus ───────────────────────────────────────
833
834  function detectMenus() {
835    // Strategy 1: data-cms-menu-id attributes (injected by MenuGenerator on all menu types)
836    var mainMenuLinks = document.querySelectorAll('a[data-cms-menu-id]');
837    for (var i = 0; i < mainMenuLinks.length; i++) {
838      var link = mainMenuLinks[i];
839      if (link.style.display === 'none') continue; // Skip hidden custom HTML anchors
840      var menuId = link.getAttribute('data-cms-menu-id');
841      addMenuItemIcons(link, menuId, null, link.textContent.trim());
842    }
843
844    // Strategy 2: data-cms-submenu-id attributes (injected by MenuGenerator)
845    var subMenuLinks = document.querySelectorAll('a[data-cms-submenu-id]');
846    for (var j = 0; j < subMenuLinks.length; j++) {
847      var subLink = subMenuLinks[j];
848      var subMenuId = subLink.getAttribute('data-cms-submenu-id');
849      addMenuItemIcons(subLink, null, subMenuId, subLink.textContent.trim());
850    }
851
852    // Strategy 3: Fallback for pages not yet rebuilt — custom_menu_ classes
853    if (mainMenuLinks.length === 0) {
854      var classLinks = document.querySelectorAll('a[class*="custom_menu_"]');
855      for (var k = 0; k < classLinks.length; k++) {
856        var cl = classLinks[k];
857        if (cl.style.display === 'none') continue;
858        var classMatch = cl.className.match(/custom_menu_(\d+)/);
859        if (classMatch) {
860          addMenuItemIcons(cl, classMatch[1], null, cl.textContent.trim());
861        }
862      }
863    }
864  }
865
866  function addMenuItemIcons(el, menuId, subMenuId, label) {
867    if (el.querySelector('.' + P + '-mi-icons')) return; // Already has icons
868    el.classList.add(P + '-mi');
869
870    var icons = document.createElement('span');
871    icons.className = P + '-mi-icons';
872
873    // Edit button
874    var editBtn = document.createElement('button');
875    editBtn.className = P + '-mi-btn edit';
876    editBtn.innerHTML = '&#9998;'; // pencil unicode
877    if (menuId) {
878      editBtn.title = 'Edit Menu #' + menuId;
879      (function(mid) {
880        editBtn.addEventListener('click', function(e) {
881          e.preventDefault(); e.stopPropagation();
882          openPopup('/' + MOD.menus + '/edit/' + mid, 'Edit Menu #' + mid);
883        });
884      })(menuId);
885    } else if (subMenuId) {
886      editBtn.title = 'Edit Sub-Menu #' + subMenuId;
887      (function(sid) {
888        editBtn.addEventListener('click', function(e) {
889          e.preventDefault(); e.stopPropagation();
890          // Look up parent menu_id to build the detail edit URL
891          fetch(_bp + '/api/live-editor/lookup?table=menus_subs&pk=sub_menu_id&id=' + sid + '&field=menu_id', { credentials: 'same-origin' })
892            .then(function(r) { return r.json(); })
893            .then(function(data) {
894              if (data && data.value) {
895                openPopup('/' + MOD.menus + '/details/edit/' + data.value + '/' + sid, 'Edit Sub-Menu #' + sid);
896              } else {
897                openPopup('/' + MOD.menus, 'Menus');
898              }
899            }).catch(function() {
900              openPopup('/' + MOD.menus, 'Menus');
901            });
902        });
903      })(subMenuId);
904    }
905
906    // Add button (new sub-menu under this menu, or new sub-menu via lookup)
907    var addBtn = document.createElement('button');
908    addBtn.className = P + '-mi-btn add';
909    addBtn.innerHTML = '+';
910    addBtn.title = 'Add Sub-Menu';
911    (function(mid, sid) {
912      addBtn.addEventListener('click', function(e) {
913        e.preventDefault(); e.stopPropagation();
914        if (mid) {
915          // Clicked on a main menu item — shift order_sequences and insert after this menu
916          fetch(_bp + '/api/live-editor/menu-reorder', {
917            method: 'POST',
918            headers: { 'Content-Type': 'application/json' },
919            credentials: 'same-origin',
920            body: JSON.stringify({ afterMenuId: mid })
921          })
922            .then(function(r) { return r.json(); })
923            .then(function(data) {
924              if (data && data.newOrderSequence) {
925                openPopup('/' + MOD.menus + '/new', 'New Menu', { preset: { order_sequence: String(data.newOrderSequence) } });
926              } else {
927                openPopup('/' + MOD.menus + '/new', 'New Menu');
928              }
929            })
930            .catch(function() {
931              openPopup('/' + MOD.menus + '/new', 'New Menu');
932            });
933        } else if (sid) {
934          // Clicked on a sub-menu item — shift order_sequences and insert after this submenu
935          fetch(_bp + '/api/live-editor/menu-reorder', {
936            method: 'POST',
937            headers: { 'Content-Type': 'application/json' },
938            credentials: 'same-origin',
939            body: JSON.stringify({ afterSubMenuId: sid })
940          })
941            .then(function(r) { return r.json(); })
942            .then(function(data) {
943              if (data && data.parentMenuId) {
944                var presetObj = {};
945                if (data.newOrderSequence) presetObj.order_sequence = String(data.newOrderSequence);
946                openPopup('/' + MOD.menus + '/details/new/' + data.parentMenuId, 'New Sub-Menu', { preset: presetObj });
947              } else {
948                openPopup('/' + MOD.menus + '/new', 'New Menu');
949              }
950            }).catch(function() {
951              openPopup('/' + MOD.menus + '/new', 'New Menu');
952            });
953        } else {
954          openPopup('/' + MOD.menus + '/new', 'New Menu');
955        }
956      });
957    })(menuId, subMenuId);
958
959    icons.appendChild(editBtn);
960    icons.appendChild(addBtn);
961
962    // Delete button for menus and submenus
963    if (menuId) {
964      var delBtn = document.createElement('button');
965      delBtn.className = P + '-mi-btn del';
966      delBtn.innerHTML = '&#10005;';
967      delBtn.title = 'Delete Menu #' + menuId;
968      (function(mid) {
969        delBtn.addEventListener('click', function(e) {
970          e.preventDefault(); e.stopPropagation();
971          performDelete(MOD.menus, mid);
972        });
973      })(menuId);
974      icons.appendChild(delBtn);
975    } else if (subMenuId) {
976      var delBtn = document.createElement('button');
977      delBtn.className = P + '-mi-btn del';
978      delBtn.innerHTML = '&#10005;';
979      delBtn.title = 'Delete Sub-Menu #' + subMenuId;
980      (function(sid) {
981        delBtn.addEventListener('click', function(e) {
982          e.preventDefault(); e.stopPropagation();
983          performDelete(MOD.menus_subs, sid);
984        });
985      })(subMenuId);
986      icons.appendChild(delBtn);
987
988      var viewBtn = document.createElement('button');
989      viewBtn.className = P + '-mi-btn view';
990      viewBtn.innerHTML = '&#128065;'; // pointing hand unicode
991      viewBtn.title = 'View index' + subMenuId + '.htm';
992      (function(sid) {
993        viewBtn.addEventListener('click', function(e) {
994          e.preventDefault(); e.stopPropagation();
995          location.href = _bp + '/index' + sid + '.htm';
996        });
997      })(subMenuId);
998      icons.appendChild(viewBtn);
999    }
1000
1001    el.appendChild(icons);
1002  }
1003
1004  // ── Detect & Overlay: Polls ───────────────────────────────────────
1005
1006  function detectPolls() {
1007    var found = {};
1008
1009    // Strategy 1: data-poll-id from PollsJSGenerator
1010    var pollEls = document.querySelectorAll('[data-poll-id]');
1011    for (var i = 0; i < pollEls.length; i++) {
1012      var pid = pollEls[i].getAttribute('data-poll-id');
1013      if (pid && !found['p' + pid]) {
1014        found['p' + pid] = true;
1015        var pollContainer = pollEls[i].closest('.cus-poll-1-container') ||
1016                           pollEls[i].closest('[class*="Poll"]') ||
1017                           pollEls[i].parentElement;
1018        addPollOverlay(pollContainer || pollEls[i], pid);
1019      }
1020    }
1021
1022    // Strategy 2: data-cms-el="poll" from LayoutGenerator
1023    var cmsPollEls = document.querySelectorAll('[data-cms-el="poll"]');
1024    for (var j = 0; j < cmsPollEls.length; j++) {
1025      var pnum = cmsPollEls[j].getAttribute('data-cms-poll');
1026      var cont = cmsPollEls[j].parentElement || cmsPollEls[j];
1027      if (pnum && !found['pn' + pnum]) {
1028        found['pn' + pnum] = true;
1029        // Try to find the poll ID from inside
1030        var innerPoll = cont.querySelector('[data-poll-id]');
1031        var pollId = innerPoll ? innerPoll.getAttribute('data-poll-id') : pnum;
1032        if (!found['p' + pollId]) {
1033          found['p' + pollId] = true;
1034          addPollOverlay(cont, pollId);
1035        }
1036      }
1037    }
1038  }
1039
1040  function addPollOverlay(container, pollId) {
1041    container.classList.add(P + '-card');
1042    container.style.position = 'relative';
1043
1044    var icons = document.createElement('span');
1045    icons.className = P + '-hl-icons';
1046    icons.style.cssText = 'position:absolute;top:4px;right:4px;z-index:50;display:inline-flex;gap:3px;opacity:0;transition:opacity .2s;pointer-events:none';
1047
1048    var btnStyle = 'width:28px;height:28px;font-size:14px';
1049
1050    // Edit
1051    var editBtn = document.createElement('button');
1052    editBtn.className = P + '-mi-btn edit';
1053    editBtn.style.cssText = btnStyle;
1054    editBtn.innerHTML = '&#9998;';
1055    editBtn.title = 'Edit Poll #' + pollId;
1056    (function(pid) {
1057      editBtn.addEventListener('click', function(e) {
1058        e.preventDefault(); e.stopPropagation();
1059        openPopup('/' + MOD.polls + '/edit/' + pid, 'Edit Poll #' + pid);
1060      });
1061    })(pollId);
1062
1063    // New
1064    var newBtn = document.createElement('button');
1065    newBtn.className = P + '-mi-btn add';
1066    newBtn.style.cssText = btnStyle;
1067    newBtn.innerHTML = '+';
1068    newBtn.title = 'New Poll';
1069    newBtn.addEventListener('click', function(e) {
1070      e.preventDefault(); e.stopPropagation();
1071      openPopup('/' + MOD.polls + '/new', 'New Poll');
1072    });
1073
1074    // Delete
1075    var delBtn = document.createElement('button');
1076    delBtn.className = P + '-mi-btn del';
1077    delBtn.style.cssText = btnStyle;
1078    delBtn.innerHTML = '&#10005;';
1079    delBtn.title = 'Delete Poll #' + pollId;
1080    (function(pid) {
1081      delBtn.addEventListener('click', function(e) {
1082        e.preventDefault(); e.stopPropagation();
1083        performDelete(MOD.polls, pid);
1084      });
1085    })(pollId);
1086
1087    icons.appendChild(editBtn);
1088    icons.appendChild(newBtn);
1089    icons.appendChild(delBtn);
1090    container.appendChild(icons);
1091
1092    container.addEventListener('mouseenter', function() { icons.style.opacity = '1'; icons.style.pointerEvents = 'auto'; container.style.backgroundColor = 'rgba(74,144,217,0.08)'; });
1093    container.addEventListener('mouseleave', function() { icons.style.opacity = '0'; icons.style.pointerEvents = 'none'; container.style.backgroundColor = ''; });
1094  }
1095
1096  // ── Detect & Overlay: Article (Detail Page) ───────────────────────
1097
1098  function detectArticleDetail() {
1099    // Strategy 1: data-cms-type="article"
1100    var wrapper = document.querySelector('[data-cms-type="article"]');
1101    if (wrapper) {
1102      var articleId = wrapper.getAttribute('data-cms-id');
1103      if (articleId) {
1104        // Target the article content area, not the outer wrapper with breadcrumb
1105        var articleBody = wrapper.querySelector('.hpe-article-detail') || wrapper;
1106        articleBody.addEventListener('mouseenter', function() { articleBody.style.backgroundColor = 'rgba(74,144,217,0.08)'; });
1107        articleBody.addEventListener('mouseleave', function() { articleBody.style.backgroundColor = ''; });
1108
1109        // Add EDIT/DELETE bar at top of article (not centered overlay — article is too tall)
1110        var bar = document.createElement('div');
1111        bar.style.cssText = 'display:flex;gap:8px;padding:6px 0;margin-bottom:6px;justify-content:center';
1112        var editBtn = document.createElement('button');
1113        editBtn.className = P + '-ov-btn edit';
1114        editBtn.innerHTML = '<i class="fa fa-pencil"></i> EDIT';
1115        editBtn.addEventListener('click', function(e) {
1116          e.preventDefault(); e.stopPropagation();
1117          openPopup('/' + MOD.articles + '/edit/' + articleId, 'Edit Article #' + articleId);
1118        });
1119        var delBtn = document.createElement('button');
1120        delBtn.className = P + '-ov-btn del';
1121        delBtn.innerHTML = '<i class="fa fa-trash"></i> DELETE';
1122        delBtn.addEventListener('click', function(e) {
1123          e.preventDefault(); e.stopPropagation();
1124          performDelete(MOD.articles, articleId);
1125        });
1126        bar.appendChild(editBtn);
1127        bar.appendChild(delBtn);
1128        articleBody.insertBefore(bar, articleBody.firstChild);
1129      }
1130      return;
1131    }
1132
1133    // Strategy 2: URL pattern {slug}-p{id}-{submenu}.htm
1134    var match = location.pathname.match(/-p(\d+)-\d+\.htm$/i);
1135    if (match) {
1136      var aid = match[1];
1137      // No specific wrapper to overlay — toolbar "Edit Article" button handles this
1138      // We don't add an overlay to the entire page
1139    }
1140  }
1141
1142  // ── Detect & Overlay: Article Cards (Listings) ────────────────────
1143
1144  function detectArticleCards() {
1145    // Skip on article detail pages — already handled by detectArticleDetail
1146    if (document.querySelector('[data-cms-type="article"]')) return;
1147    var found = {};
1148
1149    // Collect all article IDs that already have preview or highlight icons — skip them entirely
1150    var existingIcons = document.querySelectorAll('.' + P + '-pv-icons, .' + P + '-hl-icons');
1151    for (var p = 0; p < existingIcons.length; p++) {
1152      var pvParent = existingIcons[p].closest('[data-article]') || existingIcons[p].closest('[data-cms-article-id]') || existingIcons[p].parentElement;
1153      if (pvParent) {
1154        var pvId = pvParent.getAttribute('data-article') || pvParent.getAttribute('data-cms-article-id');
1155        if (pvId) found[pvId] = true;
1156      }
1157    }
1158
1159    // Strategy 1: data-cms-article-id attribute (default layout)
1160    var cards1 = document.querySelectorAll('[data-cms-article-id]');
1161    for (var i = 0; i < cards1.length; i++) {
1162      var aid1 = cards1[i].getAttribute('data-cms-article-id');
1163      if (!aid1 || found[aid1]) continue;
1164      found[aid1] = true;
1165      addArticleCardIcon(cards1[i], aid1);
1166    }
1167
1168    // Strategy 2: data-article attribute (custom section layouts)
1169    var cards2 = document.querySelectorAll('[data-article]');
1170    for (var j = 0; j < cards2.length; j++) {
1171      var aid2 = cards2[j].getAttribute('data-article');
1172      if (!aid2 || found[aid2]) continue;
1173      found[aid2] = true;
1174      addArticleCardIcon(cards2[j], aid2);
1175    }
1176
1177    // Strategy 3: Scan links matching -p{id}-{submenu}.htm (fallback)
1178    var allLinks = document.querySelectorAll('a[href]');
1179    for (var k = 0; k < allLinks.length; k++) {
1180      var link = allLinks[k];
1181      var m = link.href.match(/-p(\d+)-\d+\.htm$/i);
1182      if (!m) continue;
1183      var aid3 = m[1];
1184      if (found[aid3]) continue;
1185      // Walk up to find nearest data-article container or block parent
1186      var container = link.closest('[data-article]');
1187      if (!container) container = findArticleContainer(link);
1188      if (!container) continue;
1189      // Skip only if this exact article already has icons (not a sibling in same container)
1190      var existingCard = container.querySelector('.' + P + '-card-icons[data-aid="' + aid3 + '"]');
1191      if (existingCard) continue;
1192      found[aid3] = true;
1193      addArticleCardIcon(container, aid3);
1194    }
1195  }
1196
1197  function findArticleContainer(link) {
1198    var el = link.parentElement;
1199    for (var depth = 0; el && depth < 8; depth++) {
1200      var tag = el.tagName;
1201      if (tag === 'ARTICLE' || tag === 'LI' || tag === 'TR') return el;
1202      // Look for a row-like div that contains both image and heading columns
1203      if (tag === 'DIV' && el.classList.contains('row')) return el;
1204      if (tag === 'DIV' && el.querySelectorAll('a[href]').length > 1) return el;
1205      el = el.parentElement;
1206    }
1207    return link.parentElement;
1208  }
1209
1210  function addArticleCardIcon(card, aid) {
1211    card.classList.add(P + '-card');
1212    card.style.position = 'relative';
1213
1214    var icons = document.createElement('span');
1215    icons.className = P + '-card-icons';
1216    icons.setAttribute('data-aid', aid);
1217    icons.style.cssText = 'position:absolute;top:4px;right:4px;z-index:50;display:inline-flex;gap:3px;opacity:0;transition:opacity .2s;pointer-events:none';
1218
1219    var btnStyle = 'width:28px;height:28px;font-size:14px';
1220
1221    // Edit
1222    var editBtn = document.createElement('button');
1223    editBtn.className = P + '-mi-btn edit';
1224    editBtn.style.cssText = btnStyle;
1225    editBtn.innerHTML = '&#9998;';
1226    editBtn.title = 'Edit Article #' + aid;
1227    (function(articleId) {
1228      editBtn.addEventListener('click', function(e) {
1229        e.preventDefault(); e.stopPropagation();
1230        openPopup('/' + MOD.articles + '/edit/' + articleId, 'Edit Article #' + articleId);
1231      });
1232    })(aid);
1233
1234    // New — try to detect submenu from nearby article link
1235    var newBtn = document.createElement('button');
1236    newBtn.className = P + '-mi-btn add';
1237    newBtn.style.cssText = btnStyle;
1238    newBtn.innerHTML = '+';
1239    newBtn.title = 'New Article';
1240    (function(cardEl) {
1241      newBtn.addEventListener('click', function(e) {
1242        e.preventDefault(); e.stopPropagation();
1243        var preset = {};
1244        var aLink = cardEl.querySelector('a[href*="-p"]');
1245        if (aLink) {
1246          var sm = aLink.href.match(/-p\d+-(\d+)\.htm$/i);
1247          if (sm) preset.sub_menu_id = sm[1];
1248        }
1249        openPopup('/' + MOD.articles + '/new', 'New Article', { preset: preset });
1250      });
1251    })(card);
1252
1253    // Delete
1254    var delBtn = document.createElement('button');
1255    delBtn.className = P + '-mi-btn del';
1256    delBtn.style.cssText = btnStyle;
1257    delBtn.innerHTML = '&#10005;';
1258    delBtn.title = 'Delete Article #' + aid;
1259    (function(articleId) {
1260      delBtn.addEventListener('click', function(e) {
1261        e.preventDefault(); e.stopPropagation();
1262        performDelete(MOD.articles, articleId);
1263      });
1264    })(aid);
1265
1266    // View
1267    var viewBtn = document.createElement('button');
1268    viewBtn.className = P + '-mi-btn view';
1269    viewBtn.style.cssText = btnStyle;
1270    viewBtn.innerHTML = '&#128065;';
1271    viewBtn.title = 'View Article';
1272    (function(articleId) {
1273      viewBtn.addEventListener('click', function(e) {
1274        e.preventDefault(); e.stopPropagation();
1275        var aLink = card.querySelector('a[href*="-p' + articleId + '-"]');
1276        if (aLink) location.href = aLink.href;
1277      });
1278    })(aid);
1279
1280    icons.appendChild(editBtn);
1281    icons.appendChild(newBtn);
1282    icons.appendChild(delBtn);
1283    icons.appendChild(viewBtn);
1284    card.appendChild(icons);
1285
1286    // Background highlight on hover
1287    card.addEventListener('mouseenter', function() { icons.style.opacity = '1'; icons.style.pointerEvents = 'auto'; card.style.backgroundColor = 'rgba(74,144,217,0.08)'; });
1288    card.addEventListener('mouseleave', function() { icons.style.opacity = '0'; icons.style.pointerEvents = 'none'; card.style.backgroundColor = ''; });
1289  }
1290
1291  // ── Detect & Overlay: Sections / Custom HTML ──────────────────────
1292
1293  var dwContainerSeq = 0;
1294  function detectSections() {
1295    var found = {};
1296
1297    // Strategy 1: hpe-custom-html-N or hpe-custom-text-N classes
1298    var sectionEls = document.querySelectorAll('[class*="hpe-custom-html"],[class*="hpe-custom-text"]');
1299    for (var i = 0; i < sectionEls.length; i++) {
1300      var el = sectionEls[i];
1301      var classMatch = el.className.match(/hpe-custom-(?:html|text)-(\d+)/);
1302      if (!classMatch) continue;
1303      var secNum = classMatch[1];
1304      if (found[secNum]) continue;
1305      found[secNum] = true;
1306      addSectionIcon(el, secNum);
1307    }
1308
1309    // Strategy 2: Detect inline <script> tags containing Section{N}() calls
1310    var allScripts = document.querySelectorAll('script:not([src])');
1311    for (var j = 0; j < allScripts.length; j++) {
1312      var txt = allScripts[j].textContent || '';
1313      // Match standalone Section{N}() calls (not inside document.write chains)
1314      var secMatch = txt.match(/^\s*Section(\d+)\(\)\s*;?\s*$/);
1315      if (!secMatch) continue;
1316      var sid = secMatch[1];
1317      if (found[sid]) continue;
1318      found[sid] = true;
1319      var parent = allScripts[j].parentElement;
1320      if (parent) addSectionIcon(parent, sid);
1321    }
1322
1323    // Strategy 3: Detect Section{N}() embedded in document.write chains (e.g., social icons)
1324    // Find containers, collect <a> elements and section IDs from scripts, pair them in order
1325    var dwContainers = {};
1326    for (var k = 0; k < allScripts.length; k++) {
1327      var stxt3 = allScripts[k].textContent || '';
1328      if (/^\s*Section\d+\(\)\s*;?\s*$/.test(stxt3)) continue;
1329      if (stxt3.indexOf('document.write') === -1) continue;
1330      var re3 = /Section(\d+)\(\)/g;
1331      var em3;
1332      while ((em3 = re3.exec(stxt3)) !== null) {
1333        var par = allScripts[k].parentElement;
1334        if (!par) continue;
1335        // Group by parent element using a unique key
1336        var pKey = par._cmsLbId || (par._cmsLbId = 'p' + (++dwContainerSeq));
1337        if (!dwContainers[pKey]) dwContainers[pKey] = { el: par, ids: [] };
1338        dwContainers[pKey].ids.push(em3[1]);
1339      }
1340    }
1341    for (var cKey in dwContainers) {
1342      var cont = dwContainers[cKey];
1343      var anchors = cont.el.querySelectorAll('a');
1344      for (var n = 0; n < cont.ids.length && n < anchors.length; n++) {
1345        if (!anchors[n].querySelector('.' + P + '-sec-btn')) {
1346          addSectionIcon(anchors[n], cont.ids[n]);
1347        }
1348      }
1349    }
1350  }
1351
1352  function addSectionIcon(el, sectionId) {
1353    if (el.querySelector('.' + P + '-sec-btn')) return;
1354    el.style.position = 'relative';
1355    // Ensure position:relative works on inline elements like <a>
1356    var cs = window.getComputedStyle(el);
1357    if (cs.display === 'inline') el.style.display = 'inline-block';
1358
1359    var btn = document.createElement('button');
1360    btn.className = P + '-sec-btn';
1361    btn.title = 'Edit Section #' + sectionId;
1362    btn.innerHTML = '&#9998;';
1363    btn.style.cssText = 'position:absolute;top:0;right:0;z-index:50;width:20px;height:20px;' +
1364      'display:flex;align-items:center;justify-content:center;background:#4a90d9;color:#fff;' +
1365      'border:none;border-radius:50%;cursor:pointer;font-size:10px;opacity:0;transition:opacity .2s;' +
1366      'box-shadow:0 1px 4px rgba(0,0,0,0.3);pointer-events:none;padding:0;line-height:1';
1367    (function(sid) {
1368      btn.addEventListener('click', function(e) {
1369        e.preventDefault(); e.stopPropagation();
1370        openPopup('/' + MOD.sections + '/edit/' + sid, 'Edit Section #' + sid, { fields: ['section'] });
1371      });
1372    })(sectionId);
1373
1374    el.addEventListener('mouseenter', function() { btn.style.opacity = '1'; btn.style.pointerEvents = 'auto'; el.style.backgroundColor = 'rgba(74,144,217,0.08)'; });
1375    el.addEventListener('mouseleave', function() { btn.style.opacity = '0'; btn.style.pointerEvents = 'none'; el.style.backgroundColor = ''; });
1376
1377    el.appendChild(btn);
1378  }
1379
1380  // ── Detect: Sticky Ads ────────────────────────────────────────────
1381
1382  function detectStickyAds() {
1383    var positions = ['stickyright', 'stickybottom', 'stickyleft', 'stickytop'];
1384    for (var i = 0; i < positions.length; i++) {
1385      var el = document.getElementById(positions[i]);
1386      if (!el) continue;
1387
1388      // Find the ad group number from the script inside
1389      var scripts = el.querySelectorAll('script');
1390      var groupId = null;
1391      for (var s = 0; s < scripts.length; s++) {
1392        var m = scripts[s].textContent.match(/AdGroup(\d+)/);
1393        if (m) { groupId = m[1]; break; }
1394      }
1395      // Also check data-adnum from rendered ads
1396      if (!groupId) {
1397        var adCell = el.querySelector('.hpe-i-ad[data-adnum]');
1398        if (adCell) groupId = adCell.getAttribute('data-adnum');
1399      }
1400
1401      if (groupId) {
1402        addOverlay(el, {
1403          label: 'Sticky Ad - Group ' + groupId,
1404          buttons: [
1405            { text: 'EDIT', icon: 'pencil', cls: 'edit', action: function() {
1406              openPopup('/' + MOD.ads_groups + '/edit/' + groupId, 'Edit Ad Group ' + groupId);
1407            }}
1408          ]
1409        });
1410      }
1411    }
1412  }
1413
1414  // ── Detect & Overlay: Highlights ─────────────────────────────────
1415
1416  function detectHighlights() {
1417    var found = {};
1418
1419    // Strategy 1: data-cms-el="highlight" wrappers (from {{PutHighlight}} placeholder)
1420    var cmsHLs = document.querySelectorAll('[data-cms-el="highlight"]');
1421    for (var i = 0; i < cmsHLs.length; i++) {
1422      var num = cmsHLs[i].getAttribute('data-cms-highlight');
1423      var container = cmsHLs[i].parentElement || cmsHLs[i];
1424      if (num && !found[num]) {
1425        found[num] = true;
1426        addHighlightOverlay(container, num);
1427      }
1428    }
1429
1430    // Strategy 2: containers with cus-blocks-X-* IDs
1431    // The highlight number comes from the GetHighlights{N}() script that generated this block.
1432    // Scan sibling scripts or parent scripts to find which highlight number.
1433    var blockEls = document.querySelectorAll('[id^="cus-blocks"]');
1434    for (var j = 0; j < blockEls.length; j++) {
1435      var el = blockEls[j];
1436      var idMatch = el.id.match(/cus-blocks-?X?-?(\d+)/i);
1437      if (!idMatch) continue;
1438      var blockId = idMatch[1];
1439      if (found['b' + blockId]) continue;
1440      // Try to find highlight number from: parent data-cms attribute, or sibling GetHighlights script
1441      var hlNum = null;
1442      var nearHL = el.closest('[data-cms-el="highlight"]');
1443      if (nearHL) {
1444        hlNum = nearHL.getAttribute('data-cms-highlight');
1445      }
1446      if (!hlNum) {
1447        // Scan sibling/nearby script tags for GetHighlights{N}()
1448        var parent = el.parentElement;
1449        if (parent) {
1450          var sibScripts = parent.querySelectorAll('script:not([src])');
1451          for (var s = 0; s < sibScripts.length; s++) {
1452            var stxt = sibScripts[s].textContent || '';
1453            var sm = stxt.match(/GetHighlights(\d+)\(\)/);
1454            if (sm) { hlNum = sm[1]; break; }
1455          }
1456        }
1457      }
1458      if (!hlNum) continue; // Skip if we can't determine highlight number
1459      if (found[hlNum]) continue; // Skip if already handled by Strategy 1 or 3
1460      found['b' + blockId] = true;
1461      found[hlNum] = true;
1462      addHighlightOverlay(el, hlNum);
1463    }
1464
1465    // Strategy 3: Detect inline <script>GetHighlights{N}();</script> calls
1466    // Works for layouts with direct script calls (not using {{PutHighlight}} placeholder)
1467    var allScripts = document.querySelectorAll('script:not([src])');
1468    for (var k = 0; k < allScripts.length; k++) {
1469      var txt = allScripts[k].textContent || '';
1470      var hlMatch = txt.match(/GetHighlights(\d+)\(\)/);
1471      if (!hlMatch) continue;
1472      var hlNum2 = hlMatch[1];
1473      if (found[hlNum2]) continue;
1474      found[hlNum2] = true;
1475      var cont = allScripts[k].parentElement;
1476      if (cont) addHighlightOverlay(cont, hlNum2);
1477    }
1478  }
1479
1480  function addHighlightOverlay(container, highlightNum) {
1481    // Add 4-icon set (edit, new, delete, view) to individual article cards inside the highlight
1482    var found = {};
1483    var allLinks = container.querySelectorAll('a[href]');
1484    for (var i = 0; i < allLinks.length; i++) {
1485      var link = allLinks[i];
1486      var hrefMatch = link.href.match(/-p(\d+)-\d+\.htm$/i);
1487      if (!hrefMatch) continue;
1488      var artId = hrefMatch[1];
1489      if (found[artId]) continue;
1490      // Find article card container
1491      var card = link.closest('[data-article]');
1492      if (!card) {
1493        var el = link.parentElement;
1494        for (var d = 0; el && d < 6; d++) {
1495          if (el === container) break;
1496          if (el.tagName === 'DIV' && el.querySelectorAll('a[href]').length > 1) { card = el; break; }
1497          if (el.tagName === 'ARTICLE' || el.tagName === 'LI') { card = el; break; }
1498          el = el.parentElement;
1499        }
1500      }
1501      if (!card) card = link.parentElement || link;
1502      if (card.querySelector('.' + P + '-hl-icons')) continue;
1503      found[artId] = true;
1504      addHighlightArticleIcons(card, artId, highlightNum);
1505    }
1506  }
1507
1508  function addHighlightArticleIcons(card, articleId, highlightNum) {
1509    card.classList.add(P + '-card');
1510    card.style.position = 'relative';
1511
1512    var icons = document.createElement('span');
1513    icons.className = P + '-hl-icons';
1514    icons.style.cssText = 'position:absolute;top:4px;right:4px;z-index:50;display:inline-flex;gap:3px;opacity:0;transition:opacity .2s;pointer-events:none';
1515
1516    var hlBtnStyle = 'width:28px;height:28px;font-size:14px';
1517
1518    // Edit
1519    var editBtn = document.createElement('button');
1520    editBtn.className = P + '-mi-btn edit';
1521    editBtn.style.cssText = hlBtnStyle;
1522    editBtn.innerHTML = '&#9998;';
1523    editBtn.title = 'Edit Article #' + articleId;
1524    (function(aid) {
1525      editBtn.addEventListener('click', function(e) {
1526        e.preventDefault(); e.stopPropagation();
1527        openPopup('/' + MOD.articles + '/edit/' + aid, 'Edit Article #' + aid);
1528      });
1529    })(articleId);
1530
1531    // New (pre-set only the highlight flag — menu left blank since any menu can be in any highlight)
1532    var newBtn = document.createElement('button');
1533    newBtn.className = P + '-mi-btn add';
1534    newBtn.style.cssText = hlBtnStyle;
1535    newBtn.innerHTML = '+';
1536    newBtn.title = 'New Article (Highlight ' + highlightNum + ')';
1537    (function(hlNum) {
1538      newBtn.addEventListener('click', function(e) {
1539        e.preventDefault(); e.stopPropagation();
1540        var preset = {};
1541        if (hlNum && /^\d+$/.test(hlNum)) {
1542          preset['highlight' + hlNum + '_flag'] = 'Yes';
1543        }
1544        openPopup('/' + MOD.articles + '/new', 'New Article', { preset: preset });
1545      });
1546    })(highlightNum);
1547
1548    // Delete
1549    var delBtn = document.createElement('button');
1550    delBtn.className = P + '-mi-btn del';
1551    delBtn.style.cssText = hlBtnStyle;
1552    delBtn.innerHTML = '&#10005;';
1553    delBtn.title = 'Delete Article #' + articleId;
1554    (function(aid) {
1555      delBtn.addEventListener('click', function(e) {
1556        e.preventDefault(); e.stopPropagation();
1557        performDelete(MOD.articles, aid);
1558      });
1559    })(articleId);
1560
1561    // View
1562    var viewBtn = document.createElement('button');
1563    viewBtn.className = P + '-mi-btn view';
1564    viewBtn.style.cssText = hlBtnStyle;
1565    viewBtn.innerHTML = '&#128065;';
1566    viewBtn.title = 'View Article';
1567    (function(aid) {
1568      viewBtn.addEventListener('click', function(e) {
1569        e.preventDefault(); e.stopPropagation();
1570        var aLink = card.querySelector('a[href*="-p' + aid + '-"]');
1571        if (aLink) location.href = aLink.href;
1572      });
1573    })(articleId);
1574
1575    icons.appendChild(editBtn);
1576    icons.appendChild(newBtn);
1577    icons.appendChild(delBtn);
1578    icons.appendChild(viewBtn);
1579    card.appendChild(icons);
1580
1581    card.addEventListener('mouseenter', function() { icons.style.opacity = '1'; icons.style.pointerEvents = 'auto'; card.style.backgroundColor = 'rgba(74,144,217,0.08)'; });
1582    card.addEventListener('mouseleave', function() { icons.style.opacity = '0'; icons.style.pointerEvents = 'none'; card.style.backgroundColor = ''; });
1583  }
1584
1585  // ── Detect & Overlay: Previews ──────────────────────────────────
1586
1587  function detectPreviews() {
1588    var found = {};
1589
1590    // Strategy 1: data-cms-el="preview" wrappers (from {{PutPreview{N}}} placeholder)
1591    var cmsEls = document.querySelectorAll('[data-cms-el="preview"]');
1592    for (var i = 0; i < cmsEls.length; i++) {
1593      var subMenuId = cmsEls[i].getAttribute('data-cms-preview');
1594      if (subMenuId && !found[subMenuId]) {
1595        found[subMenuId] = true;
1596        var container = cmsEls[i].parentElement || cmsEls[i];
1597        addPreviewOverlay(container, subMenuId);
1598      }
1599    }
1600
1601    // Strategy 2: Detect <script src="/preview{N}.js"> tags in the DOM
1602    // Works for layouts with direct script tags (not using {{PutPreview}} placeholder)
1603    var previewScripts = document.querySelectorAll('script[src*="/preview"]');
1604    for (var j = 0; j < previewScripts.length; j++) {
1605      var srcMatch = previewScripts[j].src.match(/\/preview(\d+)\.js/i);
1606      if (!srcMatch) continue;
1607      var subId = srcMatch[1];
1608      if (found[subId]) continue;
1609      found[subId] = true;
1610      // The parent of the script tag is the container where preview content was written
1611      var cont = previewScripts[j].parentElement;
1612      if (cont) addPreviewOverlay(cont, subId);
1613    }
1614  }
1615
1616  function addPreviewOverlay(container, subMenuId) {
1617    // Add pencil icons to individual article cards inside the preview
1618    var found = {};
1619    var allLinks = container.querySelectorAll('a[href]');
1620    for (var i = 0; i < allLinks.length; i++) {
1621      var link = allLinks[i];
1622      var hrefMatch = link.href.match(/-p(\d+)-\d+\.htm$/i);
1623      if (!hrefMatch) continue;
1624      var artId = hrefMatch[1];
1625      if (found[artId]) continue;
1626      // Find article card container within the preview
1627      var card = link.closest('[data-article]');
1628      if (!card) {
1629        // Walk up to find a block parent with image+text
1630        var el = link.parentElement;
1631        for (var d = 0; el && d < 6; d++) {
1632          if (el === container) break;
1633          if (el.tagName === 'DIV' && el.querySelectorAll('a[href]').length > 1) { card = el; break; }
1634          if (el.tagName === 'ARTICLE' || el.tagName === 'LI') { card = el; break; }
1635          el = el.parentElement;
1636        }
1637      }
1638      if (!card || card.querySelector('.' + P + '-pv-icons')) continue;
1639      found[artId] = true;
1640      addPreviewArticleIcons(card, artId, subMenuId);
1641    }
1642  }
1643
1644  function addPreviewArticleIcons(card, articleId, subMenuId) {
1645    card.classList.add(P + '-card');
1646    card.style.position = 'relative';
1647
1648    var icons = document.createElement('span');
1649    icons.className = P + '-pv-icons';
1650    icons.style.cssText = 'position:absolute;top:4px;right:4px;z-index:50;display:inline-flex;gap:3px;opacity:0;transition:opacity .2s;pointer-events:none';
1651
1652    var pvBtnStyle = 'width:28px;height:28px;font-size:14px';
1653
1654    // Edit
1655    var editBtn = document.createElement('button');
1656    editBtn.className = P + '-mi-btn edit';
1657    editBtn.style.cssText = pvBtnStyle;
1658    editBtn.innerHTML = '&#9998;';
1659    editBtn.title = 'Edit Article #' + articleId;
1660    (function(aid) {
1661      editBtn.addEventListener('click', function(e) {
1662        e.preventDefault(); e.stopPropagation();
1663        openPopup('/' + MOD.articles + '/edit/' + aid, 'Edit Article #' + aid);
1664      });
1665    })(articleId);
1666
1667    // New (pre-set the menu dropdown to match this preview's submenu)
1668    var newBtn = document.createElement('button');
1669    newBtn.className = P + '-mi-btn add';
1670    newBtn.style.cssText = pvBtnStyle;
1671    newBtn.innerHTML = '+';
1672    newBtn.title = 'New Article (Preview ' + subMenuId + ')';
1673    (function(aid) {
1674      newBtn.addEventListener('click', function(e) {
1675        e.preventDefault(); e.stopPropagation();
1676        getMenuFromArticle(aid, function(menuStr) {
1677          var preset = menuStr ? { menu: menuStr } : null;
1678          openPopup('/' + MOD.articles + '/new', 'New Article', preset ? { preset: preset } : {});
1679        });
1680      });
1681    })(articleId);
1682
1683    // Delete
1684    var delBtn = document.createElement('button');
1685    delBtn.className = P + '-mi-btn del';
1686    delBtn.style.cssText = pvBtnStyle;
1687    delBtn.innerHTML = '&#10005;';
1688    delBtn.title = 'Delete Article #' + articleId;
1689    (function(aid) {
1690      delBtn.addEventListener('click', function(e) {
1691        e.preventDefault(); e.stopPropagation();
1692        performDelete(MOD.articles, aid);
1693      });
1694    })(articleId);
1695
1696    // View
1697    var viewBtn = document.createElement('button');
1698    viewBtn.className = P + '-mi-btn view';
1699    viewBtn.style.cssText = pvBtnStyle;
1700    viewBtn.innerHTML = '&#128065;';
1701    viewBtn.title = 'View Article';
1702    (function(aid) {
1703      viewBtn.addEventListener('click', function(e) {
1704        e.preventDefault(); e.stopPropagation();
1705        // Find the article URL from the card's links
1706        var aLink = card.querySelector('a[href*="-p' + aid + '-"]');
1707        if (aLink) location.href = aLink.href;
1708      });
1709    })(articleId);
1710
1711    icons.appendChild(editBtn);
1712    icons.appendChild(newBtn);
1713    icons.appendChild(delBtn);
1714    icons.appendChild(viewBtn);
1715    card.appendChild(icons);
1716
1717    card.addEventListener('mouseenter', function() { icons.style.opacity = '1'; icons.style.pointerEvents = 'auto'; card.style.backgroundColor = 'rgba(74,144,217,0.08)'; });
1718    card.addEventListener('mouseleave', function() { icons.style.opacity = '0'; icons.style.pointerEvents = 'none'; card.style.backgroundColor = ''; });
1719  }
1720
1721  function addPreviewArticleIcon(link, articleId) {
1722    if (link.querySelector('.' + P + '-pv-btn')) return;
1723    var wrapper = link.parentElement || link;
1724    wrapper.style.position = 'relative';
1725
1726    var btn = document.createElement('button');
1727    btn.className = P + '-pv-btn';
1728    btn.title = 'Edit Article #' + articleId;
1729    btn.innerHTML = '&#9998;';
1730    btn.style.cssText = 'position:absolute;top:2px;right:2px;z-index:50;width:22px;height:22px;' +
1731      'display:flex;align-items:center;justify-content:center;background:#4a90d9;color:#fff;' +
1732      'border:none;border-radius:50%;cursor:pointer;font-size:11px;opacity:0;transition:opacity .2s;' +
1733      'box-shadow:0 1px 4px rgba(0,0,0,0.3);pointer-events:none;padding:0;line-height:1';
1734    (function(aid) {
1735      btn.addEventListener('click', function(e) {
1736        e.preventDefault(); e.stopPropagation();
1737        openPopup('/' + MOD.articles + '/edit/' + aid, 'Edit Article #' + aid);
1738      });
1739    })(articleId);
1740
1741    wrapper.addEventListener('mouseenter', function() { btn.style.opacity = '1'; btn.style.pointerEvents = 'auto'; });
1742    wrapper.addEventListener('mouseleave', function() { btn.style.opacity = '0'; btn.style.pointerEvents = 'none'; });
1743
1744    wrapper.appendChild(btn);
1745  }
1746
1747  // ── Keyboard Shortcuts ────────────────────────────────────────────
1748
1749  function setupKeyboard() {
1750    document.addEventListener('keydown', function(e) {
1751      if (e.key === 'Escape' && popupVisible) {
1752        closePopup(false);
1753      }
1754    });
1755  }
1756
1757  // ── Page Info (for toolbar context) ───────────────────────────────
1758
1759  function getArticleIdFromUrl() {
1760    var match = location.pathname.match(/-p(\d+)-\d+\.htm$/i);
1761    return match ? match[1] : null;
1762  }
1763
1764  // ── Init ──────────────────────────────────────────────────────────
1765
1766  function init() {
1767    injectStyles();
1768    createPopup();
1769    createHeaderBar();
1770    setupKeyboard();
1771
1772    // Detect all editable elements on the page
1773    detectAds();
1774    detectLogo();
1775    detectMenus();
1776    detectPolls();
1777    detectArticleDetail();
1778    detectHighlights();
1779    detectPreviews();
1780    detectArticleCards();
1781    detectSections();
1782    detectStickyAds();
1783  }
1784
1785  if (document.readyState === 'loading') {
1786    document.addEventListener('DOMContentLoaded', init);
1787  } else {
1788    init();
1789  }
1790})();

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.