@@ -491,6 +491,29 @@ function updateGraphNodes(graphs) {
491491 } ) ;
492492 }
493493}
494+ function enableGraphPanning ( canvas ) {
495+ let drag = null ;
496+ canvas . addEventListener ( "pointerdown" , event => {
497+ if ( ! event . isPrimary || event . button !== 0 || event . target . closest ( ".roadmap-actionable,a,button" ) ) return ;
498+ drag = { id : event . pointerId , x : event . clientX , y : event . clientY , left : canvas . scrollLeft , top : canvas . scrollTop } ;
499+ canvas . setPointerCapture ( event . pointerId ) ;
500+ canvas . classList . add ( "is-panning" ) ;
501+ event . preventDefault ( ) ;
502+ } ) ;
503+ canvas . addEventListener ( "pointermove" , event => {
504+ if ( ! drag || drag . id !== event . pointerId ) return ;
505+ canvas . scrollLeft = drag . left + drag . x - event . clientX ;
506+ canvas . scrollTop = drag . top + drag . y - event . clientY ;
507+ event . preventDefault ( ) ;
508+ } ) ;
509+ const stop = event => {
510+ if ( ! drag || drag . id !== event . pointerId ) return ;
511+ drag = null ;
512+ canvas . classList . remove ( "is-panning" ) ;
513+ if ( canvas . hasPointerCapture ( event . pointerId ) ) canvas . releasePointerCapture ( event . pointerId ) ;
514+ } ;
515+ for ( const type of [ "pointerup" , "pointercancel" , "lostpointercapture" ] ) canvas . addEventListener ( type , stop ) ;
516+ }
494517function dependencyGraph ( rfc ) {
495518 const models = graphModels ( rfc ) ;
496519 const sources = models . map ( model => graphSource ( model , translated , true ) ) ;
@@ -507,6 +530,9 @@ function dependencyGraph(rfc) {
507530 activeGraphs = graphs ;
508531 for ( const [ modelIndex , model ] of models . entries ( ) ) {
509532 const canvas = element ( "div" , { class : "graph roadmap-canvas" } , element ( "p" , { class : "muted" } , "正在绘制路线图…" ) ) ;
533+ canvas . setAttribute ( "tabindex" , "0" ) ;
534+ canvas . setAttribute ( "aria-label" , `${ model . title } :拖动空白区域平移,点击节点查看详情` ) ;
535+ enableGraphPanning ( canvas ) ;
510536 const controls = element ( "div" , { class : "actions" } ) ;
511537 const legend = element ( "div" , { class : "graph-legend" } , ...[ "planned" , "in_progress" , "partial" , "implemented" , "accepted" ] . map ( state => element ( "span" , { class : `graph-key ${ state } ` } , state === "accepted" ? "已验收" : translated ( state ) ) ) ) ;
512538 const section = element ( "section" , { class : "roadmap-track" } , element ( "div" , { class : "panel-title" } , element ( "h3" , { } , model . title ) , controls ) , legend , canvas ) ;
@@ -558,7 +584,7 @@ function dependencyGraph(rfc) {
558584 const zoom = delta => { scale = Math . max ( .4 , Math . min ( 2.5 , scale + delta ) ) ; drawing . setAttribute ( "width" , String ( width * scale ) ) ; } ;
559585 const minus = button ( "−" , ( ) => zoom ( - .2 ) ) ; minus . setAttribute ( "aria-label" , `${ model . title } 缩小` ) ;
560586 const plus = button ( "+" , ( ) => zoom ( .2 ) ) ; plus . setAttribute ( "aria-label" , `${ model . title } 放大` ) ;
561- controls . append ( minus , plus , button ( "重置" , ( ) => { scale = 1 ; drawing . setAttribute ( "width" , String ( width ) ) ; } ) , button ( "下载 SVG" , ( ) => {
587+ controls . append ( minus , plus , button ( "重置" , ( ) => { scale = 1 ; drawing . setAttribute ( "width" , String ( width ) ) ; canvas . scrollLeft = canvas . scrollTop = 0 ; } ) , button ( "下载 SVG" , ( ) => {
562588 const exported = drawing . cloneNode ( true ) ;
563589 const style = document . createElementNS ( "http://www.w3.org/2000/svg" , "style" ) ;
564590 style . textContent = roadmapSVGStyles ;
0 commit comments