Изменения документа App

Редактировал(а) A1MTestAdmin 2026/10/20 11:19

От версии 45.1
отредактировано A1MTestAdmin
на 2026/10/19 02:21
Изменить комментарий: Imported from XAR
К версии 43.1
отредактировано A1MTestAdmin
на 2026/10/19 02:16
Изменить комментарий: Imported from XAR

Сводка

Подробности

Свойства страницы
Содержимое
... ... @@ -1,6 +1,6 @@
1 1  {{velocity}}
2 2  #set($discard = $xwiki.ssx.use('A1M.App'))
3 -#set($discard = $xwiki.jsx.use('A1M.App', {'minify': false}))
3 +#set($discard = $xwiki.jsx.use('A1M.App'))
4 4  #if($request.getMethod() == 'GET')
5 5  $services.a1mRead.render()
6 6  #else
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);});}