اضافه شدن قابلیت مقایسه شماتیک
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { bindCamera } from './interactions.js';
|
||||
const state=document.querySelector('#state'),id=location.pathname.split('/').pop();
|
||||
const token=new URLSearchParams(location.hash.slice(1)).get('token')||sessionStorage.getItem(`comparison:${id}`);
|
||||
if(token)sessionStorage.setItem(`comparison:${id}`,token);
|
||||
if(location.hash)history.replaceState(null,'',location.pathname);
|
||||
const headers=token?{'X-Viewer-Token':token}:{},base=`/api/v1/comparisons/${id}`;
|
||||
const controller=new AbortController(),views=[];
|
||||
addEventListener('pagehide',()=>{controller.abort();for(const view of views)view.destroy();},{once:true});
|
||||
async function request(path='',text=false){
|
||||
const response=await fetch(base+path,{headers,signal:controller.signal,cache:'no-store'});
|
||||
if(!response.ok){const body=await response.json();throw new Error(body.error?.message||'Comparison unavailable');}
|
||||
return text?response.text():response.json();
|
||||
}
|
||||
function union(boxes){
|
||||
const x=Math.min(...boxes.map(b=>b.x)),y=Math.min(...boxes.map(b=>b.y));
|
||||
return {x,y,width:Math.max(...boxes.map(b=>b.x+b.width))-x,height:Math.max(...boxes.map(b=>b.y+b.height))-y};
|
||||
}
|
||||
async function start(){
|
||||
let result;
|
||||
for(;;){
|
||||
result=await request();if(result.status==='completed')break;
|
||||
if(result.status==='failed')throw new Error(result.error?.message||'Comparison failed');
|
||||
await new Promise(resolve=>setTimeout(resolve,1000));
|
||||
}
|
||||
const texts=await Promise.all(['old','new'].map(side=>request('/svg/'+side,true)));
|
||||
const svgs=texts.map(text=>{
|
||||
const parsed=new DOMParser().parseFromString(text,'image/svg+xml');
|
||||
if(parsed.querySelector('parsererror')||parsed.documentElement.localName!=='svg')throw new Error('Invalid rendering artifact');
|
||||
return document.importNode(parsed.documentElement,true);
|
||||
});
|
||||
const content=[result.old.bounds,result.new.bounds].filter(b=>b&&b.width>0&&b.height>0);
|
||||
const bounds=union(content.length?content:[result.old.sheet,result.new.sheet]);
|
||||
for(const [index,side] of ['old','new'].entries()){
|
||||
document.querySelector(`#${side}-name`).textContent=result[side].filename;
|
||||
const surface=document.querySelector(`#${side}-surface`);surface.append(svgs[index]);
|
||||
surface.style.backgroundColor=svgs[index].querySelector('rect')?.getAttribute('fill')||'#ffffff';
|
||||
views.push(bindCamera(surface,svgs[index],bounds,source=>{
|
||||
if(document.querySelector('#sync').checked)for(const other of views)if(other!==source)other.apply(source.pose());
|
||||
}));
|
||||
}
|
||||
await document.fonts.ready;
|
||||
state.classList.remove('loading');state.textContent='';
|
||||
const changes=result.changes,byId=new Map(changes.map(c=>[c.id,c])),rows=new Map(),nodes=svgs.flatMap(svg=>[...svg.querySelectorAll('[data-change-id]')]);
|
||||
const list=document.querySelector('#changes'),filter=document.querySelector('#filter');
|
||||
let selected=null;
|
||||
for(const [status,count]of Object.entries(result.summary)){
|
||||
const badge=document.createElement('span');badge.className=status;badge.textContent=`${count} ${status}`;document.querySelector('#summary').append(badge);
|
||||
}
|
||||
if(!changes.length)list.textContent='No structural changes detected.';
|
||||
function visibleChanges(){return changes.filter(c=>filter.value==='all'||c.status===filter.value);}
|
||||
function select(change){
|
||||
selected=change.id;
|
||||
for(const [id,row]of rows)row.setAttribute('aria-current',String(id===selected));
|
||||
for(const node of nodes){if(node.dataset.changeId===selected)node.setAttribute('data-selected','');else node.removeAttribute('data-selected');}
|
||||
const boxes=[change.old_bounds,change.new_bounds].filter(Boolean);
|
||||
if(boxes.length)for(const view of views)view.focus(union(boxes));
|
||||
const details={kind:change.kind,match:change.match||'unmatched'};
|
||||
if(change.moved)details.movement=change.movement;
|
||||
if(change.fields.length)details.fields=change.fields;
|
||||
document.querySelector('#details').textContent=JSON.stringify(details,null,2);
|
||||
rows.get(selected)?.scrollIntoView({block:'nearest'});
|
||||
}
|
||||
for(const change of changes){
|
||||
const button=document.createElement('button');button.type='button';button.className=change.status;
|
||||
const label=document.createElement('span');label.textContent=change.label;
|
||||
const kind=document.createElement('small');kind.textContent=change.status+(change.moved&&change.status==='modified'?', moved':'')+' · '+change.kind;
|
||||
button.append(label,kind);button.addEventListener('click',()=>select(change));list.append(button);rows.set(change.id,button);
|
||||
}
|
||||
function applyFilters(){
|
||||
for(const node of nodes){
|
||||
const change=byId.get(node.dataset.changeId);
|
||||
const hidden=change?(filter.value!=='all'&&change.status!==filter.value):(filter.value!=='all'||document.querySelector('#only-changes').checked);
|
||||
node.toggleAttribute('data-diff-hidden',hidden);
|
||||
}
|
||||
for(const change of changes)rows.get(change.id).hidden=filter.value!=='all'&&change.status!==filter.value;
|
||||
document.body.classList.toggle('no-highlights',!document.querySelector('#highlights').checked);
|
||||
}
|
||||
for(const element of [filter,document.querySelector('#only-changes'),document.querySelector('#highlights')])element.addEventListener('change',applyFilters);
|
||||
document.querySelector('#fit').addEventListener('click',()=>{for(const view of views)view.fit();});
|
||||
document.querySelector('#sync').addEventListener('change',()=>{if(document.querySelector('#sync').checked&&views[0])views[1].apply(views[0].pose());});
|
||||
function next(delta){const candidates=visibleChanges();if(!candidates.length)return;const index=candidates.findIndex(c=>c.id===selected);select(candidates[(index+delta+candidates.length)%candidates.length]);}
|
||||
document.querySelector('#previous').addEventListener('click',()=>next(-1));document.querySelector('#next').addEventListener('click',()=>next(1));
|
||||
for(const svg of svgs)svg.addEventListener('click',event=>{const id=event.target.closest('[data-change-id]')?.dataset.changeId;if(byId.has(id))select(byId.get(id));});
|
||||
applyFilters();
|
||||
function expire(){const remaining=result.expires_at*1000-Date.now();if(remaining<=0){for(const view of views)view.destroy();document.querySelector('.workspace').replaceChildren();state.textContent='This comparison link has expired';}else setTimeout(expire,Math.min(2147483647,remaining));}expire();
|
||||
}
|
||||
start().catch(error=>{state.classList.remove('loading');state.textContent=error.name==='AbortError'?'':error.message;});
|
||||
مرجع در شماره جدید
مسدود کردن کاربر