/* ────────────────────────────────────────────────────────────── dom.js — DOM manipulation helper functions ────────────────────────────────────────────────────────────── */ /** * Create an element with optional attributes and children * @param {string} sTag - tag name * @param {object} oAttrs - attribute map * @param {Array|string} aChildren - child nodes or text * @returns {HTMLElement} */ function createElement(sTag, oAttrs, aChildren) { const eEl = document.createElement(sTag); if (oAttrs) { let sKey; for (sKey in oAttrs) { if (sKey === 'className') { eEl.className = oAttrs[sKey]; } else if (sKey === 'textContent') { eEl.textContent = oAttrs[sKey]; } else if (sKey === 'onclick' || sKey === 'oninput') { eEl[sKey] = oAttrs[sKey]; } else { eEl.setAttribute(sKey, oAttrs[sKey]); } } } if (aChildren) { if (typeof aChildren === 'string') { eEl.textContent = aChildren; } else if (Array.isArray(aChildren)) { let i = 0; for (i = 0; i < aChildren.length; i++) { if (aChildren[i]) { if (typeof aChildren[i] === 'string') { eEl.appendChild(document.createTextNode(aChildren[i])); } else { eEl.appendChild(aChildren[i]); } } } } } return eEl; } /** * Shorthand query selector * @param {string} sSelector * @param {HTMLElement} eParent * @returns {HTMLElement|null} */ function qs(sSelector, eParent) { return (eParent || document).querySelector(sSelector); } /** * Shorthand query selector all * @param {string} sSelector * @param {HTMLElement} eParent * @returns {NodeList} */ function qsa(sSelector, eParent) { return (eParent || document).querySelectorAll(sSelector); } /** * Clear all children from an element * @param {HTMLElement} eEl */ function clearChildren(eEl) { while (eEl.firstChild) { eEl.removeChild(eEl.firstChild); } } /** * Toggle a class on an element * @param {HTMLElement} eEl * @param {string} sClass * @param {boolean} bForce */ function toggleClass(eEl, sClass, bForce) { if (typeof bForce !== 'undefined') { if (bForce) { eEl.classList.add(sClass); } else { eEl.classList.remove(sClass); } } else { eEl.classList.toggle(sClass); } } /** * Build a method badge element * @param {string} sMethod - HTTP method * @returns {HTMLElement} */ function buildMethodBadge(sMethod) { let sLower = sMethod.toLowerCase(); return createElement('span', { className: 'method-badge ' + sLower, textContent: sMethod }); } /** * Split endpoint description text from the appended link token. * @param {string} sValue * @returns {{description: string, link: string}} */ function parseEndpointDescription(sValue) { let sText = sValue || ''; let sLink = ''; let oLinkPattern = new RegExp('\\|Link:([^|]+)\\|\\s*$', 'i'); let oMatch = sText.match(oLinkPattern); if (oMatch && oMatch[1]) { sLink = oMatch[1].trim(); sText = sText.replace(oLinkPattern, '').trim(); } return { description: sText, link: sLink }; } /** * Rebuild endpoint description storage format with an optional link token. * @param {string} sDescription * @param {string} sLink * @returns {string} */ function buildEndpointDescription(sDescription, sLink) { let sCleanDescription = (sDescription || '').trim(); let sCleanLink = (sLink || '').trim(); if (!sCleanLink) { return sCleanDescription; } if (!sCleanDescription) { return '|Link:' + sCleanLink + '|'; } return sCleanDescription + ' |Link:' + sCleanLink + '|'; } /** * Allow only safe absolute http(s) links for endpoint rendering. * @param {string} sLink * @returns {string} */ function getSafeEndpointLink(sLink) { let sValue = (sLink || '').trim(); if (!sValue) { return ''; } try { let oUrl = new URL(sValue); if (oUrl.protocol === 'http:' || oUrl.protocol === 'https:') { return oUrl.toString(); } } catch (e) { return ''; } return ''; } /** * Build a parameter table * @param {Array} aParams - array of param objects { name, location, required, type, description } * @returns {HTMLElement|null} */ function buildParamTable(aParams) { if (!aParams || aParams.length === 0) { return null; } let aThead = createElement('thead', null, [ createElement('tr', null, [ createElement('th', null, 'Name'), createElement('th', null, 'In'), createElement('th', null, 'Required'), createElement('th', null, 'Type'), createElement('th', null, 'Description') ]) ]); let aTbody = createElement('tbody'); let i = 0; for (i = 0; i < aParams.length; i++) { let oP = aParams[i]; aTbody.appendChild(createElement('tr', null, [ createElement('td', null, oP.name || ''), createElement('td', null, oP.location || ''), createElement('td', null, oP.required ? 'Yes' : 'No'), createElement('td', null, oP.type || ''), createElement('td', null, oP.description || '') ])); } return createElement('table', { className: 'param-table' }, [aThead, aTbody]); } /** * Build a code preview block * @param {string} sCode * @returns {HTMLElement} */ function buildCodeBlock(sCode) { return createElement('div', { className: 'body-preview', textContent: sCode }); } /** * Build a copy icon SVG safely without innerHTML. * @returns {SVGElement} */ function buildCopyIconSvg() { let eSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); let eRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); let ePath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); eSvg.setAttribute('width', '14'); eSvg.setAttribute('height', '14'); eSvg.setAttribute('viewBox', '0 0 16 16'); eSvg.setAttribute('fill', 'none'); eSvg.setAttribute('stroke', 'currentColor'); eSvg.setAttribute('stroke-width', '1.4'); eSvg.setAttribute('stroke-linecap', 'round'); eSvg.setAttribute('stroke-linejoin', 'round'); eRect.setAttribute('x', '5.5'); eRect.setAttribute('y', '5.5'); eRect.setAttribute('width', '8'); eRect.setAttribute('height', '8'); eRect.setAttribute('rx', '1.5'); ePath.setAttribute('d', 'M5.5 10.5h-1a1.5 1.5 0 0 1-1.5-1.5v-5a1.5 1.5 0 0 1 1.5-1.5h5a1.5 1.5 0 0 1 1.5 1.5v1'); eSvg.appendChild(eRect); eSvg.appendChild(ePath); return eSvg; } /** * Get localStorage key for a collection variable override * @param {string} sCollection - collection name * @param {string} sKey - variable key * @returns {string} */ function getVarStorageKey(sCollection, sKey) { return 'ppundoc_var_' + sCollection + '_' + sKey; } /** * Get the effective value for a variable (localStorage override or default) * @param {string} sCollection - collection name * @param {string} sKey - variable key * @param {string} sDefault - default value from collection * @returns {string} */ function getVarValue(sCollection, sKey, sDefault) { let sStored = localStorage.getItem(getVarStorageKey(sCollection, sKey)); return sStored !== null ? sStored : (sDefault || ''); } /** * Build variable chips with click-to-edit behaviour * @param {Array} aVars - array of { key, value } * @param {string} sCollection - collection name for localStorage scoping * @returns {HTMLElement} */ function buildVariableChips(aVars, sCollection) { let eWrap = createElement('div', { className: 'variable-list' }); let i = 0; for (i = 0; i < aVars.length; i++) { (function (oVar) { let sVal = getVarValue(sCollection, oVar.key, oVar.value); let bOverridden = localStorage.getItem(getVarStorageKey(sCollection, oVar.key)) !== null; let sText = '{{' + oVar.key + '}}'; if (sVal) { sText = sText + ' = ' + sVal; } let sClass = 'variable-chip variable-chip--editable'; if (bOverridden) { sClass = sClass + ' variable-chip--overridden'; } let eChip = createElement('span', { className: sClass, textContent: sText, title: 'Click to edit value' }); eChip.addEventListener('click', function () { /* prevent double-edit */ if (qs('.variable-chip--editing', eWrap)) { return; } eChip.classList.add('variable-chip--editing'); eChip.textContent = ''; let eLabel = createElement('span', { className: 'variable-chip-label', textContent: '{{' + oVar.key + '}} = ' }); let eInput = createElement('input', { className: 'variable-chip-input', type: 'text' }); eInput.value = sVal; let eReset = createElement('span', { className: 'variable-chip-reset', textContent: '\u00D7', title: 'Reset to default' }); eChip.appendChild(eLabel); eChip.appendChild(eInput); eChip.appendChild(eReset); eInput.focus(); function finishEdit() { let sNew = eInput.value; localStorage.setItem(getVarStorageKey(sCollection, oVar.key), sNew); sVal = sNew; eChip.classList.remove('variable-chip--editing'); eChip.classList.add('variable-chip--overridden'); let sDisplay = '{{' + oVar.key + '}}'; if (sNew) { sDisplay = sDisplay + ' = ' + sNew; } eChip.textContent = sDisplay; } eInput.addEventListener('keydown', function (oEvt) { if (oEvt.key === 'Enter') { finishEdit(); } if (oEvt.key === 'Escape') { eChip.classList.remove('variable-chip--editing'); let sDisplay = '{{' + oVar.key + '}}'; if (sVal) { sDisplay = sDisplay + ' = ' + sVal; } eChip.textContent = sDisplay; } }); eInput.addEventListener('blur', function () { /* short delay so reset click can fire first */ setTimeout(function () { if (eChip.classList.contains('variable-chip--editing')) { finishEdit(); } }, 150); }); eReset.addEventListener('mousedown', function (oEvt) { oEvt.preventDefault(); localStorage.removeItem(getVarStorageKey(sCollection, oVar.key)); sVal = oVar.value || ''; eChip.classList.remove('variable-chip--editing'); eChip.classList.remove('variable-chip--overridden'); let sDisplay = '{{' + oVar.key + '}}'; if (sVal) { sDisplay = sDisplay + ' = ' + sVal; } eChip.textContent = sDisplay; }); }); eWrap.appendChild(eChip); })(aVars[i]); } return eWrap; } /* ── Hamburger menu toggle ── */ function bindHamburger() { const eHamburger = qs('#hamburger'); const eMenu = qs('#menu'); if (!eHamburger || !eMenu) { return; } eHamburger.addEventListener('click', function (oEvt) { oEvt.stopPropagation(); eMenu.classList.toggle('open'); eHamburger.textContent = eMenu.classList.contains('open') ? '\u2715' : '\u2630'; }); document.addEventListener('click', function (oEvt) { if (!eMenu.contains(oEvt.target) && oEvt.target !== eHamburger) { eMenu.classList.remove('open'); eHamburger.textContent = '\u2630'; } }); } document.addEventListener('DOMContentLoaded', bindHamburger);