Изменения документа App
Редактировал(а) A1MTestAdmin 2026/10/20 10:43
От версии 44.1
отредактировано A1MTestAdmin
на 2026/10/19 02:19
на 2026/10/19 02:19
Изменить комментарий:
Imported from XAR
К версии 43.1
отредактировано A1MTestAdmin
на 2026/10/19 02:16
на 2026/10/19 02:16
Изменить комментарий:
Imported from XAR
Сводка
-
Объекты (1 изменено, 0 добавлено, 0 удалено)
Подробности
- XWiki.JavaScriptExtension[0]
-
- code
-
... ... @@ -105,27 +105,7 @@ 105 105 }); 106 106 return {nodes,unknown,sectors:keys}; 107 107 } 108 - // Measurement is injected: browser uses the actual SVG font, unit tests deterministic metrics. 109 - function wrapLabel(title,measure,maxWidth,maxLines=3) { 110 - const fullText=String(title||'Без названия').replace(/\s+/gu,' ').trim()||'Без названия'; 111 - if(!(maxWidth>0)||!Number.isInteger(maxLines)||maxLines<1)throw Error('Invalid label budget'); 112 - const segment=typeof Intl.Segmenter==='function'?new Intl.Segmenter('ru',{granularity:'grapheme'}):null; 113 - const chars=text=>segment?[...segment.segment(text)].map(s=>s.segment):Array.from(text); 114 - let rest=fullText;const lines=[]; 115 - while(rest&&lines.length<maxLines) { 116 - if(measure(rest)<=maxWidth){lines.push(rest);rest='';break;} 117 - const last=lines.length===maxLines-1,suffix=last?'…':'',units=chars(rest); 118 - let lo=0,hi=units.length; 119 - while(lo<hi){const mid=Math.ceil((lo+hi)/2);if(measure(units.slice(0,mid).join('')+suffix)<=maxWidth)lo=mid;else hi=mid-1;} 120 - if(!lo){if(measure('…')<=maxWidth)lines.push('…');break;} 121 - let piece=units.slice(0,lo).join(''),cut=piece.lastIndexOf(' '); 122 - if(cut>0)piece=piece.slice(0,cut); 123 - lines.push(piece.trimEnd()+suffix);rest=last?rest:rest.slice(piece.length).trimStart(); 124 - if(last)break; 125 - } 126 - return {lines,truncated:!!rest,fullText,maxWidth,maxLines}; 127 - } 128 - const api={roles,assess,filter,layout,wrapLabel}; 108 + const api={roles,assess,filter,layout}; 129 129 if(typeof module!=='undefined')module.exports=api; 130 130 root.A1MCentre=api; 131 131 if(typeof document==='undefined')return; ... ... @@ -139,31 +139,6 @@ 139 139 for(const [k,v] of Object.entries(attrs))el.setAttribute(k,String(v)); 140 140 if(text)el.textContent=text;parent.append(el);return el; 141 141 } 142 - function mapLabel(parent,title,x,top,width,fontSize,maxLines=3,kind='node') { 143 - const node=svgEl('text',{'text-anchor':'middle',class:'map-label map-label-'+kind,'data-max-width':width,'data-max-lines':maxLines},parent); 144 - node.style.fontSize=fontSize+'px'; 145 - const measure=text=>{node.textContent=text;return node.getComputedTextLength();}; 146 - const wrapped=wrapLabel(title,measure,width,maxLines),lineHeight=fontSize*1.3,margin=18; 147 - const cx=Math.max(margin+width/2,Math.min(700-margin-width/2,x)); 148 - const y=Math.max(margin,Math.min(700-margin-lineHeight*wrapped.lines.length,top)); 149 - node.replaceChildren();node.setAttribute('data-truncated',String(wrapped.truncated)); 150 - node.setAttribute('aria-label',wrapped.fullText); 151 - wrapped.lines.forEach((line,index)=>svgEl('tspan',{x:cx,y:y+fontSize+index*lineHeight},node,line)); 152 - return node; 153 - } 154 - function placeMapLabel(node,anchorX,anchorY,obstacles) { 155 - const box=node.getBBox(),margin=18,gap=6; 156 - const intersects=(a,b)=>a.x<b.x+b.width+gap&&a.x+a.width+gap>b.x&&a.y<b.y+b.height+gap&&a.y+a.height+gap>b.y; 157 - const bounded=(x,y)=>({x:Math.max(margin,Math.min(700-margin-box.width,x)),y:Math.max(margin,Math.min(700-margin-box.height,y)),width:box.width,height:box.height}); 158 - const candidates=[bounded(box.x,box.y),bounded(box.x,anchorY-18-box.height),bounded(box.x,anchorY+35+box.height)]; 159 - for(let y=margin;y<=700-margin-box.height;y+=24)for(let x=margin;x<=700-margin-box.width;x+=30)candidates.push(bounded(x,y)); 160 - const overlaps=b=>obstacles.filter(o=>intersects(b,o)).length; 161 - const scored=candidates.map(box=>({box,collisions:overlaps(box),distance:(box.x+box.width/2-anchorX)**2+(box.y-(anchorY+18))**2})); 162 - scored.sort((a,b)=>a.collisions-b.collisions||a.distance-b.distance); 163 - const chosen=scored[0].box,dx=chosen.x-box.x,dy=chosen.y-box.y; 164 - node.querySelectorAll('tspan').forEach(span=>{span.setAttribute('x',Number(span.getAttribute('x'))+dx);span.setAttribute('y',Number(span.getAttribute('y'))+dy);}); 165 - node.setAttribute('data-collision',String(overlaps(chosen)>0));obstacles.push(chosen); 166 - } 167 167 function init(host) { 168 168 let data; 169 169 try {data=JSON.parse(host.dataset.payload);if(data.schema!=='a1m-centre/2.0')throw Error('schema');} ... ... @@ -208,16 +208,11 @@ 208 208 const map=layout(rows,goal,groupSelect.value),svg=svgEl('svg',{viewBox:'0 0 700 700',class:'centre-map',role:'img','aria-label':'Три кольца вклада и подписанные секторы; полный список с объяснениями ниже'},output); 209 209 const defs=svgEl('defs',{},svg),marker=svgEl('marker',{id:'centre-arrow',viewBox:'0 0 10 10',refX:9,refY:5,markerWidth:5,markerHeight:5,orient:'auto'},defs); 210 210 svgEl('path',{d:'M1 1 L9 5 L1 9',fill:'none',stroke:'#596169'},marker); 211 - const fontSize=Math.max(13,Math.min(29,13*700/Math.max(300,svg.getBoundingClientRect().width))); 212 - [120,210,300].forEach((radius,i)=>{svgEl('circle',{cx:350,cy:350,r:radius,fill:'none',stroke:'#9aa6b2','stroke-width':1},svg);mapLabel(svg,roles[['direct','support','context'][i]],465,350-radius+4,230,fontSize,2,'ring');}); 213 - const sectorTitle=key=>byId.get(key)?.title||(groupSelect.value==='kind'?key:'Направление не указано'); 214 - map.sectors.forEach((key,i)=>{const angle=-Math.PI/2+i*2*Math.PI/map.sectors.length;svgEl('line',{x1:350,y1:350,x2:350+Math.cos(angle)*310,y2:350+Math.sin(angle)*310,stroke:'#ced3d8'},svg);const mid=angle+Math.PI/map.sectors.length;const label=mapLabel(svg,sectorTitle(key),350+Math.cos(mid)*305,350+Math.sin(mid)*305,180,fontSize,2,'sector');svgEl('title',{},label,sectorTitle(key));}); 215 - element('p','Секторы: '+map.sectors.map(sectorTitle).join(' · '),output,{class:'centre-note'}); 166 + [120,210,300].forEach((radius,i)=>{svgEl('circle',{cx:350,cy:350,r:radius,fill:'none',stroke:'#9aa6b2','stroke-width':1},svg);svgEl('text',{x:358,y:350-radius+18},svg,roles[['direct','support','context'][i]]);}); 167 + map.sectors.forEach((key,i)=>{const angle=-Math.PI/2+i*2*Math.PI/map.sectors.length;svgEl('line',{x1:350,y1:350,x2:350+Math.cos(angle)*310,y2:350+Math.sin(angle)*310,stroke:'#ced3d8'},svg);const mid=angle+Math.PI/map.sectors.length;svgEl('text',{x:350+Math.cos(mid)*325,y:350+Math.sin(mid)*325,'text-anchor':'middle'},svg,(byId.get(key)?.title||key).slice(0,30));}); 168 + element('p','Секторы: '+map.sectors.map(k=>byId.get(k)?.title||k).join(' · '),output,{class:'centre-note'}); 216 216 const placed=new Map(map.nodes.map(n=>[n.record.id,n])); 217 217 (data.edges||[]).forEach(e=>{const a=placed.get(e.source),b=placed.get(e.target);if(!a||!b)return;const l=svgEl('line',{x1:a.x,y1:a.y,x2:b.x,y2:b.y,stroke:'#596169','stroke-width':e.strength?0.6+e.strength*.25:.6,...(e.direction==='forward'?{'marker-end':'url(#centre-arrow)'}:{})},svg);svgEl('title',{},l,e.type+' · '+(e.strength||'не оценена')+' · '+e.note);}); 218 - const labelObstacles=[...svg.querySelectorAll('.map-label')].map(n=>n.getBBox()); 219 - map.nodes.forEach(n=>labelObstacles.push({x:n.x-12,y:n.y-12,width:24,height:24})); 220 - const pendingLabels=[]; 221 221 map.nodes.forEach(n=>{ 222 222 const a=svgEl('a',{href:n.record.url,'aria-label':n.record.title},svg),g=svgEl('g',{transform:`translate(${n.x},${n.y})`,fill:'#fff',stroke:'#0749a9','stroke-width':1.2,class:'node-shape'},a); 223 223 const kind=n.record.kind; ... ... @@ -228,10 +228,8 @@ 228 228 else svgEl('circle',{cx:0,cy:0,r:9},g); 229 229 if(n.record.whole)svgEl('circle',{cx:0,cy:0,r:2,fill:'#1a1a1a',stroke:'none'},g); 230 230 svgEl('title',{},a,n.record.title+' · '+roles[n.assessment.role]+' · '+n.assessment.reason); 231 - const label=mapLabel(a,n.record.title,n.x,n.y+18,190,fontSize,3,'node'); 232 - pendingLabels.push({label,node:n}); 181 + svgEl('text',{x:n.x+13,y:n.y+4},a,n.record.id); 233 233 }); 234 - pendingLabels.sort((a,b)=>a.node.ring-b.node.ring||a.node.record.id.localeCompare(b.node.record.id)).forEach(({label,node})=>placeMapLabel(label,node.x,node.y,labelObstacles)); 235 235 element('p','Сектор направления использует первое указанное направление; остальные сохраняются в фильтрах. Полные названия и объяснения доступны в списке. При плотной карте пользуйтесь фильтрами.',output,{class:'centre-note'}); 236 236 element('h3','Оценённые материалы',output);renderList(map.nodes.map(n=>n.record),goal); 237 237 element('h3','Вне колец: не оценено / связь не обоснована / не относится',output);renderList(map.unknown.map(n=>n.record),goal); ... ... @@ -245,9 +245,6 @@ 245 245 } 246 246 } 247 247 [q,mode,goalSelect,groupSelect].forEach(el=>el.addEventListener('input',render));render(); 248 - // Re-measure after embedded fonts load; keep the user's current goal and filters. 249 - if(document.fonts)document.fonts.ready.then(render); 250 - if(typeof ResizeObserver!=='undefined'){let oldWidth=host.clientWidth;new ResizeObserver(()=>{const width=host.clientWidth;if(width!==oldWidth){oldWidth=width;render();}}).observe(host);} 251 251 document.querySelectorAll('.a1m-fallback-registry').forEach(el=>el.classList.add('enhanced')); 252 252 } 253 253 function boot(){document.querySelectorAll('#a1m-centre:not([data-initialised])').forEach(h=>{h.dataset.initialised='true';init(h);});}