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 = '×'; 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">✓</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 = '✎'; 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 = '✕'; 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 = '✎'; 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 = '✎'; // 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 = '✕'; 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 = '✕'; 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 = '👁'; // 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 = '✎'; 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 = '✕'; 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 = '✎'; 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 = '✕'; 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 = '👁'; 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 = '✎'; 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 = '✎'; 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 = '✕'; 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 = '👁'; 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 = '✎'; 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 = '✕'; 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 = '👁'; 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 = '✎'; 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.