1/* ============================================================ 2 G9 TABS FINAL v3 â Text Tester, Font Showcase, Glyph 3 + Multi-family selector (fontname0..N via data-fid) 4 + Variable font axis sliders (baca tabel `fvar` via opentype.js) 5 6 Perubahan dari v2: 7 - v2 cuma baca #fontname0 -> font family >1 (mis. 18 family) 8 gagal ke-load karena script memang belum pernah "melihat" 9 elemen #fontname1..N. 10 - v3 mengumpulkan SEMUA elemen `.fontname` (0..N), bikin dropdown 11 Family kalau jumlahnya >1, dan fetch on-demand per family lewat 12 AJAX yang sama (tfa_initprevtxtcmap) â endpoint & nonce dipinjam 13 dari #fontname0 karena cuma elemen itu yang punya atributnya. 14 - Variable font: cmap/glyph set memang identik di semua bobot 15 (itu bukan bug, karena variable font cuma interpolasi outline, 16 bukan glyph terpisah per bobot). Yang ditambahkan di sini adalah 17 slider per-axis (dibaca dari tabel `fvar`) yang mengubah 18 `font-variation-settings` secara live pada teks preview. 19 Catatan jujur: perubahan axis ini berlaku untuk semua teks biasa 20 (Tester, Showcase, dan karakter di Glyph yang dirender sebagai 21 teks span), TAPI tidak berlaku pada glyph tanpa kode Unicode yang 22 digambar manual lewat SVG path (opentype.js versi ini tidak 23 menghitung interpolasi outline per-axis, cuma baca outline default). 24 ============================================================ */ 25(function () { 26 'use strict'; 27 28 function ready(fn) { 29 if (document.readyState !== 'loading') fn(); 30 else document.addEventListener('DOMContentLoaded', fn); 31 } 32 33 ready(init); 34 35 function init() { 36 if (!document.body.classList.contains('single-product')) return; 37 38 var fontProbe = document.getElementById('fontname0'); 39 var testerIdEl = document.querySelector('.fontesterid'); 40 var fontGroup = document.querySelector('.fontprevgroup'); 41 42 if (!fontProbe || !testerIdEl || !fontGroup) { 43 console.warn('[g9] Block Type Tester (.fontprevgroup) tidak ditemukan di halaman ini.'); 44 return; 45 } 46 47 var productId = testerIdEl.dataset.id; 48 var endpoint = fontProbe.dataset.endpoint; 49 var nonce = fontProbe.dataset.nonce; 50 51 if (!endpoint || !nonce || !productId) { 52 console.warn('[g9] Data endpoint/nonce/productId tidak lengkap.'); 53 return; 54 } 55 56 // ââ 0. Kumpulkan SEMUA family: #fontname0, #fontname1, ... #fontname{N} ââ 57 var families = collectFamilies(fontGroup); 58 if (!families.length) { 59 console.warn('[g9] Tidak ada elemen .fontname yang ditemukan.'); 60 return; 61 } 62 63 // ââ 1. Bangun struktur wrapper + controls bar + tab nav ââ 64 var wrapper = document.createElement('div'); 65 wrapper.className = 'g9-tabs-wrapper'; 66 67 var controlsBar = document.createElement('div'); 68 controlsBar.className = 'g9-controls-bar'; 69 wrapper.appendChild(controlsBar); 70 71 var familySelectWrap = null; 72 var familySelect = null; 73 if (families.length > 1) { 74 familySelectWrap = document.createElement('div'); 75 familySelectWrap.className = 'g9-family-select'; 76 var famLabel = document.createElement('label'); 77 famLabel.setAttribute('for', 'g9-family-picker'); 78 famLabel.textContent = 'Font Style'; 79 familySelect = document.createElement('select'); 80 familySelect.id = 'g9-family-picker'; 81 families.forEach(function (fam, idx) { 82 var opt = document.createElement('option'); 83 opt.value = String(idx); 84 opt.textContent = fam.displayName; 85 familySelect.appendChild(opt); 86 }); 87 familySelectWrap.appendChild(famLabel); 88 familySelectWrap.appendChild(familySelect); 89 controlsBar.appendChild(familySelectWrap); 90 } 91 92 var axisPanel = document.createElement('div'); 93 axisPanel.className = 'g9-axis-panel'; 94 axisPanel.style.display = 'none'; 95 controlsBar.appendChild(axisPanel); 96 97 var nav = document.createElement('nav'); 98 nav.className = 'g9-tabs-nav'; 99 nav.setAttribute('role', 'tablist'); 100 101 var tabs = [ 102 { id: 'tester', label: 'Text Tester' }, 103 { id: 'showcase', label: 'Font Showcase' }, 104 { id: 'glyph', label: 'Glyph' }, 105 { id: 'info', label: 'Product Info' } 106 ]; 107 108 var panels = {};
109 tabs.forEach(function (tab, idx) { 110 var btn = document.createElement('button'); 111 btn.type = 'button'; 112 btn.dataset.tab = tab.id; 113 btn.setAttribute('role', 'tab'); 114 if (idx === 0) btn.classList.add('is-active'); 115 btn.textContent = tab.label; 116 nav.appendChild(btn); 117 118 var panel = document.createElement('div'); 119 panel.className = 'g9-tab-panel'; 120 panel.id = 'g9tab-' + tab.id; 121 if (idx === 0) panel.classList.add('is-active'); 122 wrapper.appendChild(panel); 123 panels[tab.id] = panel; 124 }); 125 wrapper.appendChild(nav); 126 // urutan visual: controlsBar sudah appended pertama, nav kedua â betulkan urutan DOM 127 wrapper.insertBefore(nav, panels.tester); 128 wrapper.insertBefore(controlsBar, nav); 129 130 fontGroup.insertAdjacentElement('afterend', wrapper); 131 // Sembunyikan, jangan dihapus â data (#fontname0..N dataset, dll) tetap bisa dibaca script tema lain 132 fontGroup.style.display = 'none'; 133 134 // ââ Pindahkan blok "File Size / License options" (.g9productpack) yang 135 // tadinya duduk di baris bawah galeri foto ke tab "Product Info" ini. 136 // Ini DOM MOVE (appendChild memindah node aslinya, bukan menyalin/ 137 // menduplikasi), jadi kontennya, harga, checkbox, dan semua event 138 // handler WooCommerce-nya tetap sama persis â cuma lokasinya berubah, 139 // otomatis membuat baris itu tidak lagi nangkring di sebelah foto utama. ââ 140 var productPack = document.querySelector('.g9productpack'); 141 if (productPack) { 142 panels.info.appendChild(productPack); 143 } 144 145 // ââ Pindahkan juga "Product Detail" (judul + isi deskripsi produk dari 146 // the_content()) ke tab "Product Info" ini, di bawah .g9productpack di 147 // atas â DOM MOVE juga, jadi kontennya (termasuk HTML/gambar apapun di 148 // dalam deskripsi) tetap persis sama, cuma pindah lokasi ke dalam tab. ââ 149 var productDesc = document.querySelector('.g9productdesc'); 150 if (productDesc) { 151 panels.info.appendChild(productDesc); 152 } 153 154 if (!productPack && !productDesc) { 155 panels.info.innerHTML = '<div class="g9-loading-status">Info produk tidak ditemukan di halaman ini.</div>'; 156 } 157 158 var buttons = nav.querySelectorAll('button'); 159 buttons.forEach(function (btn) { 160 btn.addEventListener('click', function () { 161 buttons.forEach(function (b) { b.classList.remove('is-active'); }); 162 this.classList.add('is-active'); 163 wrapper.querySelectorAll('.g9-tab-panel').forEach(function (p) { p.classList.remove('is-active'); }); 164 panels[this.dataset.tab].classList.add('is-active'); 165 }); 166 }); 167 168 // ââ 2. Toast kecil untuk copy-to-clipboard ââ 169 var toast = document.createElement('div'); 170 toast.className = 'g9-toast'; 171 document.body.appendChild(toast); 172 function showToast(msg) { 173 toast.textContent = msg; 174 toast.classList.add('show'); 175 clearTimeout(toast._t); 176 toast._t = setTimeout(function () { toast.classList.remove('show'); }, 1400); 177 } 178 function copyText(str) { 179 if (navigator.clipboard && navigator.clipboard.writeText) { 180 navigator.clipboard.writeText(str).then(function () { showToast('Copied: "' + str + '"'); }); 181 } else { 182 var ta = document.createElement('textarea'); 183 ta.value = str; 184 document.body.appendChild(ta); 185 ta.select(); 186 try { document.execCommand('copy'); showToast('Copied: "' + str + '"'); } catch (e) {} 187 document.body.removeChild(ta); 188 } 189 } 190 191 // ââ 3. State per-family cache (biar ganti-ganti dropdown tidak fetch ulang) ââ 192 var cache = {}; // idx -> { family, format, urlfont, font(opentype)|null, axes: [] } 193 var activeIdx = 0; 194 var appliedFamilyCss = 'g9customfont-' + productId; // dipakai ulang, ganti isi @font-face lewat FontFace API per idx 195 196 // Batas waktu tunggu proses baca glyph/axis lewat opentype.js. Beberapa 197 // format/varian font (mis. WOFF dengan kompresi tertentu, atau OTF 198 // variable berbasis CFF2) bisa membuat opentype.js diam tanpa pernah 199 // memanggil callback-nya (bukan error biasa, tapi benar-benar tidak 200 // pernah selesai). Tanpa batas waktu ini, ketiga tab akan macet 201 // selamanya di "Memuat data font asli...". Dengan timeout, Text Tester 202 // & Font Showcase tetap tampil (mereka tidak butuh opentype.js sama 203 // sekali â cukup FontFace API bawaan browser) dan hanya tab Glyph + 204 // panel weight/axis yang menampilkan pesan "tidak tersedia" bila 205 // memang gagal/lambat. 206 var OPENTYPE_TIMEOUT_MS = 15000; 207
208 function familyCssName(idx) { return 'g9customfont-' + productId + '-' + idx; } 209 210 function setLoadingState(msg) { 211 panels.tester.innerHTML = '<div class="g9-loading-status">' + escapeHtml(msg) + '</div>'; 212 panels.showcase.innerHTML = '<div class="g9-loading-status">' + escapeHtml(msg) + '</div>'; 213 panels.glyph.innerHTML = '<div class="g9-loading-status">' + escapeHtml(msg) + '</div>'; 214 } 215 216 function loadFamily(idx) { 217 activeIdx = idx; 218 var fam = families[idx]; 219 axisPanel.style.display = 'none'; 220 axisPanel.innerHTML = ''; 221 222 if (cache[idx]) { 223 renderCore(cache[idx], fam); 224 renderGlyphAndAxis(cache[idx], fam); 225 return; 226 } 227 228 setLoadingState('Memuat data font asli\u2026'); 229 230 fetchRealFont({ 231 endpoint: endpoint, 232 nonce: nonce, 233 posaidi: productId, 234 fontsource: fam.fid 235 }).then(function (data) { 236 var family = familyCssName(idx); 237 var face = new FontFace(family, 'url(' + data.urlfont + ')'); 238 document.fonts.add(face); 239 240 var entry = { family: family, format: data.format, urlfont: data.urlfont, font: null, axes: [] }; 241 cache[idx] = entry; 242 243 face.load().then(function () { 244 if (activeIdx === idx) applyFontFamily(wrapper, family); 245 }).catch(function (err) { 246 console.warn('[g9] FontFace gagal dimuat untuk "' + fam.displayName + '":', err); 247 }); 248 249 // ââ Tester & Showcase cuma butuh FontFace, TIDAK butuh opentype.js. 250 // Tampilkan segera supaya format apapun (otf/ttf/woff/woff2) tidak 251 // pernah macet di dua tab ini, apapun yang terjadi pada opentype.js. ââ 252 if (activeIdx === idx) renderCore(entry, fam); 253 254 if (data.format === 'woff2') { 255 entry.woff2 = true; 256 renderGlyphAndAxis(entry, fam); 257 cleartrack(endpoint, nonce, data.fontpath); 258 return; 259 } 260 261 panels.glyph.innerHTML = '<div class="g9-loading-status">Memuat data glyph\u2026</div>'; 262 263 withTimeout(loadFontForGlyphData(data.urlfont), OPENTYPE_TIMEOUT_MS) 264 .then(function (font) { 265 entry.font = font; 266 entry.axes = extractVariableAxes(font); 267 }) 268 .catch(function (err) { 269 console.warn('[g9] Data glyph/axis tidak tersedia untuk "' + fam.displayName + '" (' + data.format + '):', err); 270 entry.glyphError = true; 271 }) 272 .then(function () { 273 cleartrack(endpoint, nonce, data.fontpath); // file temp server sudah tidak diperlukan lagi 274 if (activeIdx === idx) renderGlyphAndAxis(entry, fam); 275 }); 276 }).catch(function (err) { 277 console.error('[g9] Gagal mengambil font "' + fam.displayName + '":', err); 278 panels.tester.innerHTML = '<div class="g9-loading-status">Gagal memuat font ini (' + escapeHtml(err.message) + ').</div>'; 279 panels.showcase.innerHTML = ''; 280 panels.glyph.innerHTML = ''; 281 }); 282 } 283 284 // Bungkus promise apapun dengan batas waktu, supaya proses yang macet 285 // (bukan cuma yang eksplisit reject) tetap dianggap gagal setelah N ms. 286 function withTimeout(promise, ms) { 287 return new Promise(function (resolve, reject) { 288 var done = false; 289 var timer = setTimeout(function () { 290 if (done) return; 291 done = true; 292 reject(new Error('Timeout setelah ' + ms + 'ms')); 293 }, ms); 294 promise.then(function (val) { 295 if (done) return; 296 done = true; 297 clearTimeout(timer); 298 resolve(val); 299 }, function (err) { 300 if (done) return; 301 done = true; 302 clearTimeout(timer); 303 reject(err); 304 }); 305 }); 306 } 307 308 // Muat opentype.js (kalau belum ada) lalu parse font untuk data glyph/axis. 309 function loadFontForGlyphData(urlfont) { 310 return new Promise(function (resolve, reject) { 311 loadOpentype(function (err) { 312 if (err) { reject(err); return; } 313 try { 314 window.opentype.load(urlfont, function (loadErr, font) { 315 if (loadErr) { reject(loadErr); return; } 316 resolve(font); 317 }); 318 } catch (syncErr) { 319 // Sebagian versi opentype.js bisa throw langsung (bukan 320 // lewat callback) untuk file yang tidak didukung penuh 321 // (mis. OTF variable/CFF2). Tangkap di sini supaya tidak 322 // jadi uncaught error yang membekukan proses. 323 reject(syncErr); 324 } 325 }); 326 }); 327 } 328 329 // Render Text Tester + Font Showcase. Independen dari opentype.js. 330 function renderCore(entry, fam) { 331 renderTester(panels.tester, fam.displayName); 332 renderShowcase(panels.showcase, fam.displayName); 333 if (document.fonts.check('1em "' + entry.family + '"')) { 334 applyFontFamily(wrapper, entry.family); 335 } 336 } 337 338 // Render tab Glyph + panel slider weight/axis. Bergantung pada opentype.js, 339 // jadi bisa gagal per-format tanpa memengaruhi Tester/Showcase di atas. 340 function renderGlyphAndAxis(entry, fam) { 341 if (entry.woff2) { 342 renderGlyphWoff2Notice(panels.glyph); 343 } else if (entry.font) { 344 try { 345 renderGlyphTab(panels.glyph, entry.font, copyText); 346 } catch (renderErr) { 347 console.error('[g9] Gagal render Glyph tab:', renderErr); 348 panels.glyph.innerHTML = '<div class="g9-glyph-status">Terjadi error saat menampilkan glyph (' + escapeHtml(renderErr.message) + ').</div>'; 349 } 350 } else if (entry.glyphError) { 351 panels.glyph.innerHTML = '<div class="g9-glyph-status">Preview glyph & weight variable tidak didukung browser ini untuk file ' + escapeHtml((entry.format || '').toString()) + ' ini. Text Tester dan Font Showcase di atas tetap memakai font aslinya.</div>'; 352 } else { 353 panels.glyph.innerHTML = '<div class="g9-glyph-status">Data glyph tidak tersedia untuk font ini.</div>'; 354 } 355
356 renderAxisSliders(entry, wrapper, axisPanel); 357 358 if (document.fonts.check('1em "' + entry.family + '"')) { 359 applyFontFamily(wrapper, entry.family); 360 } 361 } 362 363 if (familySelect) { 364 familySelect.addEventListener('change', function () { 365 loadFamily(parseInt(this.value, 10)); 366 }); 367 } 368 369 loadFamily(0); 370 371 console.log('[g9] Tabs v3 siap â', families.length, 'family terdeteksi.'); 372 } 373 374 // ââ Kumpulkan semua elemen .fontname (fontname0..N) + nama tampilan + meta key ââ 375 function collectFamilies(fontGroup) { 376 var nodes = fontGroup.querySelectorAll('.fontname'); 377 var out = []; 378 nodes.forEach(function (node, i) { 379 var fid = node.dataset.fid; 380 if (!fid) return; 381 var displayName = 'Style ' + (i + 1); 382 var next = node.nextElementSibling; 383 if (next && next.classList.contains('fttlfrm')) { 384 var ttl = next.querySelector('.fttl'); 385 if (ttl && ttl.textContent.trim()) displayName = ttl.textContent.trim(); 386 } 387 out.push({ fid: fid, displayName: displayName }); 388 }); 389 return out; 390 } 391 392 // ââ Helper: AJAX request identik dengan yang dipakai tema (tfa_initprevtxtcmap) ââ 393 function fetchRealFont(opts) { 394 return new Promise(function (resolve, reject) { 395 var fd = new FormData(); 396 fd.append('action', 'tfa_initprevtxtcmap'); 397 fd.append('posaidi', opts.posaidi); 398 fd.append('fontsource', opts.fontsource); 399 400 var xhr = new XMLHttpRequest(); 401 xhr.open('POST', opts.endpoint, true); 402 xhr.setRequestHeader('X-WP-Nonce', opts.nonce); 403 xhr.onload = function () { 404 try { 405 var json = JSON.parse(xhr.responseText); 406 if (!json || !json.urlfont) { reject(new Error('Response tidak berisi urlfont')); return; } 407 resolve(json); 408 } catch (e) { reject(e); } 409 }; 410 xhr.onerror = function () { reject(new Error('Network error saat mengambil font')); }; 411 xhr.send(fd); 412 }); 413 } 414 415 // ââ Helper: bersihkan file temp di server (samain dengan cleartrack tema) ââ 416 function cleartrack(endpoint, nonce, fontpath) { 417 if (!fontpath) return; 418 var fd = new FormData(); 419 fd.append('action', 'tfa_cleartrack'); 420 fd.append('fontpath', fontpath); 421 var xhr = new XMLHttpRequest(); 422 xhr.open('POST', endpoint, true); 423 xhr.setRequestHeader('X-WP-Nonce', nonce); 424 xhr.send(fd); 425 } 426 427 // ââ Helper: load opentype.js sekali saja ââ 428 // Sumber utama: build resmi jsdelivr yang sudah terbukti cepat & stabil. 429 // Sumber cadangan: build terbaru dari opentype.js.org (langsung dari 430 // branch master), dicoba HANYA kalau sumber utama gagal â supaya kasus 431 // normal (mayoritas font TTF/OTF/WOFF) tetap secepat sebelumnya dan 432 // tidak kena timeout gara-gara menunggu sumber cadangan yang lebih lambat. 433 var OPENTYPE_CDN_SOURCES = [ 434 'https://cdn.jsdelivr.net/npm/[email protected]/dist/opentype.min.js', 435 'https://opentype.js.org/dist/opentype.min.js' 436 ]; 437 var _otPromise = null; 438 function loadScriptFrom(sources, i) { 439 return new Promise(function (resolve, reject) { 440 if (i >= sources.length) { reject(new Error('Semua sumber opentype.js gagal dimuat.')); return; } 441 var s = document.createElement('script'); 442 s.src = sources[i]; 443 s.async = true; 444 s.onload = function () { 445 if (window.opentype && typeof window.opentype.load === 'function') { 446 resolve(); 447 } else { 448 resolve(loadScriptFrom(sources, i + 1)); 449 } 450 }; 451 s.onerror = function () { resolve(loadScriptFrom(sources, i + 1)); }; 452 document.head.appendChild(s); 453 }); 454 } 455 function loadOpentype(cb) { 456 if (window.opentype && typeof window.opentype.load === 'function') { cb(null); return; } 457 if (!_otPromise) { _otPromise = loadScriptFrom(OPENTYPE_CDN_SOURCES, 0); } 458 _otPromise.then(function () { 459 if (window.opentype && typeof window.opentype.load === 'function') { cb(null); } 460 else { cb(new Error('opentype.js tidak tersedia setelah semua sumber dicoba.')); } 461 }).catch(function (err) { cb(err); }); 462 } 463 464 function applyFontFamily(wrapper, family) { 465 wrapper.querySelectorAll('.font-preview, .preview, .g9-showcase-style-title, .g9-glyph-item .char').forEach(function (el) { 466 el.style.fontFamily = '"' + family + '"'; 467 }); 468 wrapper.dataset.fontFamily = family; 469 } 470 471 function escapeHtml(str) { 472 var div = document.createElement('div'); 473 div.textContent = str; 474 return div.innerHTML; 475 } 476 477 // ââ Variable font: baca tabel fvar via opentype.js ââ 478 function extractVariableAxes(font) { 479 if (!font.tables || !font.tables.fvar || !font.tables.fvar.axes) return []; 480 return font.tables.fvar.axes.map(function (axis) { 481 var label = axis.tag; 482 if (axis.name && axis.name.en) label = axis.name.en; 483 else if (typeof axis.name === 'string') label = axis.name; 484 return { 485 tag: axis.tag, 486 label: label, 487 min: axis.minValue, 488 max: axis.maxValue, 489 def: axis.defaultValue 490 }; 491 }); 492 } 493
494 function renderAxisSliders(entry, wrapper, axisPanel) { 495 axisPanel.innerHTML = ''; 496 if (!entry.axes || !entry.axes.length) { 497 axisPanel.style.display = 'none'; 498 return; 499 } 500 axisPanel.style.display = 'flex'; 501 502 var currentValues = {}; 503 entry.axes.forEach(function (axis) { currentValues[axis.tag] = axis.def; }); 504 505 function applyVariationSettings() { 506 var css = Object.keys(currentValues).map(function (tag) { 507 return '"' + tag + '" ' + currentValues[tag]; 508 }).join(', '); 509 wrapper.querySelectorAll('.font-preview, .preview, .g9-showcase-style-title, .g9-glyph-item .char').forEach(function (el) { 510 el.style.fontVariationSettings = css; 511 }); 512 } 513 514 entry.axes.forEach(function (axis) { 515 var row = document.createElement('div'); 516 row.className = 'g9-axis-row'; 517 518 var labelRow = document.createElement('div'); 519 labelRow.className = 'g9-axis-label-row'; 520 var label = document.createElement('label'); 521 label.textContent = axis.label + ' (' + axis.tag + ')'; 522 var valueTag = document.createElement('span'); 523 valueTag.className = 'g9-axis-value'; 524 valueTag.textContent = axis.def; 525 labelRow.appendChild(label); 526 labelRow.appendChild(valueTag); 527 528 var input = document.createElement('input'); 529 input.type = 'range'; 530 input.min = axis.min; 531 input.max = axis.max; 532 input.step = (axis.max - axis.min > 20) ? 1 : 0.01; 533 input.value = axis.def; 534 input.addEventListener('input', function () { 535 currentValues[axis.tag] = this.value; 536 valueTag.textContent = this.value; 537 applyVariationSettings(); 538 }); 539 540 row.appendChild(labelRow); 541 row.appendChild(input); 542 axisPanel.appendChild(row); 543 }); 544 545 var note = document.createElement('div'); 546 note.className = 'g9-axis-note'; 547 note.textContent = 'Adjust the font weight to see live changes in the text preview. Image glyphs without Unicode are not affected.'; 548 axisPanel.appendChild(note); 549 550 applyVariationSettings(); 551 } 552 553 // ââ Render Text Tester ââ 554 var G9_DEFAULT_TESTER_TEXT = 'The quick brown fox jumps over the lazy dog'; 555 function renderTester(panel, fontDisplayName) { 556 panel.innerHTML = 557 '<div class="g9-font-tester">' + 558 '<label for="g9-tester-input">Type Tester</label>' + 559 '<input type="text" id="g9-tester-input" class="tester-input" placeholder="Type your text here...">' + 560 '<div class="preview" id="g9-tester-preview">' + escapeHtml(G9_DEFAULT_TESTER_TEXT) + '</div>' + 561 '</div>'; 562 var testerInput = panel.querySelector('#g9-tester-input'); 563 var testerPreview = panel.querySelector('#g9-tester-preview'); 564 testerInput.addEventListener('input', function () { 565 testerPreview.textContent = this.value || G9_DEFAULT_TESTER_TEXT; 566 }); 567 } 568 569 // ââ Render Font Showcase â konten kustom tema "penjaga suar" (bukan tema kompetitor) ââ 570 var SHOWCASE_BLOCKS = [ 571 { title: 'UPPERCASE', text: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' }, 572 { title: 'LOWERCASE', text: 'abcdefghijklmnopqrstuvwxyz' }, 573 { title: 'NUMBERS', text: '0123456789' }, 574 { title: 'SYMBOLS', text: '!@#$%^&*()_+-=[]{}|;:\',.<>?/~`' }, 575 { title: 'FULL NAME', text: 'Prof.Nathaniel Ashworth-Cole, Ph.D' }, 576 { title: 'TITLE', text: 'Keeper of the Nightbeam Light' }, 577 { title: 'ADDRESS', text: '14 Harbor Light Lane, Port Solitude, ME 04654' }, 578 { title: 'PHONE', text: '+1 (207) 555-0148' }, 579 { title: 'EMAIL', text: '[email protected]' }, 580 { title: 'URL', text: 'www.portsolitudelighthouse.com' }, 581 { title: 'SHORT SENTENCE', text: 'The beacon burned steady through the fog.' }, 582 { title: 'MEDIUM SENTENCE', text: 'Every night for forty years, Elias climbed the spiral stairs to light the lamp before the tide turned.' }, 583 { 584 title: 'LONG SENTENCE', 585 text: 'Long before the coastal road existed, ships found their way home by a single flame Elias tended alone, ' + 586 'and even now, retired and grey, he still walks up at dusk just to be sure the light is lit.' 587 } 588 ]; 589
590 function renderShowcase(panel, fontDisplayName) { 591 panel.innerHTML = ''; 592 var showcaseDiv = document.createElement('div'); 593 showcaseDiv.className = 'g9-showcase'; 594 595 var styleHeader = document.createElement('div'); 596 styleHeader.className = 'g9-showcase-style-title'; 597 styleHeader.textContent = fontDisplayName; 598 showcaseDiv.appendChild(styleHeader); 599 600 SHOWCASE_BLOCKS.forEach(function (block) { 601 var el = document.createElement('div'); 602 el.className = 'example-block'; 603 el.innerHTML = '<h3><span class="label-chip">' + escapeHtml(block.title) + '</span></h3><div class="font-preview">' + escapeHtml(block.text) + '</div>'; 604 showcaseDiv.appendChild(el); 605 }); 606 panel.appendChild(showcaseDiv); 607 } 608 609 function renderGlyphWoff2Notice(panel) { 610 panel.innerHTML = 611 '<div class="g9-glyph-status">Preview karakter tidak didukung untuk format WOFF2 ' + 612 '(pembatasan yang sama seperti Character Map bawaan tema).</div>'; 613 } 614 615 // ââ Kategorisasi Unicode nyata (dibaca dari glyph.unicodes / glyph.name via opentype.js) ââ 616 var CATEGORY_LABELS = { 617 basic: 'Basic Latin', 618 numeric: 'Numeric', 619 punctuation: 'Punctuation', 620 extended: 'Extended Latin', 621 math: 'Math Symbols', 622 currency: 'Currency', 623 othersymbols: 'Other Symbols', 624 other: 'Other', 625 remaining: 'Remaining Glyphs' 626 }; 627 var CATEGORY_ORDER = ['basic', 'numeric', 'punctuation', 'extended', 'math', 'currency', 'othersymbols', 'other', 'remaining']; 628 629 function categorizeCodepoint(cp) { 630 var ch = String.fromCodePoint(cp); 631 var asciiPunct = '!"#%&\'()*,-./:;?@[\\]_{}'; 632 if (cp === 0x24 || cp === 0xA2 || cp === 0xA3 || cp === 0xA4 || cp === 0xA5 || (cp >= 0x20A0 && cp <= 0x20CF)) return 'currency'; 633 if (cp === 0x2B || cp === 0x3C || cp === 0x3D || cp === 0x3E || cp === 0xB1 || cp === 0xD7 || cp === 0xF7 || (cp >= 0x2200 && cp <= 0x22FF)) return 'math'; 634 if (cp >= 0x30 && cp <= 0x39) return 'numeric'; 635 if (asciiPunct.indexOf(ch) !== -1) return 'punctuation'; 636 if (cp >= 0x2000 && cp <= 0x206F) return 'punctuation'; 637 if (cp === 0x20 || (cp >= 0x41 && cp <= 0x5A) || (cp >= 0x61 && cp <= 0x7A) || cp === 0x5E || cp === 0x60 || cp === 0x7E) return 'basic'; 638 if ((cp >= 0xC0 && cp <= 0xFF) || (cp >= 0x100 && cp <= 0x17F) || (cp >= 0x180 && cp <= 0x24F)) return 'extended'; 639 if ([0xA1, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAE, 0xAF, 0xB0, 0xB4, 0xB6, 0xB7, 0xB8, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0x2122].indexOf(cp) !== -1) return 'othersymbols'; 640 if (cp >= 0x2190 && cp <= 0x21FF) return 'othersymbols'; 641 if (cp >= 0x2600 && cp <= 0x27BF) return 'othersymbols'; 642 return 'other'; 643 } 644 645 function renderGlyphTab(panel, font, copyText) { 646 var buckets = {}; 647 CATEGORY_ORDER.forEach(function (k) { buckets[k] = []; }); 648 649 var total = font.glyphs.length; 650 for (var i = 0; i < total; i++) { 651 var glyph = font.glyphs.get(i); 652 var gname = glyph.name || ''; 653 if (gname === '.notdef' || gname === '.null' || gname === 'nonmarkingreturn') continue; 654 var unicodes = glyph.unicodes || []; 655 if (!unicodes.length) { 656 buckets.remaining.push({ glyph: glyph, name: glyph.name || ('glyph' + i) }); 657 continue; 658 } 659 unicodes.forEach(function (cp) { 660 var cat = categorizeCodepoint(cp); 661 buckets[cat].push({ cp: cp, name: glyph.name || ('U+' + cp.toString(16)), glyph: glyph }); 662 }); 663 } 664 665 panel.innerHTML = ''; 666 var unitsPerEm = font.unitsPerEm || 1000; 667 668 CATEGORY_ORDER.forEach(function (catKey) { 669 var items = buckets[catKey]; 670 if (!items.length) return; 671 672 var section = document.createElement('div'); 673 section.className = 'g9-glyph-section'; 674 var h = document.createElement('h3'); 675 h.textContent = CATEGORY_LABELS[catKey] + ' (' + items.length + ')'; 676 section.appendChild(h); 677 678 var grid = document.createElement('div'); 679 grid.className = 'g9-glyph-grid'; 680 681 items.forEach(function (item) { 682 var cell = document.createElement('div'); 683 cell.className = 'g9-glyph-item'; 684 685 if (catKey === 'remaining') { 686 cell.appendChild(glyphToSvg(item.glyph, unitsPerEm)); 687 cell.title = item.name; 688 cell.addEventListener('click', function () { copyText(item.name); }); 689 } else { 690 var charSpan = document.createElement('span'); 691 charSpan.className = 'char'; 692 charSpan.textContent = String.fromCodePoint(item.cp); 693 cell.appendChild(charSpan); 694 cell.title = item.name; 695 cell.addEventListener('click', function () { copyText(String.fromCodePoint(item.cp)); }); 696 } 697 698 var nameSpan = document.createElement('span'); 699 nameSpan.className = 'gname'; 700 nameSpan.textContent = item.name; 701 cell.appendChild(nameSpan); 702 703 if (item.cp) { 704 var uni = document.createElement('span'); 705 uni.className = 'unicode'; 706 uni.textContent = 'U+' + item.cp.toString(16).toUpperCase().padStart(4, '0');
707 cell.appendChild(uni); 708 } 709 710 var badge = document.createElement('span'); 711 badge.className = 'copy-badge'; 712 badge.textContent = '\ud83d\udccb copy'; 713 cell.appendChild(badge); 714 715 grid.appendChild(cell); 716 }); 717 718 section.appendChild(grid); 719 panel.appendChild(section); 720 }); 721 722 if (!panel.children.length) { 723 panel.innerHTML = '<div class="g9-glyph-status">Tidak ada data glyph yang bisa dibaca dari font ini.</div>'; 724 } 725 } 726 727 // Gambar outline glyph langsung dari path font (untuk glyph tanpa kode Unicode / ligature / alternate) 728 function glyphToSvg(glyph, unitsPerEm) { 729 var path = glyph.getPath(0, 0, unitsPerEm); 730 var d = path.toPathData(2); 731 var ns = 'http://www.w3.org/2000/svg'; 732 var svg = document.createElementNS(ns, 'svg'); 733 svg.setAttribute('viewBox', '0 -' + (unitsPerEm * 0.8) + ' ' + unitsPerEm + ' ' + unitsPerEm); 734 svg.setAttribute('class', 'g9-glyph-svg'); 735 var p = document.createElementNS(ns, 'path'); 736 p.setAttribute('d', d); 737 p.setAttribute('fill', 'currentColor'); 738 p.setAttribute('transform', 'scale(1,-1)'); 739 svg.appendChild(p); 740 return svg; 741 } 742})();
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.