On phones place the existing menu editor above the catalog and add the same return-to-top control used by New Order. Preserve form nodes, unsaved values and active input focus; keep desktop/tablet layout and read-only permissions. Full QA passed, including iPhone WebKit and existing promotion focus regression. Extend real-asset production verification with a backend-blocked mobile-menu scenario.
114 lines
8.0 KiB
JavaScript
114 lines
8.0 KiB
JavaScript
(()=>{
|
|
'use strict';
|
|
function boot(){
|
|
const view=document.getElementById('new'),work=view?.querySelector('.work');
|
|
const catalog=work?.querySelector(':scope > .catalog'),order=work?.querySelector(':scope > section.card');
|
|
if(!work||!catalog||!order||document.getElementById('ctOrderTop'))return;
|
|
const mobile=matchMedia('(max-width:760px)');
|
|
const button=document.createElement('button');button.id='ctOrderTop';button.type='button';button.hidden=true;
|
|
button.setAttribute('aria-label','Наверх к заказу');button.title='Наверх к заказу';
|
|
button.innerHTML='<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="M12 19V5m-6 6 6-6 6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>Наверх</span>';
|
|
document.body.appendChild(button);
|
|
const style=document.createElement('style');style.textContent=`
|
|
#ctOrderTop{display:none}
|
|
@media(max-width:760px){
|
|
#new.on{display:flex!important;flex-direction:column}
|
|
#new .work{order:-1;width:100%}
|
|
#new .work.sun-resizable-work>section.card.sun-order-pane{order:-1!important;grid-column:1!important;grid-row:1!important;position:static!important}
|
|
#new .work.sun-resizable-work>.catalog{order:0!important;grid-column:1!important;grid-row:2!important}
|
|
#new #orderForm .lines{min-height:0!important}
|
|
#ctOrderTop:not([hidden]){display:flex;align-items:center;justify-content:center;gap:6px;position:fixed;right:16px;bottom:calc(88px + env(safe-area-inset-bottom,0px));z-index:900;min-width:48px;min-height:48px;padding:10px 14px;border:1px solid #d7bb65;border-radius:24px;background:#f6e7af;color:#302919;box-shadow:0 4px 18px #0002;font:700 13px/1.2 Arial,sans-serif;cursor:pointer}
|
|
#ctOrderTop:focus-visible{outline:3px solid #546747;outline-offset:3px}
|
|
}
|
|
@media print{#ctOrderTop{display:none!important}}
|
|
`;document.head.appendChild(style);
|
|
function visibility(){button.hidden=!mobile.matches||!view.classList.contains('on')||document.body.classList.contains('sun-cloud-auth-required')||window.scrollY<300;}
|
|
function layout(){
|
|
// Move the existing node, preserving inputs, listeners and the current draft.
|
|
if(mobile.matches){if(order.nextElementSibling!==catalog)work.insertBefore(order,catalog);}
|
|
else{const splitter=work.querySelector(':scope > .sun-work-splitter');const after=splitter||catalog;if(after.nextElementSibling!==order)after.insertAdjacentElement('afterend',order);}
|
|
visibility();
|
|
}
|
|
let frame=0;function update(){if(frame)return;frame=requestAnimationFrame(()=>{frame=0;visibility()});}
|
|
button.addEventListener('click',()=>{window.scrollTo({top:0,left:0,behavior:'instant'});order.setAttribute('tabindex','-1');order.focus({preventScroll:true});visibility();});
|
|
window.addEventListener('scroll',update,{passive:true});mobile.addEventListener('change',layout);
|
|
const observer=new MutationObserver(update);observer.observe(view,{attributes:true,attributeFilter:['class']});observer.observe(document.body,{attributes:true,attributeFilter:['class']});
|
|
layout();
|
|
}
|
|
// The menu page is created later by ops-ux. Attach once when it exists, then
|
|
// observe only its visibility and direct detail children, never the whole app.
|
|
function bootMenu(){
|
|
const view=document.getElementById('sun-menu-editor-v1762'),layout=view?.querySelector('.sun-menu-layout');
|
|
const list=layout?.querySelector(':scope > .sun-menu-list-pane'),detail=document.getElementById('sunMenuDetailV1762');
|
|
if(!view||!layout||!list||!detail)return false;
|
|
if(view.dataset.ctMobileMenu==='1')return true;
|
|
view.dataset.ctMobileMenu='1';
|
|
const mobile=matchMedia('(max-width:760px)');
|
|
const button=document.createElement('button');button.id='ctMenuTop';button.type='button';button.hidden=true;
|
|
button.setAttribute('aria-label','Наверх к редактору меню');button.title='Наверх к редактору меню';
|
|
button.setAttribute('aria-controls','sunMenuDetailV1762');
|
|
button.innerHTML='<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="M12 19V5m-6 6 6-6 6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>Наверх</span>';
|
|
document.body.appendChild(button);
|
|
const style=document.createElement('style');style.id='ctMobileMenuStyle';style.textContent=`
|
|
#ctMenuTop{display:none}
|
|
@media(max-width:760px){
|
|
#sun-menu-editor-v1762 .sun-menu-layout{grid-template-columns:minmax(0,1fr)}
|
|
#sun-menu-editor-v1762 #sunMenuDetailV1762{grid-column:1;grid-row:1;min-width:0;min-height:0}
|
|
#sun-menu-editor-v1762 .sun-menu-list-pane{grid-column:1;grid-row:2;min-width:0;min-height:0}
|
|
#sun-menu-editor-v1762 .sun-menu-list{max-height:none;overflow:visible}
|
|
#sun-menu-editor-v1762 .sun-menu-detail-placeholder{min-height:76px}
|
|
#ctMenuTop:not([hidden]){display:flex;align-items:center;justify-content:center;gap:6px;position:fixed;right:16px;bottom:calc(88px + env(safe-area-inset-bottom,0px));z-index:900;min-width:48px;min-height:48px;padding:10px 14px;border:1px solid #d7bb65;border-radius:24px;background:#f6e7af;color:#302919;box-shadow:0 4px 18px #0002;font:700 13px/1.2 Arial,sans-serif;cursor:pointer}
|
|
#ctMenuTop:focus-visible{outline:3px solid #546747;outline-offset:3px}
|
|
}
|
|
@media print{#ctMenuTop{display:none!important}}
|
|
`;document.head.appendChild(style);
|
|
function active(){return mobile.matches&&view.classList.contains('on')&&!document.body.classList.contains('sun-cloud-auth-required');}
|
|
function visibility(){button.hidden=!active()||document.body.classList.contains('sun-modal-open')||window.scrollY<300;}
|
|
function neutralHint(){
|
|
const label=detail.querySelector('.sun-menu-detail-placeholder b');
|
|
if(label?.textContent==='Выберите позицию слева, чтобы открыть карточку.')label.textContent='Выберите позицию из списка, чтобы открыть карточку.';
|
|
}
|
|
function arrange(){
|
|
// Move the LIST, not the form: an unsaved or focused editor keeps its node,
|
|
// input values, file selection and listeners across viewport changes.
|
|
if(mobile.matches){if(detail.nextElementSibling!==list)detail.after(list);}
|
|
else if(list.nextElementSibling!==detail)layout.insertBefore(list,detail);
|
|
neutralHint();visibility();
|
|
}
|
|
function returnToEditor(){
|
|
if(!active())return;
|
|
window.scrollTo({top:0,left:0,behavior:'instant'});
|
|
detail.setAttribute('tabindex','-1');detail.focus({preventScroll:true});visibility();
|
|
}
|
|
let frame=0;
|
|
function update(){if(frame)return;frame=requestAnimationFrame(()=>{frame=0;neutralHint();visibility();});}
|
|
button.addEventListener('click',returnToEditor);
|
|
view.addEventListener('click',e=>{
|
|
const target=e.target.closest?.('[data-menu-item-v1762],#sunMenuAddV1762');
|
|
if(!target||target.disabled||!active())return;
|
|
// The existing click handler docks the editor in a zero-delay task. Wait
|
|
// for it, and do not scroll if the user has left Menu in the meantime.
|
|
setTimeout(()=>requestAnimationFrame(()=>{
|
|
// Never take focus or scroll away after the user has started editing.
|
|
const focused=document.activeElement;
|
|
if(detail.contains(focused)&&focused?.matches('input,textarea,select,[contenteditable="true"]'))return;
|
|
if(detail.querySelector(':scope > .dialog,:scope > .sun-menu-readonly'))returnToEditor();
|
|
}),0);
|
|
});
|
|
window.addEventListener('scroll',update,{passive:true});mobile.addEventListener('change',arrange);
|
|
const observer=new MutationObserver(update);
|
|
observer.observe(view,{attributes:true,attributeFilter:['class']});
|
|
observer.observe(document.body,{attributes:true,attributeFilter:['class']});
|
|
observer.observe(detail,{childList:true});
|
|
arrange();return true;
|
|
}
|
|
function start(){
|
|
boot();
|
|
if(!bootMenu()){
|
|
const waiting=new MutationObserver(()=>{if(bootMenu())waiting.disconnect();});
|
|
waiting.observe(document.body,{childList:true});
|
|
}
|
|
}
|
|
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start();
|
|
})();
|