键盘导航
地图画布有焦点时,方向键平移100个逻辑像素;加号或等号放大一级,减号缩小一级,Shift配合缩放键改变两级。点击地图取得焦点,Tab可进入画布;输入框和代码编辑区继续处理自己的按键。Ctrl/Alt/Meta组合键、输入法组合、已被处理的事件及未知键交给宿主。当前共同键盘导航接通二维平移、缩放和方向旋转:Shift配合左右方向键旋转15度。Shift上下方向键的倾角、球面/三维输入仍待实现。
await map.keyboard.enable()、disable()、isEnabled()、isActive() 不接受参数;SDK使用view.keyboard。默认开关包含独立keyboard字段,interactive:false关闭全部默认导航。await map.keyboard.enableRotation()/disableRotation() 不接受参数,用独立的keyboardRotation字段控制全部方向键平移与Shift左右旋转;不影响缩放。关闭或重新启用keyboard不会覆盖该选项;选项默认true,整体keyboard仍需启用。禁用后仍保留其他导航开关、指针事件和主动相机操作;启用不产生按键。当前按键瞬时应用,共用相机变化事件;完成后isActive为false,不用假定的延迟或相机动画冒充输入活动。
监听sqs.gestureschange后查询初始policy,用历史event.gestures.keyboard同步自己编写的控件。停止脚本/销毁地图释放监听;控件布局由使用方决定。
动态库能力位SQSM_MAP_KEYBOARD_NAVIGATION=262144,ABI1。sqsm_map_keyboard_plan_json只读识别key及可选shiftKey/ctrlKey/altKey/metaKey:null表示未处理/禁用,否则返回pan的offset、zoom的delta或rotate的delta。旋转能力SQSM_MAP_KEYBOARD_ROTATION=524288,ABI仍为1;Shift左右方向键返回负/正15度,禁用rotation后全部方向键未处理,缩放保留。请求1KiB,键名32字节,未知字段或类型拒绝。宿主负责系统焦点,按返回动作调用共同panBy或把当前zoom加delta并按实际getMinZoom/getMaxZoom限值调用zoomTo;rotate动作使用当前bearing加delta调用rotateTo;三种动作使用duration:0。实际处理前后用sqsm_map_gesture_input_json报告keyboard活动。计划查询本身不移动相机,不改变命令水位或活动,不发送政策事件。输出独立拥有,失败清零,销毁后仍须释放。
相同跨端脚本 / 可直接复制的完整HTML。网页主线程/严格Worker与实际原生QuickJS分别验收;egui真实输入帧不等于完整桌面GUI。Windows/macOS、移动、硬件和正式发行待验收。
import {map} from "sqsmap";
await map.keyboard.enable();
export default {enabled:await map.keyboard.isEnabled(),active:await map.keyboard.isActive()};
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SqsMap keyboard navigation</title>
<style>body{margin:0;font:16px system-ui;background:#f5f7fa;color:#182330}main{display:grid;grid-template-columns:1fr 260px;min-height:100vh}#map{min-height:70vh}aside{padding:20px}fieldset{margin:12px 0}button{padding:8px;margin:4px}pre{white-space:pre-wrap;overflow-wrap:anywhere}@media(max-width:650px){main{grid-template-columns:1fr}}</style>
<main><div id="map"></div><aside><h1>Keyboard pan / bearing navigation</h1><p>These controls change default navigation. Your click listeners keep working.</p><div id="controls"></div><button id="rotation">Keyboard pan / rotation</button><button id="sample">Query actual activity</button><button id="external">External writer: disable drag</button><button id="destroy">Destroy map</button><pre id="status">Starting</pre></aside></main>
<script type="module">
import {MapView} from './dist/index.js';
const controls=document.querySelector('#controls'),status=document.querySelector('#status');let view,closed=false,busy=false,off,offPolicy,offMove,offUp,offCapture;
const buttons=[];async function refresh(){if(!view||closed)return;let snapshot;try{snapshot=await view.gesturesState();}catch(error){if(!closed)throw error;return;}if(closed)return;for(const {name,button} of buttons){button.textContent=name+': '+(snapshot[name]?'enabled':'disabled');button.disabled=busy;button.setAttribute('aria-pressed',String(snapshot[name]));}const rotation=document.querySelector('#rotation');rotation.textContent='Keyboard pan / rotation: '+(snapshot.keyboardRotation?'enabled':'disabled');rotation.setAttribute('aria-pressed',String(snapshot.keyboardRotation));rotation.disabled=busy||view.capabilities.keyboardRotation!==true;}
function fail(error){if(!closed)status.textContent=String(error);}
document.querySelector('#destroy').addEventListener('click',destroy);window.addEventListener('pagehide',destroy,{once:true});
try{
view=await MapView.create(document.querySelector('#map'),{worker:false});if(closed){view.destroy();throw Error('Initialization cancelled.');}view.jumpTo({center:[0,0],zoom:3});await view.flush({render:true});
view.addSource('demo',{type:'geojson',data:{type:'FeatureCollection',features:[{type:'Feature',properties:{},geometry:{type:'Point',coordinates:[0,0]}}]}});view.addLayer({id:'demo',source:'demo',type:'circle',paint:{'circle-radius':30,'circle-color':'#2563eb'}});await view.flush({render:true});
for(const name of ['dragPan','scrollZoom','doubleClickZoom','keyboard']){const button=document.createElement('button');button.type='button';controls.append(button);buttons.push({name,button});button.addEventListener('click',async()=>{if(busy||closed)return;busy=true;try{await refresh();if(await view[name].isEnabled())await view[name].disable();else await view[name].enable();status.textContent='Policy applied.';}catch(error){fail(error);}finally{busy=false;await refresh();}});}
document.querySelector('#rotation').addEventListener('click',async()=>{if(busy||closed)return;busy=true;try{await refresh();if((await view.gesturesState()).keyboardRotation)await view.keyboard.disableRotation();else await view.keyboard.enableRotation();status.textContent='Keyboard pan / rotation policy applied.';}catch(error){fail(error);}finally{busy=false;await refresh();}});
async function sample(){if(closed)return;try{const activity={};for(const name of ['dragPan','scrollZoom','doubleClickZoom','keyboard'])activity[name]=await view[name].isActive();if(!closed)status.textContent='Actual recognized activity: '+JSON.stringify(activity);}catch(error){fail(error);}}
document.querySelector('#sample').addEventListener('click',sample);view.canvas.addEventListener('lostpointercapture',sample);offCapture=()=>view.canvas.removeEventListener('lostpointercapture',sample);offMove=view.on('mousemove',()=>sample());offUp=view.on('mouseup',()=>sample());
offPolicy=view.onGesturesChange(()=>refresh().catch(fail));document.querySelector('#external').addEventListener('click',()=>{if(!closed)view.dragPan.disable().catch(fail);});
off=view.on('click',event=>{status.textContent='Click events remain available: '+JSON.stringify(event.lngLat);});await refresh();status.textContent='Ready. Click the map, then use arrows or + / -. Shift plus/minus changes two levels; Shift left/right rotates 15 degrees.';
}catch(error){fail(error);view?.destroy();closed=true;}
function destroy(){if(closed)return;closed=true;off?.unsubscribe();offPolicy?.();offMove?.unsubscribe();offUp?.unsubscribe();offCapture?.();view?.destroy();for(const {button} of buttons)button.disabled=true;document.querySelector('#destroy').disabled=true;document.querySelector('#external').disabled=true;document.querySelector('#sample').disabled=true;document.querySelector('#rotation').disabled=true;status.textContent='Destroyed; subscriptions released.';}
</script></html>