/* Gardenas - site.js.php (patched, ES5 only) * - Mobile nav: close on link click / ESC / desktop resize, backdrop close, scroll lock * - Anchor jump: header-height offset (no CSS var dependency during scroll) * - Photo uploader: D&D + picker, 64px thumbs, × remove, ★ primary (keeps input.files for non-DataTransfer) * - Global D&D guard * - UI niceties: counters (files/MB), consent checkbox gates submit */ (function(){ 'use strict'; /* ------------------------------- * Utilities * ------------------------------- */ function $(sel, root){ return (root||document).querySelector(sel); } function $all(sel, root){ return Array.prototype.slice.call((root||document).querySelectorAll(sel)); } function on(el, ev, fn, opts){ if(el){ el.addEventListener(ev, fn, opts||false); } } function hasClass(el, c){ return el && (' '+el.className+' ').indexOf(' '+c+' ') > -1; } function addClass(el, c){ if(el && !hasClass(el,c)){ el.className = (el.className ? el.className+' ' : '') + c; } } function remClass(el, c){ if(el && hasClass(el,c)){ el.className = (' '+el.className+' ').replace(' '+c+' ',' ').trim(); } } // Element.matches polyfill if(!Element.prototype.matches){ Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector || function(s){ var m = (this.document || this.ownerDocument).querySelectorAll(s), i = 0; while(m[i] && m[i] !== this){ i++; } return !!m[i]; }; } /* ------------------------------- * Anchor offset + scrolling * ------------------------------- */ function setAnchorOffset(){ var header = $('header.site'); var h = header ? header.getBoundingClientRect().height : 0; var offset = Math.round(h + 6); try{ document.documentElement.style.setProperty('--anchor-offset', offset + 'px'); }catch(_){} } window.setAnchorOffset = setAnchorOffset; function prefersReduced(){ try{ return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch(_){ return false; } } function scrollToWithOffset(targetEl){ if(!targetEl){ return; } var header = $('header.site'); var off = header ? Math.round(header.getBoundingClientRect().height + 6) : 0; var rect = targetEl.getBoundingClientRect(); var y = (window.pageYOffset || document.documentElement.scrollTop) + rect.top - off; if(prefersReduced()){ window.scrollTo(0, y); }else{ try{ window.scrollTo({ top: y, behavior: 'smooth' }); } catch(_){ window.scrollTo(0, y); } } } /* ------------------------------- * Mobile navigation * ------------------------------- */ function syncMenuState(){ var navc = $('#navc') || $('input.navc[type="checkbox"]'); var label = $('#menuToggle') || $('.nav-toggle'); var open = !!(navc && navc.checked); if(label){ try{ label.setAttribute('aria-expanded', open ? 'true' : 'false'); }catch(_){ } } var root = document.documentElement; if(open){ addClass(root, 'menu-open'); } else { remClass(root, 'menu-open'); } setTimeout(setAnchorOffset, 0); } function initMobileNav(){ var navc = $('#navc') || $('input.navc[type="checkbox"]'); var backdrop = $('label.backdrop'); if(backdrop){ on(backdrop, 'click', function(){ if(navc){ navc.checked = false; syncMenuState(); } }); } on(document, 'keydown', function(e){ e = e || window.event; if((e.key && e.key.toLowerCase() === 'escape') || e.keyCode === 27){ if(navc && navc.checked){ navc.checked = false; syncMenuState(); } } }); on(window, 'resize', function(){ setAnchorOffset(); // Close on orientation change (e.g., smartphone landscape) on(window, 'orientationchange', function(){ if(navc && navc.checked){ navc.checked = false; syncMenuState(); } setAnchorOffset(); }); if(window.innerWidth >= 1024 && navc && navc.checked){ navc.checked = false; syncMenuState(); } }); if(navc){ on(navc, 'change', syncMenuState); } syncMenuState(); } /* ------------------------------- * Anchor links * ------------------------------- */ function initAnchorLinks(){ var links = $all('a[href^=\"#\"]'); var navc = $('#navc') || $('input.navc[type=\"checkbox\"]'); function handleHash(href){ if(!href || href === '#'){ return; } var id = href.charAt(0) === '#' ? href.slice(1) : href; var el = document.getElementById(id); if(!el){ try{ location.hash = href; }catch(_){ } return; } scrollToWithOffset(el); try{ history.pushState(null, '', '#' + id); }catch(_){ location.hash = '#' + id; } } for(var i=0;i= 0 && v < files.length){ starIndex = v; } } // build thumbs for(var i=0;i idx){ starHidden.value = Math.max(0, currentStar - 1); } } rebuildPreview(); }else{ if(item && item.parentNode){ item.parentNode.removeChild(item); } } }); on(btnStar, 'click', function(){ var all = $all('.thumb', grid); for(var k=0;k= 0) ? newIdx : 0; } rebuildPreview(); }); } item.appendChild(img); item.appendChild(btnX); item.appendChild(btnStar); grid.appendChild(item); })(i); } updateCounters(); syncSubmitEnabled(); } on(inp, 'change', function(){ rebuildPreview(); }); if(dz){ on(dz, 'dragenter', function(e){ e.preventDefault(); addClass(dz, 'is-dragover'); }); on(dz, 'dragover', function(e){ e.preventDefault(); addClass(dz, 'is-dragover'); }); on(dz, 'dragleave', function(e){ if(e.target === dz){ remClass(dz, 'is-dragover'); } }); on(dz, 'drop', function(e){ e.preventDefault(); remClass(dz, 'is-dragover'); if(e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length){ if(supportsDT){ var dt = new DataTransfer(); var existing = inp.files, i; if(existing){ for(i=0;i= 0) preload(nodes[i-1]); } function openAt(idx, dir){ if(idx < 0 || idx >= nodes.length){ return; } current = idx; var n = nodes[idx]; var u = (typeof getFullSrc === 'function') ? getFullSrc(n) : (n.getAttribute('data-full') || n.getAttribute('src')); // 入れ替えアニメーション(方向あり) if (typeof dir === 'number') { img.style.transition = 'none'; img.style.opacity = '0'; img.style.transform = 'translateX(' + (dir*60) + 'px)'; setTimeout(function(){ img.src = u; img.alt = n.getAttribute('alt') || ''; cap.textContent = getCaption(n); requestAnimationFrame(function(){ img.style.transition = 'transform .18s ease, opacity .18s ease'; img.style.transform = 'translateX(0)'; img.style.opacity = '1'; }); }, 0); } else { img.src = u; img.alt = n.getAttribute('alt') || ''; cap.textContent = getCaption(n); } document.documentElement.style.overflow = 'hidden'; backdrop.className = 'lb-backdrop is-open'; try{ btnClose.focus(); }catch(_){} showHintOnce(); preloadNeighbors(idx); } function close(){ backdrop.className = 'lb-backdrop'; document.documentElement.style.overflow = ''; try{ if(lastFocus && lastFocus.focus){ lastFocus.focus(); } }catch(_){} current = -1; } // --- 操作 on(backdrop, 'click', function(ev){ var t = ev.target || ev.srcElement; if (t === backdrop) { close(); } }); on(btnClose, 'click', function(){ close(); }); // 左右ボタン & 外側左右クリック on(btnPrev, 'click', function(e){ if(current>0){ openAt(current-1, -1); } }); on(btnNext, 'click', function(e){ if(current>=0 && current0) openAt(current-1, -1); } else { if(current= 0 && current < nodes.length - 1){ openAt(current + 1, +1); } } else if(k === 'ArrowLeft' || k === 37){ if(current > 0){ openAt(current - 1, -1); } } }); // --- スワイプ / ドラッグ(スムーズ追従) var sx=0, sy=0, t0=0, dragging=false, lastDX=0; function startAt(x,y){ sx=x; sy=y; t0=Date.now(); dragging=true; img.style.transition='transform .0s ease'; lastDX=0; } function moveAt(x,y){ if(!dragging) return; var dx = x - sx, dy = y - sy; var ax = Math.abs(dx), ay = Math.abs(dy); if (ax > ay * 1.2) { lastDX = dx; img.style.transform = 'translateX(' + dx + 'px)'; } } function endAt(x,y){ if(!dragging) return; dragging=false; var dx = lastDX, ax = Math.abs(dx), dt = Date.now() - t0; img.style.transition='transform .18s ease'; if (dt < 800 && ax > 80) { if (dx < 0 && current < nodes.length-1) { openAt(current+1, +1); } else if (dx > 0 && current > 0) { openAt(current-1, -1); } else { img.style.transform = 'translateX(0)'; } } else { img.style.transform = 'translateX(0)'; } } // touch on(backdrop, 'touchstart', function(e){ if (!e.touches || e.touches.length !== 1) { return; } var t = e.touches[0]; startAt(t.clientX, t.clientY); }); on(backdrop, 'touchmove', function(e){ if (!e.touches || e.touches.length !== 1) { return; } var t = e.touches[0]; moveAt(t.clientX, t.clientY); }); on(backdrop, 'touchend', function(e){ if (!e.changedTouches || e.changedTouches.length !== 1) { return; } var t = e.changedTouches[0]; endAt(t.clientX, t.clientY); }); // mouse / pointer (PC) on(img, 'mousedown', function(e){ if (e.button !== 0) return; startAt(e.clientX, e.clientY); if (e.preventDefault) e.preventDefault(); else e.returnValue = false; }); on(document, 'mousemove', function(e){ if(dragging) moveAt(e.clientX, e.clientY); }); on(document, 'mouseup', function(e){ if(dragging) endAt(e.clientX, e.clientY); }); // --- 画像クリック(委譲) on(scope, 'click', function(ev){ var t = ev.target || ev.srcElement; if(!t || t.tagName !== 'IMG'){ return; } var i, idx = -1; for(i=0;i= 1 && duration <= 120)) { duration = 18; } var scale = parseFloat(ds.heroScale); if(!(scale >= 1.0 && scale <= 2.0)) { scale = 1.15; } var overlay = parseFloat(ds.heroOverlay); if(!(overlay >= 0 && overlay <= 0.8)) { overlay = 0; } // Respect reduced motion if (prefersReduced()) { motion = 'none'; } // Apply CSS variables try { el.style.setProperty('--hero-duration', String(duration) + 's'); el.style.setProperty('--hero-scale', String(scale)); el.style.setProperty('--hero-overlay', String(overlay)); } catch(_){} // Remove existing motion-* classes var cls = el.className.split(/\s+/); for (var k=cls.length-1; k>=0; k--) { if (/^motion-/.test(cls[k])) { cls.splice(k,1); } } el.className = cls.join(' ').trim(); // Determine axis for panning var isHoriz = (motion === 'kb-left' || motion === 'kb-right' || motion === 'kb-alt'); var isVert = (motion === 'kb-up' || motion === 'kb-down'); var isDiag = (motion === 'kb-diag' || motion === 'tri-pan'); // Base background el.style.backgroundRepeat = 'no-repeat'; if (motion === 'zoom') { // Zoom: animate background-size via keyframes (100% -> 100%*scale) el.style.backgroundSize = '100%'; // keep CSS-defined background-position (do not override) } else if (motion !== 'none') { // Pan: ensure overscan so movement is visible var isSmall = (window.matchMedia && window.matchMedia('(max-width: 640px)').matches); var minBoost = isDiag ? (isSmall ? 1.16 : 1.25) : (isSmall ? 1.10 : 1.18); // mobile: softer overscan var targetScale = Math.max(scale || 1, minBoost); function applySize(imgW, imgH){ var cw = el.clientWidth || 1, ch = el.clientHeight || 1; var cRatio = cw / ch; var iRatio = (imgW>0 && imgH>0) ? (imgW/imgH) : cRatio; if (isVert && iRatio < cRatio){ // portrait-ish image inside wider container → boost height el.style.backgroundSize = 'auto calc(100% * ' + targetScale + ')'; } else { // default: boost width (covers horizontal/diag) el.style.backgroundSize = 'calc(100% * ' + targetScale + ') auto'; } // keep CSS-defined background-position (do not override) } // Try to read background image URL var bg = (el.currentStyle && el.currentStyle.backgroundImage) || (window.getComputedStyle ? getComputedStyle(el).backgroundImage : ''); var m = /url\((['\"]?)(.*?)\1\)/.exec(bg || ''); var url = m && m[2] ? m[2] : ''; // Fallback apply immediately applySize(0,0); if (url){ var img = new Image(); img.onload = function(){ applySize(img.naturalWidth||0, img.naturalHeight||0); }; img.src = url; } } // Apply motion class var map = { 'none': '', 'zoom': 'motion-zoom', 'tri-pan': 'motion-tri-pan', 'kb-left': 'motion-kb-left', 'kb-right': 'motion-kb-right', 'kb-up': 'motion-kb-up', 'kb-down': 'motion-kb-down', 'kb-diag': 'motion-kb-diag', 'kb-alt': 'motion-kb-alt' }; var add = map[motion] || ''; if (add) { el.className = (el.className ? el.className + ' ' : '') + add; } if (motion === 'tri-pan') { try { var hasImg = !!(el.querySelector('.hero-img, picture > img, img')); if (!hasImg) { if ((' ' + el.className + ' ').indexOf(' motion-tri-pan--bg ') === -1) { el.className += ' motion-tri-pan--bg'; } var bgZoom = Math.max(106, Math.min(200, Math.round((scale||1)*100))); el.style.setProperty('--hero-bg-zoom', String(bgZoom) + '%'); } } catch(_){ /* no-op */ } } } catch(e){ /* no-op */ } } /* ------------------------------- * Boot * ------------------------------- */ on(document, 'DOMContentLoaded', function(){ setAnchorOffset(); initMobileNav(); initGlobalDnDGuard(); initPhotoUploader(); initAnchorLinks(); initLightbox(); initHeroMotion(); }); on(window, 'load', setAnchorOffset); on(window, 'resize', setAnchorOffset); })(); // ==== [ADD 2025-09-07] NEWS (layout--cards): show full content initially ==== (function(){ function initNewsCardsFull(){ var sec = document.querySelector('#news.layout--cards'); if(!sec) return; var items = sec.querySelectorAll('article.card[data-item]'); for(var i=0;i{ if(el!==panel){ el.setAttribute('hidden',''); } }); sec.querySelectorAll('[data-toggle="more"][aria-expanded="true"]').forEach(b=>{ if(b!==btn){ b.setAttribute('aria-expanded','false'); b.textContent='続きを読む'; } }); } } if(isOpen){ panel.setAttribute('hidden',''); item.querySelectorAll('[data-toggle="more"]').forEach(b=>{ b.setAttribute('aria-expanded','false'); b.textContent='続きを読む'; }); }else{ panel.removeAttribute('hidden'); item.querySelectorAll('[data-toggle="more"]').forEach(b=>{ b.setAttribute('aria-expanded','true'); b.textContent='短くする'; }); panel.scrollIntoView({behavior:'smooth', block:'nearest'}); } }); // Auto-hide button when excerpt is not clamped window.addEventListener('load', function(){ document.querySelectorAll('#news [data-excerpt]').forEach(p=>{ const btn = (p.parentElement && p.parentElement.querySelector('[data-inline-actions] [data-toggle="more"]')) || p.querySelector('[data-toggle="more"]'); if(!btn) return; // give layout a tick const over = p.scrollHeight - 1 > p.clientHeight; if(!over){ btn.style.display='none'; } }); }); })(); /* ==== [PATCH: expand-thumb LIST] ==== */ document.addEventListener('DOMContentLoaded', function(){ try{ var root = document.getElementById('news'); if(!root) return; // 初期同期 root.querySelectorAll('[data-toggle="more"]').forEach(function(btn){ var article = btn.closest('article.card'); if(!article) return; var expanded = btn.getAttribute('aria-expanded') === 'true'; article.classList.toggle('is-thumb-expanded', expanded); }); // クリック後に既存処理の後で同期 root.addEventListener('click', function(ev){ var btn = ev.target.closest('[data-toggle="more"]'); if(!btn) return; setTimeout(function(){ var article = btn.closest('article.card'); if(!article) return; var expanded = btn.getAttribute('aria-expanded') === 'true'; article.classList.toggle('is-thumb-expanded', expanded); }, 0); }); }catch(_){} }); /* ==== [PATCH: expand-thumb LIST] ==== */ // end /* === [SWAP STEP1 2025-09-09] #news expand state without :has() === - Adds .is-expanded to
when its [data-more] loses/gets the 'hidden' attribute. - Non-invasive: doesn't touch existing toggle logic; uses MutationObserver and initial sync. */ (function(){ try{ var root = document.querySelector('#news'); if(!root) return; function syncArticle(article){ if(!article) return; var more = article.querySelector('[data-more]'); if(!more) return; var expanded = !more.hasAttribute('hidden'); if(expanded){ article.classList.add('is-expanded'); }else{ article.classList.remove('is-expanded'); } } // Initial sync for currently visible state root.querySelectorAll('article.card').forEach(function(a){ syncArticle(a); }); // Observe [data-more] hidden attribute changes var obs = new MutationObserver(function(muts){ muts.forEach(function(m){ if(m.type === 'attributes' && m.attributeName === 'hidden'){ var more = m.target; if(!(more && more.closest)) return; var art = more.closest('article'); if(art && art.closest('#news')) syncArticle(art); } }); }); root.querySelectorAll('[data-more]').forEach(function(m){ obs.observe(m, { attributes:true, attributeFilter:['hidden'] }); }); // Optional: listen for dynamically added articles/more blocks (if any future code injects them) var ro = new MutationObserver(function(muts){ muts.forEach(function(m){ m.addedNodes && m.addedNodes.forEach(function(node){ if(!(node instanceof Element)) return; if(node.matches && node.matches('article.card')){ syncArticle(node); var more = node.querySelector('[data-more]'); if(more) obs.observe(more, { attributes:true, attributeFilter:['hidden'] }); }else{ node.querySelectorAll && node.querySelectorAll('article.card').forEach(function(a){ syncArticle(a); var more2 = a.querySelector('[data-more]'); if(more2) obs.observe(more2, { attributes:true, attributeFilter:['hidden'] }); }); } }); }); }); ro.observe(root, { childList:true, subtree:true }); }catch(e){ console && console.warn && console.warn('[SWAP STEP1] expand-state init error:', e); } })(); /* === [/SWAP STEP1] === */ (function(){ 'use strict'; var S1 = [{"slug":"features","layout":"compact","tokens":["opt-title-2","opt-excerpt-3","opt-thumb-16x9"]},{"slug":"use-cases","layout":"list","tokens":["opt-title-2","opt-excerpt-3","opt-thumb-16x9"]},{"slug":"photo","layout":"list","tokens":["opt-title-2","opt-excerpt-3","opt-thumb-16x9"]},{"slug":"contact","layout":"list","tokens":["opt-title-2","opt-excerpt-3","opt-thumb-16x9"]}]; if (!Array.isArray(S1) || S1.length === 0) return; function byId(id){ return document.getElementById(id); } function removeLayoutClasses(el){ if(!el) return; var cls = el.className || ''; if(!cls) return; el.className = cls.replace(/\blayout--\S+/g, '').trim(); } function applyTokens(el, tokens){ if(!el || !tokens) return; for (var i=0;iの直前) if (footer && parent) { parent.insertBefore(el, footer); } if (DEV && info.removed && info.removed.length){ console.warn('[S1.1] Removed tokens@%s:', s.slug, info.removed); } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', reorder); } else { reorder(); } })(); // === [/S1] ========================================================================= /* Copy on click for asset panel ID/URL */ document.addEventListener('click', function(e){ const t = e.target; if(t && t.classList && t.classList.contains('copy-on-click')){ t.select && t.select(); const val = t.value || t.getAttribute('value') || ''; if(val){ navigator.clipboard && navigator.clipboard.writeText(val).then(()=>{ t.classList.add('copied'); setTimeout(()=> t.classList.remove('copied'), 800); }).catch(()=>{}); } } }); /* ========================================================= * RA: Admin copy actions (.copy-id / .copy-img) * - 最小差分:公開側へは影響しない( のみで有効) * - 単一点配線:captureフェーズで委譲(動的追加にも追従) * - 画像コピーは ClipboardItem→失敗時URLコピーへ自動フォールバック * - IDコピーは writeText→失敗時 execCommand('copy') フォールバック * ========================================================= */ (function(){ 'use strict'; try { if (!document || !document.body || !document.body.hasAttribute('data-admin')) return; // load flags (console確認用) window.__RA_COPY_ID_V1__ = true; window.__RA_COPY_IMG_V4__ = true; function copyText(text){ // Secure Contextでは clipboard.writeText を優先 try { if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(String(text)); } } catch(_) {} // 非HTTPS等では execCommand フォールバック return new Promise(function(resolve, reject){ var ta = document.createElement('textarea'); ta.value = String(text); ta.setAttribute('readonly', 'readonly'); ta.style.position = 'fixed'; ta.style.left = '-9999px'; ta.style.fontSize = '12px'; document.body.appendChild(ta); ta.select(); try { var ok = document.execCommand && document.execCommand('copy'); document.body.removeChild(ta); ok ? resolve() : reject(new Error('execCommand failed')); } catch(e){ document.body.removeChild(ta); reject(e); } }); } function closest(el, sel){ if (!el) return null; if (el.closest) return el.closest(sel); // 超簡易フォールバック(親を遡るだけ) var node = el; while (node && node.nodeType === 1){ // querySelectorAllを使わず classNameベース簡易判定(.foo のみ想定) if (sel.charAt(0) === '.' && (' '+node.className+' ').indexOf(' '+sel.slice(1)+' ') > -1) return node; node = node.parentNode; } return null; } function findIdFrom(el){ var idBtn = closest(el, '.copy-id'); if (idBtn){ if (idBtn.dataset && idBtn.dataset.id) return idBtn.dataset.id; // 近傍の media.php?id= 探索 var scope = closest(idBtn, 'li') || closest(idBtn, '.card') || closest(idBtn, '.asset') || document; var a = scope.querySelector ? scope.querySelector('a[href*="media.php?id="]') : null; if (a){ var m = (a.href||'').match(/[?&]id=(\d+)/); if (m) return m[1]; } // テキストから "ID : 53" を抽出 var txt = idBtn.textContent || ''; var m2 = txt.match(/\bID\s*[::]\s*(\d+)\b/); if (m2) return m2[1]; } return null; } function fetchBlob(url){ return fetch(url, { credentials: 'same-origin', cache: 'no-cache' }) .then(function(r){ if(!r.ok) throw new Error('fetch '+r.status); return r.blob(); }); } function copyImage(url){ var nav = navigator; if (nav.clipboard && nav.clipboard.write && window.ClipboardItem){ return fetchBlob(url).then(function(blob){ var type = blob.type || 'image/png'; var item = new ClipboardItem( (function(o){ o[type]=blob; return o; })({}) ); return nav.clipboard.write([item]); }); } // フォールバック:URL文字列コピー return copyText(url); } document.addEventListener('click', function(ev){ var t = ev.target; if (!t) return; // --- IDコピー --- var idBtn = closest(t, '.copy-id'); if (idBtn){ var id = findIdFrom(t); if (!id){ console.warn('[copy-id] id not found'); return; } copyText(String(id)) .then(function(){ if (idBtn.classList){ idBtn.classList.add('ok'); setTimeout(function(){ idBtn.classList.remove('ok'); }, 420); } }) .catch(function(e){ console.warn('[copy-id] failed', e); }); return; } // --- 画像コピー --- var imgBtn = closest(t, '.copy-img'); if (imgBtn){ var scope = closest(imgBtn, 'li') || closest(imgBtn, '.card') || closest(imgBtn, '.asset') || document; var img = scope && scope.querySelector ? scope.querySelector('img') : null; var src = img && (img.getAttribute('data-src') || img.src); if (!src){ var a = scope && scope.querySelector ? scope.querySelector('a[href*="media.php?id="]') : null; if (a) src = a.href; } if (!src){ console.warn('[copy-img] src not found'); return; } // captureフェーズ内で即実行し user activation を保持 copyImage(src) .then(function(){ if (imgBtn.classList){ imgBtn.classList.add('ok'); setTimeout(function(){ imgBtn.classList.remove('ok'); }, 420); } }) .catch(function(e){ console.warn('[copy-img] image copy failed -> URL fallback', e); return copyText(src); }) .catch(function(e){ console.warn('[copy-img] failed', e); }); } }, true); // capture } catch(e){ console.warn('[admin-actions] init failed', e); } })();