diff --git a/kmw/engine/18.0.115/keymanweb.js b/kmw/engine/18.0.115/keymanweb.js new file mode 100644 index 00000000..51903dac --- /dev/null +++ b/kmw/engine/18.0.115/keymanweb.js @@ -0,0 +1,30 @@ +"use strict";(()=>{var ra=Object.create;var Nt=Object.defineProperty,ca=Object.defineProperties,co=Object.getOwnPropertyDescriptor,oa=Object.getOwnPropertyDescriptors,aa=Object.getOwnPropertyNames,lo=Object.getOwnPropertySymbols,oo=Object.getPrototypeOf,ao=Object.prototype.hasOwnProperty,ga=Object.prototype.propertyIsEnumerable,da=Reflect.get;var ro=(a,t,e)=>t in a?Nt(a,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[t]=e,y=(a,t)=>{for(var e in t||(t={}))ao.call(t,e)&&ro(a,e,t[e]);if(lo)for(var e of lo(t))ga.call(t,e)&&ro(a,e,t[e]);return a},A=(a,t)=>ca(a,oa(t)),o=(a,t)=>Nt(a,"name",{value:t,configurable:!0});var ua=(a,t)=>()=>(t||a((t={exports:{}}).exports,t),t.exports),Sn=(a,t)=>{for(var e in t)Nt(a,e,{get:t[e],enumerable:!0})},Ba=(a,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of aa(t))!ao.call(a,i)&&i!==e&&Nt(a,i,{get:()=>t[i],enumerable:!(n=co(t,i))||n.enumerable});return a};var Ia=(a,t,e)=>(e=a!=null?ra(oo(a)):{},Ba(t||!a||!a.__esModule?Nt(e,"default",{value:a,enumerable:!0}):e,a));var Ge=(a,t,e,n)=>{for(var i=n>1?void 0:n?co(t,e):t,s=a.length-1,l;s>=0;s--)(l=a[s])&&(i=(n?l(t,e,i):l(i))||i);return n&&i&&Nt(t,e,i),i};var Ji=(a,t,e)=>da(oo(a),e,t);var W=(a,t,e)=>new Promise((n,i)=>{var s=c=>{try{r(e.next(c))}catch(g){i(g)}},l=c=>{try{r(e.throw(c))}catch(g){i(g)}},r=c=>c.done?n(c.value):Promise.resolve(c.value).then(s,l);r((e=e.apply(a,t)).next())});var uo=ua((yg,qs)=>{"use strict";var ba=Object.prototype.hasOwnProperty,_="~";function Vn(){}o(Vn,"Events");Object.create&&(Vn.prototype=Object.create(null),new Vn().__proto__||(_=!1));function ha(a,t,e){this.fn=a,this.context=t,this.once=e||!1}o(ha,"EE");function go(a,t,e,n,i){if(typeof e!="function")throw new TypeError("The listener must be a function");var s=new ha(e,n||a,i),l=_?_+t:t;return a._events[l]?a._events[l].fn?a._events[l]=[a._events[l],s]:a._events[l].push(s):(a._events[l]=s,a._eventsCount++),a}o(go,"addListener");function ki(a,t){--a._eventsCount===0?a._events=new Vn:delete a._events[t]}o(ki,"clearEvent");function w(){this._events=new Vn,this._eventsCount=0}o(w,"EventEmitter");w.prototype.eventNames=o(function(){var t=[],e,n;if(this._eventsCount===0)return t;for(n in e=this._events)ba.call(e,n)&&t.push(_?n.slice(1):n);return Object.getOwnPropertySymbols?t.concat(Object.getOwnPropertySymbols(e)):t},"eventNames");w.prototype.listeners=o(function(t){var e=_?_+t:t,n=this._events[e];if(!n)return[];if(n.fn)return[n.fn];for(var i=0,s=n.length,l=new Array(s);iFa,TouchLayoutKeySp:()=>Bo});var Fa=["T_*_MT_SHIFT_TO_SHIFT","T_*_MT_SHIFT_TO_CAPS","T_*_MT_SHIFT_TO_DEFAULT"],Bo=(c=>(c[c.normal=0]="normal",c[c.special=1]="special",c[c.specialActive=2]="specialActive",c[c.customSpecial=3]="customSpecial",c[c.customSpecialActive=4]="customSpecialActive",c[c.deadkey=8]="deadkey",c[c.blank=9]="blank",c[c.spacer=10]="spacer",c))(Bo||{});var ya=55296,pa=56319,Ca=56320,Ga=57343;function Ei(a){return a>=ya&&a<=pa}o(Ei,"Uni_IsSurrogate1");function Yi(a){return a>=Ca&&a<=Ga}o(Yi,"Uni_IsSurrogate2");var Io={modifierCodes:{LCTRL:m.LCTRLFLAG,RCTRL:m.RCTRLFLAG,LALT:m.LALTFLAG,RALT:m.RALTFLAG,SHIFT:m.K_SHIFTFLAG,CTRL:m.K_CTRLFLAG,ALT:m.K_ALTFLAG,META:m.K_METAFLAG,CAPS:m.CAPITALFLAG,NO_CAPS:m.NOTCAPITALFLAG,NUM_LOCK:m.NUMLOCKFLAG,NO_NUM_LOCK:m.NOTNUMLOCKFLAG,SCROLL_LOCK:m.SCROLLFLAG,NO_SCROLL_LOCK:m.NOTSCROLLFLAG,VIRTUAL_KEY:m.ISVIRTUALKEY,VIRTUAL_CHAR_KEY:m.VIRTUALCHARKEY},modifierBitmasks:{ALL:127,ALT_GR_SIM:5,CHIRAL:31,IS_CHIRAL:15,NON_CHIRAL:112,NON_LEGACY:111},stateBitmasks:{ALL:16128,CAPS:768,NUM_LOCK:3072,SCROLL_LOCK:12288},keyCodes:y({},Ni),codesUS:[["0123456789",";=,-./`","[\\]'"],[")!@#$%^&*(",":+<_>?~",'{|}"']],isFrameKey(a){switch(a){case"K_SHIFT":case"K_LOPT":case"K_ROPT":case"K_NUMLOCK":case"K_CAPS":return!0;default:if(Io.keyCodes[a]>=5e4)return!0}return!1},getModifierState(a){var t=0;a.indexOf("shift")>=0&&(t|=m.K_SHIFTFLAG);var e=!1;a.indexOf("leftctrl")>=0&&(t|=m.LCTRLFLAG,e=!0),a.indexOf("rightctrl")>=0&&(t|=m.RCTRLFLAG,e=!0),a.indexOf("ctrl")>=0&&!e&&(t|=m.K_CTRLFLAG);var n=!1;return a.indexOf("leftalt")>=0&&(t|=m.LALTFLAG,n=!0),a.indexOf("rightalt")>=0&&(t|=m.RALTFLAG,n=!0),a.indexOf("alt")>=0&&!n&&(t|=m.K_ALTFLAG),t},getStateFromLayer(a){var t=0;return a.indexOf("caps")>=0?t|=m.CAPITALFLAG:t|=m.NOTCAPITALFLAG,t}},C=Io;var $s=class $s{codeForEvent(t){return C.keyCodes[t.kName]||t.Lcode}forAny(t,e,n){var i="";if((i=this.forSpecialEmulation(t))!=null)return i;if(!e&&(i=this.forNumpadKeys(t))!=null)return i;if((i=this.forUnicodeKeynames(t,n))!=null)return i;if((i=this.forBaseKeys(t,n))!=null)return i;switch(this.codeForEvent(t)){default:return null}}isCommand(t){switch(this.codeForEvent(t)){default:return!1}}applyCommand(t,e){}forSpecialEmulation(t){switch(this.codeForEvent(t)){case C.keyCodes.K_BKSP:return"\b";case C.keyCodes.K_ENTER:return` +`;default:return null}}forNumpadKeys(t){if(t.Lcode>=C.keyCodes.K_NP0&&t.Lcode<=C.keyCodes.K_NPSLASH){if(t.Lcode<106)var e=t.Lcode-48;else e=t.Lcode-64;return String._kmwFromCharCode(e)}else return null}forUnicodeKeynames(t,e){let n=t.kName;if(!n||n.substr(0,2)!="U_")return null;let i="",s=n.substr(2).split("_");for(let l of s){let r=parseInt(l,16);if(0<=r&&r<=31||128<=r&&r<=159||isNaN(r)){e&&(e.errorLog="Suppressing Unicode control code in "+n);continue}else i+=String.kmwFromCharCode(r)}return i||null}forBaseKeys(t,e){let n=t.Lcode,i=t.Lmodifiers;if(i==m.K_SHIFTFLAG)i=1;else if(i!=0)return e&&(e.warningLog="KMW only defines default key output for the 'default' and 'shift' layers!"),null;try{if(n==C.keyCodes.K_SPACE)return" ";if(n>=C.keyCodes.K_0&&n<=C.keyCodes.K_9)return C.codesUS[i][0][n-C.keyCodes.K_0];if(n>=C.keyCodes.K_A&&n<=C.keyCodes.K_Z)return String.fromCharCode(n+(i?0:32));if(n>=C.keyCodes.K_COLON&&n<=C.keyCodes.K_BKQUOTE)return C.codesUS[i][1][n-C.keyCodes.K_COLON];if(n>=C.keyCodes.K_LBRKT&&n<=C.keyCodes.K_QUOTE)return C.codesUS[i][2][n-C.keyCodes.K_LBRKT];if(n==C.keyCodes.K_oE2)return i?"|":"\\"}catch(s){e&&(e.errorLog="Error detected with default mapping for key: code = "+n+", shift state = "+(i==1?"shift":"default"))}return null}};o($s,"DefaultRules");var Ae=$s;var Qa=new Ae,An=class An{constructor(t){this.isSynthetic=!0;for(let e in t)t[e]!==void 0&&(this[e]=t[e])}static constructNullKeyEvent(t){return new An({Lcode:0,kName:"",device:t,Lstates:void 0,Lmodifiers:void 0,vkCode:void 0,LisVirtualKey:void 0})}get isModifier(){switch(this.Lcode){case 16:case 17:case 18:case 20:case 144:case 145:return!0;default:return!1}}setMnemonicCode(t,e){if(this.Lcode!=C.keyCodes.K_SPACE){let i=new An(this);for(let s in this)i[s]=this[s];i.kName="K_xxxx",i.Lmodifiers=t?16:0;var n=Qa.forAny(i,!0);this.vkCode=this.Lcode,n?this.Lcode=n.charCodeAt(0):this.isModifier||delete this.Lcode}e&&(this.Lcode>=65&&this.Lcode<=90||this.Lcode>=97&&this.Lcode<=122)&&(this.Lmodifiers^=16,this.Lcode^=32)}};o(An,"KeyEvent");var ee=An;var nl=class nl{};o(nl,"KeyMap");var Ne=nl,il=class il{constructor(){this.FF=new Ne;this.Safari=new Ne;this.Opera=new Ne;this.FF.k61=187,this.FF.k59=186,this.FF.k173=189}};o(il,"BrowserKeyMaps");var el=il,sl=class sl{constructor(){this.se=new Ne,this.se.k220=192,this.se.k187=189,this.se.k219=187,this.se.k221=219,this.se.k186=221,this.se.k191=220,this.se.k192=186,this.se.k189=191,this.uk=new Ne,this.uk.k223=192,this.uk.k192=222,this.uk.k222=226,this.uk.k220=220}};o(sl,"LanguageKeyMaps");var tl=sl,We=class We{constructor(){}static _usCodeInit(){var t=new Ne,e=new Ne;t.k192=96,t.k49=49,t.k50=50,t.k51=51,t.k52=52,t.k53=53,t.k54=54,t.k55=55,t.k56=56,t.k57=57,t.k48=48,t.k189=45,t.k187=61,t.k81=113,t.k87=119,t.k69=101,t.k82=114,t.k84=116,t.k89=121,t.k85=117,t.k73=105,t.k79=111,t.k80=112,t.k219=91,t.k221=93,t.k220=92,t.k65=97,t.k83=115,t.k68=100,t.k70=102,t.k71=103,t.k72=104,t.k74=106,t.k75=107,t.k76=108,t.k186=59,t.k222=39,t.k90=122,t.k88=120,t.k67=99,t.k86=118,t.k66=98,t.k78=110,t.k77=109,t.k188=44,t.k190=46,t.k191=47,e.k192=126,e.k49=33,e.k50=64,e.k51=35,e.k52=36,e.k53=37,e.k54=94,e.k55=38,e.k56=42,e.k57=40,e.k48=41,e.k189=95,e.k187=43,e.k81=81,e.k87=87,e.k69=69,e.k82=82,e.k84=84,e.k89=89,e.k85=85,e.k73=73,e.k79=79,e.k80=80,e.k219=123,e.k221=125,e.k220=124,e.k65=65,e.k83=83,e.k68=68,e.k70=70,e.k71=71,e.k72=72,e.k74=74,e.k75=75,e.k76=76,e.k186=58,e.k222=34,e.k90=90,e.k88=88,e.k67=67,e.k86=86,e.k66=66,e.k78=78,e.k77=77,e.k188=60,e.k190=62,e.k191=63,We._usCharCodes=[t,e]}static _USKeyCodeToCharCode(t){return We.usCharCodes[t.Lmodifiers&16?1:0]["k"+t.Lcode]}static get usCharCodes(){return We._usCharCodes||We._usCodeInit(),We._usCharCodes}};o(We,"KeyMapping"),We.browserMap=new el,We.languageMap=new tl;var te=We;function ce(a){if(typeof a!="object"||!a)return a;{let t=Array.isArray(a)?[]:{},e=Object.keys(a);for(let n of e)a[n]!==void 0&&(t[n]=ce(a[n]));return t}}o(ce,"deepCopy");var N=class N{constructor(t,e,n,i){switch(t.toLowerCase()){case N.Browser.Chrome:case N.Browser.Edge:case N.Browser.Firefox:case N.Browser.Native:case N.Browser.Opera:case N.Browser.Safari:this.browser=t.toLowerCase();break;default:this.browser=N.Browser.Other}switch(e.toLowerCase()){case N.FormFactor.Desktop:case N.FormFactor.Phone:case N.FormFactor.Tablet:this.formFactor=e.toLowerCase();break;default:throw"Invalid form factor specified for device: "+e}switch(n.toLowerCase()){case N.OperatingSystem.Windows.toLowerCase():case N.OperatingSystem.macOS.toLowerCase():case N.OperatingSystem.Linux.toLowerCase():case N.OperatingSystem.Android.toLowerCase():case N.OperatingSystem.iOS.toLowerCase():this.OS=n.toLowerCase();break;default:this.OS=N.OperatingSystem.Other}this.touchable=i}};o(N,"DeviceSpec");var Ft=N;(n=>{let a;(u=>(u.Chrome="chrome",u.Edge="edge",u.Firefox="firefox",u.Native="native",u.Opera="opera",u.Safari="safari",u.Other="other"))(a=n.Browser||(n.Browser={}));let t;(d=>(d.Windows="windows",d.macOS="macosx",d.Linux="linux",d.Android="android",d.iOS="ios",d.Other="other"))(t=n.OperatingSystem||(n.OperatingSystem={}));let e;(r=>(r.Desktop="desktop",r.Phone="phone",r.Tablet="tablet"))(e=n.FormFactor||(n.FormFactor={}))})(Ft||(Ft={}));function ll(a){return new Ft(a.browser,"desktop",a.OS,!1)}o(ll,"physicalKeyDeviceAlias");var V=Ft;var oe=class oe{};o(oe,"KEYMAN_VERSION"),oe.VERSION="18.0.115",oe.VERSION_RELEASE="18.0",oe.VERSION_MAJOR="18",oe.VERSION_MINOR="0",oe.VERSION_PATCH="115",oe.TIER="alpha",oe.VERSION_TAG="-alpha",oe.VERSION_WITH_TAG="18.0.115-alpha",oe.VERSION_ENVIRONMENT="alpha",oe.VERSION_GIT_TAG="release@18.0.115-alpha";var Wn=oe,et=Wn;var Ie=class Ie{constructor(t){if(t==null){this.components=[].concat(Ie.DEVELOPER_VERSION_FALLBACK.components);return}if(Array.isArray(t)){let i=t;if(i.length<2)throw new Error("Version string must have at least a major and minor component!");this.components=[].concat(i);return}let e=t.split("."),n=[];if(e.length<2)throw new Error("Version string must have at least a major and minor component!");for(let i=0;i0)return e?-1:1;i++}while(i1114111||Math.floor(n)!==n)throw new RangeError("Invalid code point "+n);n<65536?t.push(n):(n-=65536,t.push((n>>10)+55296),t.push(n%1024+56320))}return String.fromCharCode.apply(void 0,t)},String.prototype.kmwCharCodeAt=function(a){var t=String(this),e=0;if(a<0||a>=t.length)return NaN;for(var n=0;n=55296&&i<=56319&&t.length>e+1){var s=t.charCodeAt(e+1);if(s>=56320&&s<=57343)return(i-55296<<10)+(s-56320)+65536}return i},String.prototype.kmwIndexOf=function(a,t){var e=String(this),n=e.indexOf(a,t);if(n<0)return n;for(var i=0,s=0;s!==null&&st){var s=a;a=t,t=s}n=e.kmwCodePointToCodeUnit(a),i=e.kmwCodePointToCodeUnit(t)}return(isNaN(n)||n===null)&&(n=0),(isNaN(i)||i===null)&&(i=e.length),e.substring(n,i)},String.prototype.kmwNextChar=function(a){var t=String(this);if(a===null||a<0||a>=t.length-1)return null;var e=t.charCodeAt(a);if(e>=55296&&e<=56319&&t.length>a+1){var n=t.charCodeAt(a+1);if(n>=56320&&n<=57343)return a==t.length-2?null:a+2}return a+1},String.prototype.kmwPrevChar=function(a){var t=String(this);if(a==null||a<=0||a>t.length)return null;var e=t.charCodeAt(a-1);if(e>=56320&&e<=57343&&a>1){var n=t.charCodeAt(a-2);if(n>=55296&&n<=56319)return a-2}return a-1},String.prototype.kmwCodePointToCodeUnit=function(a){if(a===null)return null;var t=String(this),e=0;if(a<0){e=t.length;for(var n=0;n>a;n--)e=t.kmwPrevChar(e);return e}if(a==t.kmwLength())return t.length;for(var n=0;n=0?t.kmwSubstr(a,1):""},String.prototype.kmwBMPNextChar=function(a){var t=String(this);return a<0||a>=t.length-1?null:a+1},String.prototype.kmwBMPPrevChar=function(a){var t=String(this);return a<=0||a>t.length?null:a-1},String.prototype.kmwBMPCodePointToCodeUnit=function(a){return a},String.prototype.kmwBMPCodeUnitToCodePoint=function(a){return a},String.prototype.kmwBMPLength=function(){var a=String(this);return a.length},String.prototype.kmwBMPSubstr=function(a,t){var e=String(this);return a>-1?e.substr(a,t):e.substr(e.length+a,-a)},String.kmwEnableSupplementaryPlane=function(a){var t=String.prototype;String._kmwFromCharCode=a?String.kmwFromCharCode:String.fromCharCode,t._kmwCharAt=a?t.kmwCharAt:t.charAt,t._kmwCharCodeAt=a?t.kmwCharCodeAt:t.charCodeAt,t._kmwIndexOf=a?t.kmwIndexOf:t.indexOf,t._kmwLastIndexOf=a?t.kmwLastIndexOf:t.lastIndexOf,t._kmwSlice=a?t.kmwSlice:t.slice,t._kmwSubstring=a?t.kmwSubstring:t.substring,t._kmwSubstr=a?t.kmwSubstr:t.kmwBMPSubstr,t._kmwLength=a?t.kmwLength:t.kmwBMPLength,t._kmwNextChar=a?t.kmwNextChar:t.kmwBMPNextChar,t._kmwPrevChar=a?t.kmwPrevChar:t.kmwBMPPrevChar,t._kmwCodePointToCodeUnit=a?t.kmwCodePointToCodeUnit:t.kmwBMPCodePointToCodeUnit,t._kmwCodeUnitToCodePoint=a?t.kmwCodeUnitToCodePoint:t.kmwBMPCodeUnitToCodePoint},String._kmwFromCharCode||String.kmwEnableSupplementaryPlane(!1)}o(fn,"extendString");fn();var rl=class rl{constructor(t){this._isFulfilled=!1;this._isRejected=!1;this._promise=new Promise((e,n)=>{this._resolve=i=>{this._isFulfilled=!0,e(i)},this._reject=i=>{this._isRejected=!0,n(i)},t&&t(this._resolve,this._reject)})}get resolve(){return this._resolve}get reject(){return this._reject}get isFulfilled(){return this._isFulfilled}get isRejected(){return this._isRejected}get isResolved(){return this.isFulfilled||this.isRejected}then(t,e){return this._promise.then(t,e)}catch(t){return this._promise.catch(t)}finally(t){return this._promise.finally(t)}get corePromise(){return this._promise}};o(rl,"ManagedPromise");var f=rl;var cl=class cl extends f{constructor(e){let n=null;super(l=>{n=setTimeout(()=>{this.isResolved||l(!0)},e)});this.timerHandle=n;let i=this._resolve;this._resolve=l=>{clearTimeout(this.timerHandle),i(l)};let s=this._reject;this._reject=l=>{clearTimeout(this.timerHandle),s(l)}}};o(cl,"TimeoutPromise");var Ee=cl,ae=o(a=>new Ee(a).corePromise,"timedPromise");var J=ma.TouchLayoutKeySp;var Ua=200,v=class v{static buildDefaultLayout(t,e,n){var Xn;let i=n;typeof v.dfltLayout[i]!="object"&&(i="desktop");let s=C.modifierBitmasks.NON_CHIRAL,l=M.CURRENT;e&&(s=e.modifierBitmask,l=e.compilerVersion),t||(t=this.DEFAULT_RAW_SPEC);var r=ce(v.dfltLayout[i]),c,g=r.layer,d=t.KLS,u=t.K102,I,B,h,b,F,G,U=(s&C.modifierBitmasks.IS_CHIRAL)!=0;if(t.F){let ke=/^(?:(?:italic|bold) )* *[0-9.eE-]+(?:[a-z]+) "(.+)"$/.exec(t.F);ke&&(r.font=ke[1])}var Q=!(typeof d=="undefined"||!d);Q||(d=t.KLS=v.processLegacyDefinitions(t.BK));var p=Object.getOwnPropertyNames(d),X=[];if(p.splice(p.indexOf("default"),1),p=["default"].concat(p),e&&e.emulatesAltGr&&(p.indexOf("leftctrl-leftalt")==-1&&p.indexOf("rightalt")!=-1&&(p.push("leftctrl-leftalt"),d["leftctrl-leftalt"]=d.rightalt),p.indexOf("leftctrl-leftalt-shift")==-1&&p.indexOf("rightalt-shift")!=-1&&(p.push("leftctrl-leftalt-shift"),d["leftctrl-leftalt-shift"]=d["rightalt-shift"])),r.displayUnderlying=e?!!e.scriptObject.KDU:!1,n=="desktop")for(X=v.generateLayerIds(U),c=0;c0&&(g[c]=ce(g[0])),g[c].id=R[c],g[c].nextlayer=R[c],v.formatDefaultLayer(g[c],U,n,!!u);for(c=0;c=0&&Je0&&L!=null&&(L.sp=J.specialActive,L.sk=null,L.text=(Xn=v.modifierSpecials[$e])!=null?Xn:"*Shift*")}return r}static getLayerId(t){var e="";return t==0?"default":(t&m.LCTRLFLAG&&(e=(e.length>0?e+"-":"")+"leftctrl"),t&m.RCTRLFLAG&&(e=(e.length>0?e+"-":"")+"rightctrl"),t&m.LALTFLAG&&(e=(e.length>0?e+"-":"")+"leftalt"),t&m.RALTFLAG&&(e=(e.length>0?e+"-":"")+"rightalt"),t&m.K_SHIFTFLAG&&(e=(e.length>0?e+"-":"")+"shift"),t&m.K_CTRLFLAG&&(e=(e.length>0?e+"-":"")+"ctrl"),t&m.K_ALTFLAG&&(e=(e.length>0?e+"-":"")+"alt"),e)}static generateLayerIds(t){var e,n;t?(e=32,n=1):(e=8,n=16);for(var i=[],s=0;s?~~~~~ ",v.DEFAULT_RAW_SPEC={F:"Tahoma",BK:v.dfltText.split("")},v.modifierSpecials={leftalt:"*LAlt*",rightalt:"*RAlt*",alt:"*Alt*",leftctrl:"*LCtrl*",rightctrl:"*RCtrl*",ctrl:"*Ctrl*","ctrl-alt":"*AltGr*","leftctrl-leftalt":"*LAltCtrl*","rightctrl-rightalt":"*RAltCtrl*","leftctrl-leftalt-shift":"*LAltCtrlShift*","rightctrl-rightalt-shift":"*RAltCtrlShift*",shift:"*Shift*","shift-alt":"*AltShift*","shift-ctrl":"*CtrlShift*","shift-ctrl-alt":"*AltCtrlShift*","leftalt-shift":"*LAltShift*","rightalt-shift":"*RAltShift*","leftctrl-shift":"*LCtrlShift*","rightctrl-shift":"*RCtrlShift*"},v.dfltShiftToCaps={id:"T_*_MT_SHIFT_TO_CAPS",text:"*ShiftLock*",sp:1,nextlayer:"caps"},v.dfltShiftToDefault={id:"T_*_MT_SHIFT_TO_DEFAULT",text:"*Shift*",sp:1,nextlayer:"default"},v.dfltShiftToShift={id:"T_*_MT_SHIFT_TO_SHIFT",text:"*Shift*",sp:1,nextlayer:"shift"},v.dfltLayout={desktop:{defaultHint:"dot",font:"Tahoma,Helvetica",layer:[{id:"default",row:[{id:1,key:[{id:"K_BKQUOTE"},{id:"K_1"},{id:"K_2"},{id:"K_3"},{id:"K_4"},{id:"K_5"},{id:"K_6"},{id:"K_7"},{id:"K_8"},{id:"K_9"},{id:"K_0"},{id:"K_HYPHEN"},{id:"K_EQUAL"},{id:"K_BKSP",text:"*BkSp*",sp:1,width:130}]},{id:2,key:[{id:"K_TAB",text:"*Tab*",sp:1,width:130},{id:"K_Q"},{id:"K_W"},{id:"K_E"},{id:"K_R"},{id:"K_T"},{id:"K_Y"},{id:"K_U"},{id:"K_I"},{id:"K_O"},{id:"K_P"},{id:"K_LBRKT"},{id:"K_RBRKT"},{id:"K_BKSLASH"}]},{id:3,key:[{id:"K_CAPS",text:"*Caps*",sp:1,width:165},{id:"K_A"},{id:"K_S"},{id:"K_D"},{id:"K_F"},{id:"K_G"},{id:"K_H"},{id:"K_J"},{id:"K_K"},{id:"K_L"},{id:"K_COLON"},{id:"K_QUOTE"},{id:"K_ENTER",text:"*Enter*",sp:1,width:165}]},{id:4,key:[{id:"K_SHIFT",text:"*Shift*",sp:1,width:130},{id:"K_oE2"},{id:"K_Z"},{id:"K_X"},{id:"K_C"},{id:"K_V"},{id:"K_B"},{id:"K_N"},{id:"K_M"},{id:"K_COMMA"},{id:"K_PERIOD"},{id:"K_SLASH"},{id:"K_RSHIFT",text:"*Shift*",sp:1,width:130}]},{id:5,key:[{id:"K_LCONTROL",text:"*Ctrl*",sp:1,width:170},{id:"K_LALT",text:"*Alt*",sp:1,width:160},{id:"K_SPACE",text:"",width:770},{id:"K_RALT",text:"*Alt*",sp:1,width:160},{id:"K_RCONTROL",text:"*Ctrl*",sp:1,width:170}]}]}]},tablet:{defaultHint:"dot",font:"Tahoma,Helvetica",layer:[{id:"default",row:[{id:0,key:[{id:"K_1"},{id:"K_2"},{id:"K_3"},{id:"K_4"},{id:"K_5"},{id:"K_6"},{id:"K_7"},{id:"K_8"},{id:"K_9"},{id:"K_0"},{id:"K_HYPHEN"},{id:"K_EQUAL"},{sp:10,width:1}]},{id:1,key:[{id:"K_Q",pad:25},{id:"K_W"},{id:"K_E"},{id:"K_R"},{id:"K_T"},{id:"K_Y"},{id:"K_U"},{id:"K_I"},{id:"K_O"},{id:"K_P"},{id:"K_LBRKT"},{id:"K_RBRKT"},{sp:10,width:1}]},{id:2,key:[{id:"K_A",pad:50},{id:"K_S"},{id:"K_D"},{id:"K_F"},{id:"K_G"},{id:"K_H"},{id:"K_J"},{id:"K_K"},{id:"K_L"},{id:"K_COLON"},{id:"K_QUOTE"},{id:"K_BKSLASH",width:90}]},{id:3,key:[{id:"K_oE2",width:90},{id:"K_Z"},{id:"K_X"},{id:"K_C"},{id:"K_V"},{id:"K_B"},{id:"K_N"},{id:"K_M"},{id:"K_COMMA"},{id:"K_PERIOD"},{id:"K_SLASH"},{id:"K_BKQUOTE"},{sp:10,width:1}]},{id:4,key:[{id:"K_SHIFT",text:"*Shift*",sp:1,width:200,sk:[{id:"K_LCONTROL",text:"*Ctrl*",sp:1,width:50,nextlayer:"ctrl"},{id:"K_LCONTROL",text:"*LCtrl*",sp:1,width:50,nextlayer:"leftctrl"},{id:"K_RCONTROL",text:"*RCtrl*",sp:1,width:50,nextlayer:"rightctrl"},{id:"K_LALT",text:"*Alt*",sp:1,width:50,nextlayer:"alt"},{id:"K_LALT",text:"*LAlt*",sp:1,width:50,nextlayer:"leftalt"},{id:"K_RALT",text:"*RAlt*",sp:1,width:50,nextlayer:"rightalt"},{id:"K_ALTGR",text:"*AltGr*",sp:1,width:50,nextlayer:"ctrl-alt"}]},{id:"K_LOPT",text:"*Menu*",sp:1,width:150},{id:"K_SPACE",text:"",width:570},{id:"K_BKSP",text:"*BkSp*",sp:1,width:150},{id:"K_ENTER",text:"*Enter*",sp:1,width:200}]}]}]},phone:{defaultHint:"dot",font:"Tahoma,Helvetica",layer:[{id:"default",row:[{id:0,key:[{id:"K_1"},{id:"K_2"},{id:"K_3"},{id:"K_4"},{id:"K_5"},{id:"K_6"},{id:"K_7"},{id:"K_8"},{id:"K_9"},{id:"K_0"},{id:"K_HYPHEN"},{id:"K_EQUAL"},{sp:10,width:1}]},{id:1,key:[{id:"K_Q",pad:25},{id:"K_W"},{id:"K_E"},{id:"K_R"},{id:"K_T"},{id:"K_Y"},{id:"K_U"},{id:"K_I"},{id:"K_O"},{id:"K_P"},{id:"K_LBRKT"},{id:"K_RBRKT"},{sp:10,width:1}]},{id:2,key:[{id:"K_A",pad:50},{id:"K_S"},{id:"K_D"},{id:"K_F"},{id:"K_G"},{id:"K_H"},{id:"K_J"},{id:"K_K"},{id:"K_L"},{id:"K_COLON"},{id:"K_QUOTE"},{id:"K_BKSLASH",width:90}]},{id:3,key:[{id:"K_oE2",width:90},{id:"K_Z"},{id:"K_X"},{id:"K_C"},{id:"K_V"},{id:"K_B"},{id:"K_N"},{id:"K_M"},{id:"K_COMMA"},{id:"K_PERIOD"},{id:"K_SLASH"},{id:"K_BKQUOTE"},{sp:10,width:1}]},{id:4,key:[{id:"K_SHIFT",text:"*Shift*",sp:1,width:200,sk:[{id:"K_LCONTROL",text:"*Ctrl*",sp:1,width:50,nextlayer:"ctrl"},{id:"K_LCONTROL",text:"*LCtrl*",sp:1,width:50,nextlayer:"leftctrl"},{id:"K_RCONTROL",text:"*RCtrl*",sp:1,width:50,nextlayer:"rightctrl"},{id:"K_LALT",text:"*Alt*",sp:1,width:50,nextlayer:"alt"},{id:"K_LALT",text:"*LAlt*",sp:1,width:50,nextlayer:"leftalt"},{id:"K_RALT",text:"*RAlt*",sp:1,width:50,nextlayer:"rightalt"},{id:"K_ALTGR",text:"*AltGr*",sp:1,width:50,nextlayer:"ctrl-alt"}]},{id:"K_LOPT",text:"*Menu*",width:150,sp:1},{id:"K_SPACE",width:570,text:""},{id:"K_BKSP",text:"*BkSp*",width:150,sp:1},{id:"K_ENTER",text:"*Enter*",width:200,sp:1}]}]}]}};var E=v;function fe(a,t,e){e.enumerable=!0}o(fe,"Enumerable");var bo={id:"string",text:"string",layer:"string",nextlayer:"string",font:"string",fontsize:"string",sp:"number",pad:"number",width:"number",sk:"subkeys",flick:"flicks",multitap:"subkeys",hint:"string",default:"boolean"},ho=["n","ne","e","se","s","sw","w","nw"];function ol(a,t){let e=Object.getPrototypeOf(t);for(let n in t)if(!a.hasOwnProperty(n)){let i=Object.getOwnPropertyDescriptor(e,n);i?Object.defineProperty(a,n,i):a[n]=t[n]}return a}o(ol,"assignDefaultsWithPropDefs");var q=class q{constructor(t,e,n){this.isMnemonic=!1;Object.assign(this,t),!this.text&&typeof this.id=="string"&&(this.text=ne.unicodeIDToText(this.id)),this.displayLayer=n,this.layer=this.layer||n,this._baseKeyEvent=()=>this.constructBaseKeyEvent(e,n)}get baseKeyID(){if(typeof this.id!="undefined")return this.id}get isPadding(){return this.sp==J.spacer}get coreID(){if(typeof this.id=="undefined")return;let t=this.id||"";return this.displayLayer!=this.layer&&(t=t+"+"+this.layer),t}get elementID(){if(typeof this.id!="undefined")return this.displayLayer+"-"+this.coreID}get baseKeyEvent(){let t=this._baseKeyEvent;return typeof t=="function"&&(t=t()),new ee(t)}static unicodeIDToText(t,e){if(!t||t.substring(0,2)!="U_")return null;let n="",i=t.substring(2).split("_");for(let s of i){let l=parseInt(s,16);if(0<=l&&l<=31||128<=l&&l<=159||isNaN(l)){e&&e(s);continue}else n+=String.kmwFromCharCode(l)}return n||null}static sanitize(t){typeof t.width=="string"&&(t.width=parseInt(t.width,10)),t.width||(t.width=ne.DEFAULT_KEY_WIDTH),typeof t.pad=="string"&&(t.pad=parseInt(t.pad,10)),t.pad||(t.pad=ne.DEFAULT_PAD),typeof t.sp=="string"&&(t.sp=Number.parseInt(t.sp,10)),t.sp||(t.sp=ne.DEFAULT_KEY.sp);for(let e of Object.keys(bo)){let n=bo[e];switch(n){case"subkeys":let i=t[e];if(i===void 0)break;if(!Array.isArray(i))delete t[e];else for(let r=0;r0){let I=l[l.length-1];if(l.length==1&&I.pad<0){let B=I.width/i,h=1-(g+B+d);c(I,h,B,g)}else{let B=I.pad/i,h=1-(g+B+d);c(I,B,h,g)}}ol(t,new tt);let u=t;u.proportionalY=s}populateKeyMap(t){this.key.forEach(function(e){e.coreID&&(t[e.coreID]=e)})}};o(tt,"ActiveRow"),tt.SPECIAL_LABEL=/\*\w+\*/,Ge([fe],tt.prototype,"populateKeyMap",1);var Ti=tt,Yt=class Yt{constructor(){}static sanitize(t){for(let e of t.row)Ti.sanitize(e)}static polyfill(t,e){t.aligned=!1;let n=t.row,i=0;for(let r of n){let c=0,g=r.key;for(let d of g)c+=d.width+d.pad;c>i&&(i=c)}e.formFactor=="desktop"?i+=5:i+=ne.DEFAULT_RIGHT_MARGIN;let s=t.row.length;for(let r=0;r1?this.keyMap[e[0]].getSubkey(e[1]):this.keyMap[t]}};o(Yt,"ActiveLayer"),Ge([fe],Yt.prototype,"constructKeyMap",1),Ge([fe],Yt.prototype,"getKey",1);var wi=Yt,Tt=class Tt{constructor(){this.hasFlicks=!1;this.hasLongpresses=!1;this.hasMultitaps=!1}getLayer(t){if(!this.layerMap[t]){let e=this.layer.find(n=>n.id==t);if(!e)return null;wi.sanitize(e),wi.polyfill(e,this),this.layerMap[t]=e}return this.layerMap[t]}static correctLayerEmptyRowBug(t){for(let e=0;e=0;s--)(!Array.isArray(i[s].key)||i[s].key.length==0)&&i.splice(s,1)}}static sanitize(t){Tt.correctLayerEmptyRowBug(t.layer)}static polyfill(t,e,n){if(t==null)throw new Error("Cannot build an ActiveLayout for a null specification.");let i={hasFlicks:!1,hasLongpresses:!1,hasMultitaps:!1};this.sanitize(t);for(let r of t.layer)for(let c of r.row)for(let g of c.key)i.hasLongpresses||(i.hasLongpresses=!!g.sk),i.hasFlicks||(i.hasFlicks=!!g.flick),i.hasMultitaps||(i.hasMultitaps=!!g.multitap);let s={};ol(t,new Tt);let l=t;if(l.keyboard=e,l.formFactor=n,l.layerMap=s,n!="desktop"&&t.layer.find(r=>r.id=="caps")){let r=l.getLayer("default"),c=l.getLayer("shift"),g=r.getKey("K_SHIFT"),d=c==null?void 0:c.getKey("K_SHIFT");g&&d&&!g.multitap&&!d.multitap&&!g.sk&&!d.sk&&(i.hasMultitaps=!0,g.multitap=[y({},E.dfltShiftToCaps),y({},E.dfltShiftToDefault)],d.multitap=[y({},E.dfltShiftToCaps),y({},E.dfltShiftToShift)],g.multitap.forEach((u,I)=>g.multitap[I]=new mt(u,l,"default")),d.multitap.forEach((u,I)=>d.multitap[I]=new mt(u,l,"shift")))}return l.hasFlicks=i.hasFlicks,l.hasLongpresses=i.hasLongpresses,l.hasMultitaps=i.hasMultitaps,l}};o(Tt,"ActiveLayout"),Ge([fe],Tt.prototype,"getLayer",1);var wt=Tt;var dl=class dl{constructor(){this.stores={}}};o(dl,"CacheTag");var gl=dl,Ki=(n=>(n[n.NOT_LOADED=void 0]="NOT_LOADED",n[n.POLYFILLED=1]="POLYFILLED",n[n.CALIBRATED=2]="CALIBRATED",n))(Ki||{}),Mt=class Mt{constructor(t){t?this.scriptObject=t:this.scriptObject=Mt.DEFAULT_SCRIPT_OBJECT,this.layoutStates={}}process(t,e){return this.scriptObject.gs(t,e)}processNewContextEvent(t,e){return this.scriptObject.gn?this.scriptObject.gn(t,e):!1}processPostKeystroke(t,e){return this.scriptObject.gpk?this.scriptObject.gpk(t,e):!1}get isHollow(){return this.scriptObject==Mt.DEFAULT_SCRIPT_OBJECT}get id(){return this.scriptObject.KI}get name(){return this.scriptObject.KN}get variableStores(){let t=this.scriptObject.KVS,e={};if(Array.isArray(t))for(let n of t)e[n]=this.scriptObject[n];return e}set variableStores(t){let e=this.scriptObject.KVS;if(Array.isArray(e))for(let n of e)typeof t[n]=="string"&&(this.scriptObject[n]=t[n])}get _legacyLayoutSpec(){return this.scriptObject.KV}get _layouts(){return this.scriptObject.KVKL}set _layouts(t){this.scriptObject.KVKL=t}get compilerVersion(){return new M(this.scriptObject.KVER)}get isMnemonic(){return!!this.scriptObject.KM}get definesPositionalOrMnemonic(){return typeof this.scriptObject.KM!="undefined"}get helpText(){return this.scriptObject.KH}get hasScript(){return!!this.scriptObject.KHF}embedScript(t){this.scriptObject.KHF(t)}get oskStyling(){return this.scriptObject.KCSS}get isCJK(){var t;return typeof this.scriptObject.KLC!="undefined"?t=this.scriptObject.KLC:typeof this.scriptObject.LanguageCode!="undefined"&&(t=this.scriptObject.LanguageCode),t=="cmn"||t=="jpn"||t=="kor"}get isRTL(){return!!this.scriptObject.KRTL}get modifierBitmask(){return this.scriptObject.KMBM||C.modifierBitmasks.NON_CHIRAL}get isChiral(){return!!(this.modifierBitmask&C.modifierBitmasks.IS_CHIRAL)}get desktopFont(){return this.scriptObject.KV?this.scriptObject.KV.F:null}get cacheTag(){let t=this.scriptObject._kmw;return t||(t=new gl,this.scriptObject._kmw=t),t}get explodedStores(){return this.cacheTag.stores}get emulatesAltGr(){if(!this.isChiral||this._legacyLayoutSpec==null)return!1;let t=this._legacyLayoutSpec.KLS;if(!t)return!1;var e=m.LCTRLFLAG|m.LALTFLAG,n=t[E.getLayerId(e)],i=t[E.getLayerId(m.K_SHIFTFLAG|e)];if(n!=null&&n!=t[E.getLayerId(m.RALTFLAG)]||i!=null&&i!=t[E.getLayerId(m.RALTFLAG|m.K_SHIFTFLAG)])return!1;var s=this.modifierBitmask;return(s&e)!=e||n==null&&i==null,!0}get usesSupplementaryPlaneChars(){let t=this.scriptObject;return t&&(t.KS&&t.KS==1||t.KN=="Hieroglyphic")}get version(){return this.scriptObject.KBVER||""}usesDesktopLayoutOnDevice(t){return this.scriptObject.KVKL?t.formFactor==V.FormFactor.Desktop:!0}notify(t,e,n){typeof this.scriptObject.KNS=="function"&&this.scriptObject.KNS(t,e,n)}findOrConstructLayout(t){if(this._layouts){if(this._layouts[t]!==void 0)return this._layouts[t];if(t==V.FormFactor.Phone&&this._layouts[V.FormFactor.Tablet])return this._layouts[V.FormFactor.Phone]=this._layouts[V.FormFactor.Tablet];if(t==V.FormFactor.Tablet&&this._layouts[V.FormFactor.Phone])return this._layouts[V.FormFactor.Tablet]=this._layouts[V.FormFactor.Phone]}let e=null;if(this._legacyLayoutSpec!=null&&this._legacyLayoutSpec.KLS)e=this._legacyLayoutSpec;else if(this._legacyLayoutSpec!=null&&this._legacyLayoutSpec.BK!=null){for(var n=this._legacyLayoutSpec.BK,i=0;i0){e=this._legacyLayoutSpec;break}}if(!e&&(this.helpText==""||t!=V.FormFactor.Desktop)&&(e={F:"Tahoma",BK:E.dfltText}),this._layouts||(this._layouts={}),e){let s=this._layouts[t]=E.buildDefaultLayout(e,this,t);return s.isDefault=!0,s}else return this._layouts[t]=null,null}layout(t){let e=this.findOrConstructLayout(t);if(e)if(this.layoutStates[t]==Ki.NOT_LOADED){let n=wt.polyfill(e,this,t);return this.layoutStates[t]=1,n}else return e;else return null}refreshLayouts(){let t=[V.FormFactor.Desktop,V.FormFactor.Phone,V.FormFactor.Tablet],e=this;t.forEach(function(n){e.layoutStates[n]=Ki.NOT_LOADED})}markLayoutCalibrated(t){this.layoutStates[t]!=Ki.NOT_LOADED&&(this.layoutStates[t]=2)}getLayoutState(t){return this.layoutStates[t]}constructNullKeyEvent(t,e){e=e||{K_CAPS:!1,K_NUMLOCK:!1,K_SCROLL:!1};let n=ee.constructNullKeyEvent(t);return this.setSyntheticEventDefaults(n,e),n}constructKeyEvent(t,e,n){let i=t.baseKeyEvent;i.device=e,this.isMnemonic&&i.setMnemonicCode(t.layer.indexOf("shift")!=-1,n.K_CAPS),this.setSyntheticEventDefaults(i,n);let l={K_CAPS:C.stateBitmasks.CAPS,K_NUMLOCK:C.stateBitmasks.NUM_LOCK,K_SCROLL:C.stateBitmasks.SCROLL_LOCK}[i.kName];return l&&(i.Lstates^=l,i.LmodifierChange=!0),i}setSyntheticEventDefaults(t,e){t.device.touchable||(t.Lstates=0,t.Lstates|=e.K_CAPS?m.CAPITALFLAG:m.NOTCAPITALFLAG,t.Lstates|=e.K_NUMLOCK?m.NUMLOCKFLAG:m.NOTNUMLOCKFLAG,t.Lstates|=e.K_SCROLL?m.SCROLLFLAG:m.NOTSCROLLFLAG),t.kName&&t.kName.substr(0,2)=="U_"&&(t.LisVirtualKey=!1),typeof t.Lcode=="undefined"&&(t.Lcode=this.getVKDictionaryCode(t.kName),t.Lcode||(t.Lcode=1)),(t.Lmodifiers&C.modifierBitmasks.ALT_GR_SIM)==C.modifierBitmasks.ALT_GR_SIM&&this.emulatesAltGr&&(t.Lmodifiers&=~C.modifierBitmasks.ALT_GR_SIM,t.Lmodifiers|=m.RALTFLAG)}getVKDictionaryCode(t){let e=this.scriptObject.VKDictionary||{};if(!this.scriptObject.VKDictionary){if(typeof this.scriptObject.KVKD=="string"){let s=this.scriptObject.KVKD.split(" ");for(var n=0;n(i.KEYBOARD="keyboard",i.LANGUAGE="language",i.LANGUAGE_KEYBOARD="languageKeyboard",i.BLANK="blank",i))(Fo||{}),Qe=Fo;function pl(a,t){if(a)return{family:a.family,path:t,files:a.filename||a.source}}o(pl,"internalizeFont");var nt=class nt{static get spacebarTextMode(){return typeof this.spacebarTextModeSrc=="string"?this.spacebarTextModeSrc:this.spacebarTextModeSrc()}static set spacebarTextMode(t){this.spacebarTextModeSrc=t}constructor(t,e){if(typeof t!="string")if(t.KI||t.KL||t.KLC||t.KFont||t.KOskFont){let n=t;this.KI=n.KI,this.KN=n.KN,this.KL=n.KL,this.KLC=n.KLC,this.KFont=n.KFont,this.KOskFont=n.KOskFont,this._displayName=n instanceof nt?n._displayName:n.displayName}else{let n=t;n.languages||(n.languages=n.language),this.KI=n.id,this.KN=n.name,this.KL=n.languages.name,this.KLC=n.languages.id,this.KFont=pl(n.languages.font,e),this.KOskFont=pl(n.languages.oskFont,e)}else this.KI=t,this.KLC=e}static fromMultilanguageAPIStub(t){let e=[];t.languages||(t.languages=t.language);for(let n of t.languages){let i={id:t.id,name:t.name,languages:n};e.push(new nt(i))}return e}get id(){return this.KI}get name(){return this.KN}get langId(){return this.KLC}get langName(){return this.KL}get displayName(){if(this._displayName)return this._displayName;let t=this.KN,e=this.KL;switch(nt.spacebarTextMode){case Qe.KEYBOARD:return t;case Qe.LANGUAGE:return e;case Qe.LANGUAGE_KEYBOARD:return t==e?e:e+" - "+t;case Qe.BLANK:return"";default:return t}}set displayName(t){this._displayName=t}get textFont(){return this.KFont}get oskFont(){return this.KOskFont}validateForOSK(){return this.KLC?this.displayName===void 0||nt.spacebarTextMode!=Qe.BLANK&&!this.displayName?new Error("A display name is missing for this keyboard and cannot be generated under current settings."):null:this.KI||this.KN?new Error(`No language code was specified for use with the ${this.KI||this.KN} keyboard`):new Error("No language code was specified for use with the corresponding keyboard")}validateForCustomKeyboard(){return!this.KI||!this.KN||!this.KL||!this.KLC?new Error("To use a custom keyboard, you must specify keyboard id, keyboard name, language and language code."):null}};o(nt,"KeyboardProperties"),nt.spacebarTextModeSrc=Qe.KEYBOARD;var Ye=nt;var mo=o(a=>a.substring(a.length-1)!="/"?a+"/":a,"addDelimiter"),Cl=class Cl{constructor(t,e){e=mo(e),this.sourcePath=e,this.protocol=e.replace(/(.{3,5}:)(.*)/,"$1"),this.updateFromOptions(t)}updateFromOptions(t){let e=this.sourcePath.replace(/(https?:\/\/)([^\/]*)(.*)/,"$1$2/");this._root=e,t.root!=""?this._root=this.fixPath(t.root):this._root=this.fixPath(e);let n=t.resources;n==""&&(n=this.sourcePath),this._resources=this.fixPath(n),this._keyboards=this.fixPath(t.keyboards),this._fonts=this.fixPath(t.fonts)}fixPath(t){return t.length==0||(t=mo(t),t.replace(/^(http)s?:.*/,"$1")=="http"||t.replace(/^(file):.*/,"$1")=="file")?t:t.substring(0,2)=="//"?this.protocol+t:t.substring(0,1)=="/"?this.root+t.substring(1):this.sourcePath+t}get fonts(){return this._fonts}updateFontPath(t){this._fonts=this.fixPath(t)}get root(){return this._root}get resources(){return this._resources}get keyboards(){return this._keyboards}};o(Cl,"PathConfiguration");var zt=Cl;var Gl={root:"",resources:"",keyboards:"",fonts:""};var Ql=class Ql{constructor(t,e){this.suggestions=t,this.transcriptionID=e}};o(Ql,"ReadySuggestions");var Ct=Ql;var Ul=class Ul extends S.default{constructor(e,n){super();this.initNewContext=!0;this._currentSuggestions=[];this.swallowPrediction=!1;this.doRevert=!1;this.recentRevert=!1;this.doTryAccept=o((e,n)=>{let i=this.recentAcceptCause;if(!i&&this.selected)this.accept(this.selected),n.shouldSwallow=!this.currentTarget.getTextAfterCaret(),this.recentAcceptCause="key";else if(i&&e=="space"){if(this.recentAcceptCause=null,i=="key"){n.shouldSwallow=!1;return}n.shouldSwallow=!!this.langProcessor.wordbreaksAfterSuggestions&&!this.currentTarget.getTextAfterCaret()}else n.shouldSwallow=!1},"doTryAccept");this.doTryRevert=o(()=>{this.doRevert?(this.doRevert=!1,this.recentAcceptCause=null):this.recentAcceptCause&&(this.showRevert(),this.swallowPrediction=!0)},"doTryRevert");this.invalidateSuggestions=o(e=>{this.initNewContext=!1,this.selected=null,(!this.swallowPrediction||e=="context")&&(this.recentAcceptCause=null,this.doRevert=!1,this.recentRevert=!1,e=="context"&&(this.swallowPrediction=!1,this.initNewContext=!0)),e!="new"&&this.clearSuggestions()},"invalidateSuggestions");this.updateSuggestions=o(e=>{let n=e.suggestions;this._currentSuggestions=n,this.selected=null,this.keepSuggestion=null;for(let i of n)i.tag=="keep"&&(this.keepSuggestion=i),i.autoAccept&&!this.selected&&(this.selected=i);this.keepSuggestion&&this._currentSuggestions.splice(this._currentSuggestions.indexOf(this.keepSuggestion),1),this.swallowPrediction?this.swallowPrediction=!1:(this.recentAcceptCause=null,this.doRevert=!1,this.recentRevert=!1),this.sendUpdateEvent()},"updateSuggestions");this.onModelStateChange=o(e=>{(e=="configured"||e=="inactive")&&this.resetContext()},"onModelStateChange");this.langProcessor=e,this.getLayerId=n;let i=o(()=>this.currentTarget&&e.state=="configured","validSuggestionState");this.suggestionApplier=s=>i()?e.applySuggestion(s,this.currentTarget,n):null,this.suggestionReverter=s=>W(this,null,function*(){if(i()){let l=yield e.applyReversion(s,this.currentTarget);this.swallowPrediction=!0,this.updateSuggestions(new Ct(l,s.id?-s.id:void 0))}}),this.connect()}get currentTarget(){return this._currentTarget}setCurrentTarget(e){let n=this._currentTarget;return this._currentTarget=e,n!=e?this.resetContext():Promise.resolve([])}connect(){this.langProcessor.addListener("invalidatesuggestions",this.invalidateSuggestions),this.langProcessor.addListener("suggestionsready",this.updateSuggestions),this.langProcessor.addListener("tryaccept",this.doTryAccept),this.langProcessor.addListener("tryrevert",this.doTryRevert),this.langProcessor.addListener("statechange",this.onModelStateChange)}disconnect(){this.langProcessor.removeListener("invalidatesuggestions",this.invalidateSuggestions),this.langProcessor.removeListener("suggestionsready",this.updateSuggestions),this.langProcessor.removeListener("tryaccept",this.doTryAccept),this.langProcessor.removeListener("tryrevert",this.doTryRevert),this.langProcessor.removeListener("statechange",this.onModelStateChange),this.clearSuggestions()}get currentSuggestions(){let e=[],n=this.activateKeep()&&this.keepSuggestion,i=this.selected&&this.keepSuggestion!=this.selected;return n&&(i||this.keepSuggestion.matchesModel)?e.push(this.keepSuggestion):this.doRevert&&e.push(this.revertSuggestion),e.concat(this._currentSuggestions)}acceptInternal(e){return e?e.tag=="revert"?(this.suggestionReverter(e),null):this.suggestionApplier(e):null}accept(e){let n=this;return this.selected=null,this.doRevert=!1,this.revertAcceptancePromise=this.acceptInternal(e),this.revertAcceptancePromise?(this.revertAcceptancePromise.then(function(i){i&&(n.revertSuggestion=i)}),this.recentAcceptCause="banner",this.recentRevert=!1,this.swallowPrediction=!0,this.revertAcceptancePromise):(e&&e.tag=="revert"&&(this.recentAcceptCause=null,this.recentRevert=!0),Promise.resolve(null))}showRevert(){this.doRevert=!0,this.sendUpdateEvent()}clearSuggestions(){this.updateSuggestions({suggestions:[],transcriptionID:0})}activateKeep(){return!this.recentAcceptCause&&!this.recentRevert&&!this.initNewContext}sendUpdateEvent(){this.emit("update",this.currentSuggestions)}resetContext(){let e=this.currentTarget;return e?this.langProcessor.invalidateContext(e,this.getLayerId()):Promise.resolve([])}};o(Ul,"PredictionContext");var Gt=Ul;var zi=class zi{constructor(t){t.OS==V.OperatingSystem.Android?this.popupCanvasBackgroundColor="#999":this.popupCanvasBackgroundColor=zi.prefersDarkMode()?"#0f1319":"#ffffff"}static prefersDarkMode(){return window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches}};o(zi,"StyleConstants");var Hn=zi;var xl=class xl{constructor(){this.touchable="ontouchstart"in window,this.OS="",this.formFactor="desktop",this.browser="",this.dyPortrait=0,this.dyLandscape=0,this.version="0",this.orientation=window.orientation}getDPI(){var t=document.createElement("DIV"),e=t.style,n=96;return document.readyState!=="complete"||(t.id="calculateDPI",e.position="absolute",e.display="block",e.visibility="hidden",e.left="10px",e.top="10px",e.width="1in",e.height="10px",document.body.appendChild(t),n=typeof window.devicePixelRatio=="undefined"?t.offsetWidth:t.offsetWidth*window.devicePixelRatio,document.body.removeChild(t)),n}detect(){var t=!1;if(navigator&&navigator.userAgent){var e=navigator.userAgent;if(e.indexOf("iPad")>=0)this.OS="iOS",this.formFactor="tablet",this.dyPortrait=this.dyLandscape=0;else if(e.indexOf("iPhone")>=0)this.OS="iOS",this.formFactor="phone",this.dyPortrait=this.dyLandscape=25;else if(e.indexOf("Android")>=0){this.OS="Android",this.formFactor="phone",this.dyPortrait=75,this.dyLandscape=25;try{var n=new RegExp("(?:Android\\s+)(\\d+\\.\\d+\\.\\d+)");this.version=e.match(n)[1]}catch(g){}}else if(e.indexOf("Linux")>=0)this.OS="Linux";else if(e.indexOf("Macintosh")>=0){let d=/Intel Mac OS X (\d+(?:[_\.]\d+)+)/i.exec(e);if(!d)console.warn("KMW could not properly parse the user agent string.A suboptimal keyboard layout may result."),this.OS="MacOSX";else if(d.length>1&&d[1]){let u=d[1].replace("_","."),I=new M(u);t=M.MAC_POSSIBLE_IPAD_ALIAS.compareTo(I)<=0,this.OS="MacOSX"}}else e.indexOf("Windows NT")>=0&&(this.OS="Windows",e.indexOf("Touch")>=0&&(this.formFactor="phone"),typeof navigator.msMaxTouchPoints=="number"&&navigator.msMaxTouchPoints>0&&(this.touchable=!0))}let i=Math.min(screen.width,screen.height),s=Math.max(screen.width,screen.height),l=i/s;this.OS!="iOS"&&this.formFactor=="phone"&&(i>=600&&l>.5625||l>=.625)&&(this.formFactor="tablet");let r=navigator.platform=="Win32"||navigator.platform=="MacIntel";this.OS=="iOS"&&!("ongesturestart"in window)&&!r&&(this.OS="Android"),this.browser="web",(this.OS=="iOS"||this.OS.toLowerCase()=="macosx")&&(this.browser="safari");var c=/Firefox|Chrome|OPR|Safari|Edge/;if(c.test(navigator.userAgent)&&(navigator.userAgent.indexOf("Firefox")>=0&&"onmozorientationchange"in screen?this.browser="firefox":navigator.userAgent.indexOf("OPR")>=0?this.browser="opera":navigator.userAgent.indexOf(" Edge/")>=0?this.browser="edge":navigator.userAgent.indexOf("Chrome")>=0?this.browser="chrome":navigator.userAgent.indexOf("Safari")>=0&&(this.browser="safari")),t&&this.browser=="safari"&&window.TouchEvent){this.OS="iOS",this.formFactor="tablet",this.dyPortrait=this.dyLandscape=0;let g=screen.height/screen.width;g<1&&(g=1/g),g>1.6&&(this.formFactor="phone",this.dyPortrait=this.dyLandscape=25)}return this.colorScheme=Hn.prefersDarkMode()?"dark":"light",this.coreSpec}get coreSpec(){return new V(this.browser,this.formFactor,this.OS,this.touchable)}};o(xl,"DeviceDetector");var Qt=xl;var Xl=class Xl extends S.default{constructor(e,n){super();this.applyCacheBusting=!1;if(!n){let i=new Qt;i.detect(),n=i.coreSpec}this.sourcePath=e,this.hostDevice=n,this.deferForInitialization=new f}initialize(e){this._paths?this._paths.updateFromOptions(e):this._paths=new zt(e,this.sourcePath),typeof e.setActiveOnRegister=="boolean"?this.activateFirstKeyboard=e.setActiveOnRegister:this.activateFirstKeyboard=!0,this._spacebarText=e.spacebarText,Ye.spacebarTextMode=()=>this.spacebarText}finalizeInit(){this.deferForInitialization.resolve()}get paths(){return this._paths}get spacebarText(){return this._spacebarText}set spacebarText(e){this._spacebarText!=e&&(this._spacebarText=e,this.emit("spacebartext",e))}get softDevice(){return this.hostDevice}get hardDevice(){return ll(this.hostDevice)}get stubNamespacer(){return this._stubNamespacer}set stubNamespacer(e){this._stubNamespacer=e}debugReport(){return{hostDevice:this.hostDevice,initialized:this.deferForInitialization.isResolved}}onRuleFinalization(e,n){}};o(Xl,"EngineConfiguration");var Jn=Xl,Zl=y({setActiveOnRegister:!0,spacebarText:Qe.LANGUAGE_KEYBOARD},Gl);var kn=class kn extends S.default{constructor(e){super();this.pendingActivations=[];this.engineConfig=e}get predictionContext(){return this._predictionContext}configure(e){this._resetContext=e.resetContext,this._predictionContext=e.predictionContext,this.keyboardCache=e.keyboardCache}insertText(e,n,i){let s=this.activeTarget;return s!=null?(n!=null&&e.output(0,s,n),typeof i!="undefined"&&i!==null&&e.deadkeyOutput(0,s,i),s.invalidateSelection(),!0):!1}resetContext(){this._resetContext(this.activeTarget),this.predictionContext.resetContext()}findAndPopActivation(e){let n;for(n=0;n{let g=new f;this.emit("keyboardasyncload",i,g.corePromise);let d=this.keyboardCache.fetchKeyboardForStub(i),u=new Promise((B,h)=>{let b=`Sorry, the ${i.name} keyboard for ${i.langName} is not currently available.`;window.setTimeout(()=>h(new Error(b)),kn.TIMEOUT_THRESHOLD)}),I=Promise.race([d,u]);return I.then(()=>{g.resolve(null),u.catch(()=>{})}),I.catch(B=>{throw g.resolve(B),B}),I});return{keyboard:this.deferredKeyboardActivation(r,i,this.currentKeyboardSrcTarget()).then(g=>W(this,null,function*(){return g?r:Promise.resolve(null)})),metadata:i}}}};o(kn,"ContextManagerBase"),kn.TIMEOUT_THRESHOLD=1e4;var Nn=kn;var Vl=class Vl extends S.default{};o(Vl,"HardKeyboard");var Dt=Vl;function Sl(a,t,e){if(t&&t.isMnemonic&&a.setMnemonicCode(!!(a.Lmodifiers&m.K_SHIFTFLAG),!!(a.Lmodifiers&m.CAPITALFLAG)),t&&!t.isMnemonic){var n=te.languageMap[e];n&&n["k"+a.Lcode]&&(a.Lcode=n["k"+a.Lcode]),!t.definesPositionalOrMnemonic&&!(a.Lmodifiers&C.modifierBitmasks.NON_LEGACY)&&!a.isModifier&&(a=new ee({Lcode:te._USKeyCodeToCharCode(a),Lmodifiers:0,LisVirtualKey:!1,vkCode:a.Lcode,Lstates:a.Lstates,kName:"",device:a.device,isSynthetic:!1}))}return a}o(Sl,"processForMnemonicsAndLegacy");function Al(a,t,e){let n=Math.min(a.length,t.length),i,s,l,r,c;for(e?(i=s=a.length-1,l=s-n,r=-1,c=t.length-a.length):(i=s=0,l=n,r=1,c=0);s!=l&&a.charAt(s)==t.charAt(s+c);s+=r);if(s!=i&&s!=l){let g=a.charCodeAt(s-r),d=a.charCodeAt(s),u=t.charCodeAt(s+c),I=e?Yi:Ei,B=e?Ei:Yi;if(I(g)&&(B(d)||B(u)))return s-r}return s}o(Al,"findCommonSubstringEndIndex");var Ut=class Ut{constructor(t,e){this.p=t,this.d=e,this.o=Ut.ordinalSeed++}match(t,e){var n=this.p==t&&this.d==e;return n}set(){this.matched=1}reset(){this.matched=0}before(t){return this.ot&&(n.p+=e)}equal(t){if(this.dks.length!=t.dks.length)return!1;let e=t.dks,n=[];for(let i of this.dks)if(!e.find(l=>i.equal(l)))return!1;return n.length==e.length}count(){return this.dks.length}};o(Oi,"DeadkeyTracker");var Di=Oi;fn();function xt(a){var t;return a?a.insert===""&&a.deleteLeft===0&&((t=a.deleteRight)!=null?t:0)===0:!0}o(xt,"isEmptyTransform");var Yn=class Yn{constructor(t,e,n,i){this.insert=t,this.deleteLeft=e,this.deleteRight=n,this.erasedSelection=i}};o(Yn,"TextTransform"),Yn.nil=new Yn("",0,0,!1);var Wl=Yn,Tn=class Tn{constructor(t,e,n,i){let s=this.token=Tn.tokenSeed++;this.keystroke=t,this.transform=e,this.alternates=i,this.preInput=n,this.transform.id=this.token,i&&i.forEach(function(l){l.sample.id=s})}};o(Tn,"Transcription"),Tn.tokenSeed=0;var fl=Tn,Ll=class Ll{constructor(){this._dks=new Di}get isSynthetic(){return!0}resetContext(){this.deadkeys().clear()}deadkeys(){return this._dks}hasDeadkeyMatch(t,e){return this.deadkeys().isMatch(this.getDeadkeyCaret(),t,e)}insertDeadkeyBeforeCaret(t){var e=new En(this.getDeadkeyCaret(),t);this.deadkeys().add(e)}adjustDeadkeys(t){this.deadkeys().adjustPositions(this.getDeadkeyCaret(),t)}setDeadkeys(t){this._dks=t.clone()}buildTransformFrom(t){let e=this.getTextBeforeCaret(),n=t.getTextBeforeCaret(),i=Al(n,e,!1),s=n.substring(i)._kmwLength(),l=e.substring(i),r=this.getTextAfterCaret(),c=t.getTextAfterCaret(),g=Al(c,r,!0),d=c.substring(0,g+1)._kmwLength();return new Wl(l,s,d,t.getSelectedText()&&!this.getSelectedText())}buildTranscriptionFrom(t,e,n,i){let s=this.buildTransformFrom(t);return new fl(e,s,k.from(t,n),i)}restoreTo(t){this.clearSelection(),this.setTextBeforeCaret(t.getTextBeforeCaret()),this.setTextAfterCaret(t.getTextAfterCaret()),this._dks=t._dks.clone()}apply(t){this.clearSelection(),t.deleteRight&&this.setTextAfterCaret(this.getTextAfterCaret()._kmwSubstr(t.deleteRight)),t.deleteLeft&&this.deleteCharsBeforeCaret(t.deleteLeft),t.insert&&this.insertTextBeforeCaret(t.insert),this._dks.clear()}setTextBeforeCaret(t){this.deleteCharsBeforeCaret(this.getTextBeforeCaret()._kmwLength()),this.insertTextBeforeCaret(t)}saveProperties(){}restoreProperties(){}};o(Ll,"OutputTarget");var it=Ll;var Ot=class Ot extends it{constructor(e,n,i){super();this.selForward=!0;this.text=e||"";var s=this.text._kmwLength();this.selStart=typeof n=="number"?n:s,this.selEnd=typeof i=="number"?i:this.selStart,this.selForward=this.selEnd>=this.selStart}static from(e,n){let i;if(e instanceof Ot){let s=e;i=new Ot(s.text,s.selStart,s.selEnd)}else{let s=e.getText(),l=s._kmwLength(),r=l,c=0;if(e.hasSelection()){let g=e.getTextBeforeCaret(),d=e.getTextAfterCaret();r=g._kmwLength(),c=l-d._kmwLength()}i=new Ot(s,r,c)}return i.setDeadkeys(e.deadkeys()),i}clearSelection(){this.text=this.getTextBeforeCaret()+this.getTextAfterCaret(),this.selEnd=this.selStart,this.selForward=!0}invalidateSelection(){}isSelectionEmpty(){return this.selStart==this.selEnd}hasSelection(){return!0}getDeadkeyCaret(){return this.selStart}setSelection(e,n){if(this.selStart=e,this.selEnd=typeof n=="number"?n:e,this.selForward=n>=e,!this.selForward){let i=this.selStart;this.selStart=this.selEnd,this.selEnd=i}}getTextBeforeCaret(){return this.text.kmwSubstr(0,this.selStart)}getSelectedText(){return this.text.kmwSubstr(this.selStart,this.selEnd-this.selStart)}getTextAfterCaret(){return this.text.kmwSubstr(this.selEnd)}getText(){return this.text}deleteCharsBeforeCaret(e){e>=0&&(e>this.selStart&&(e=this.selStart),this.adjustDeadkeys(-e),this.text=this.text.kmwSubstr(0,this.selStart-e)+this.text.kmwSubstr(this.selStart),this.selStart-=e,this.selEnd-=e)}insertTextBeforeCaret(e){this.adjustDeadkeys(e._kmwLength()),this.text=this.getTextBeforeCaret()+e+this.text.kmwSubstr(this.selStart),this.selStart+=e.kmwLength(),this.selEnd+=e.kmwLength()}handleNewlineAtCaret(){this.insertTextBeforeCaret(` +`)}setTextAfterCaret(e){this.text=this.getTextBeforeCaret()+e}isEqual(e){return this.text==e.text&&this.selStart==e.selStart&&this.selEnd==e.selEnd&&this.deadkeys().equal(e.deadkeys())}doInputEvent(){}};o(Ot,"Mock");var k=Ot;var Rl=class Rl{constructor(){this.transcription=null;this.setStore={};this.saveStore={};this.variableStores={};this.triggersDefaultCommand=!1}finalize(t,e,n){if(!this.transcription)throw"Cannot finalize a RuleBehavior with no transcription.";t.beepHandler&&this.beep&&t.beepHandler(e);for(let i in this.setStore){let s=t.keyboardInterface.systemStores[i];if(s)try{s.set(this.setStore[i])}catch(l){t.errorLogger&&t.errorLogger("Rule attempted to perform illegal operation - 'platform' may not be changed.")}else t.warningLogger&&t.warningLogger("Unknown store affected by keyboard rule: "+i)}if(t.keyboardInterface.applyVariableStores(this.variableStores),t.keyboardInterface.variableStoreSerializer)for(let i in this.saveStore)t.keyboardInterface.variableStoreSerializer.saveStore(t.activeKeyboard.id,i,this.saveStore[i]);if(this.triggersDefaultCommand){let i=this.transcription.keystroke;t.defaultRules.applyCommand(i,e)}t.warningLogger&&this.warningLog?t.warningLogger(this.warningLog):t.errorLogger&&this.errorLog&&t.errorLogger(this.errorLog)}mergeInDefaults(t){let e=this.transcription.keystroke,n=t.transcription.keystroke;if(e.Lcode!=n.Lcode||e.Lmodifiers!=n.Lmodifiers)throw"RuleBehavior default-merge not supported unless keystrokes are identical!";this.triggersDefaultCommand=this.triggersDefaultCommand||t.triggersDefaultCommand;let i=k.from(this.transcription.preInput,!1);i.apply(this.transcription.transform),i.apply(t.transcription.transform),this.transcription=i.buildTranscriptionFrom(this.transcription.preInput,e,!1,this.transcription.alternates)}};o(Rl,"RuleBehavior");var ie=Rl;var vl=class vl{constructor(t){this.id=t}set(t){throw new Error("System store with ID "+this.id+" may not be directly set.")}};o(vl,"SystemStore");var Pi=vl,Hl=class Hl extends Pi{constructor(e,n){super(e);this.handler=null;this._value=n}get value(){return this._value}matches(e){return this._value==e}set(e){this.handler&&this.handler(this,e)||(this._value=e)}};o(Hl,"MutableSystemStore");var Pt=Hl,Jl=class Jl extends Pi{constructor(e){super(31);this.kbdInterface=e}matches(e){var n,i,s=e.split(" ");let l=this.kbdInterface.activeDevice;for(n=0;n0&&I[0].p>c){I.splice(0,1);continue}else if(I.length>0&&I[0].p==c)s.deadContext[e-s.valContext.length-1]=I[0],s.valContext=[I[0].d].concat(s.valContext),I.splice(0,1);else{var g=this.context(++l,1,i);s.valContext=[g].concat(s.valContext)}}this.cachedContextEx.set(e,e,s)}var d=s;d.valContext=d.valContext.slice(0,n);for(var u=0;u255&&(l=e.vkCode),e.LisVirtualKey||l>255?((n&16384)==16384||l>255)&&(s=i==l&&(n&c)==d,s=s&&this.stateMatch(e,n&g)):n&16384||(s=l==i),s||this.activeTargetOutput.deadkeys().resetMatched(),s}stateMatch(e,n){return(n&e.Lstates)==n}keyInformation(e){var n=new kl;return n.vk=e.LisVirtualKey,n.code=e.Lcode,n.modifiers=e.Lmodifiers,n}deadkeyMatch(e,n,i){return n.hasDeadkeyMatch(e,i)}beep(e){this.resetContextCache(),this.ruleBehavior.beep=!0}_ExplodeStore(e){if(typeof e=="string"){let s=this.activeKeyboard.explodedStores;if(s[e])return s[e];for(var n=[],i=0;i=0}_Index(e,n){return e=this._ExplodeStore(e),this._AnyIndices[n-1]0){i=this._BuildExtendedContext(e,e,n);let r=0;for(var s=0;sc&&(e=c)}n.deadkeys().resetMatched(),this.output(e,n,"")}output(e,n,i){this.resetContextCache(),n.saveProperties(),n.clearSelection(),n.deadkeys().deleteMatched(),e>=0&&n.deleteCharsBeforeCaret(e),n.insertTextBeforeCaret(i),n.restoreProperties()}contextExOutput(e,n,i,s){this.resetContextCache(),e>=0&&this.output(e,n,"");let l=this.ruleContextEx.get(i,i),r=l.deadContext[s-1],c=l.valContext[s-1];if(r)n.insertDeadkeyBeforeCaret(r.d);else if(typeof c=="string")this.output(-1,n,c);else throw new Error("contextExOutput: should never be a numeric valContext with no corresponding deadContext")}deadkeyOutput(e,n,i){this.resetContextCache(),e>=0&&this.output(e,n,""),n.insertDeadkeyBeforeCaret(i)}ifStore(e,n,i){var s=!0;let l=this.systemStores[e];return l&&(s=l.matches(n)),s}setStore(e,n,i){return this.resetContextCache(),e==33&&this.activeDevice.touchable?(this.ruleBehavior.setStore[e]=n,!0):!1}loadStore(e,n,i){return this.resetContextCache(),this.variableStoreSerializer&&this.variableStoreSerializer.loadStore(e,n)[n]||i}saveStore(e,n){this.resetContextCache();var i=this.activeKeyboard;if(!i||typeof i.id=="undefined"||i.id=="")return!1;let s={};return s[e]=n,this.ruleBehavior?this.ruleBehavior.saveStore[e]=s:this.variableStoreSerializer.saveStore(this.activeKeyboard.id,e,s),!0}resetContextCache(){this.cachedContext.reset(),this.cachedContextEx.reset()}defaultBackspace(e){e.isSelectionEmpty()?this.output(1,e,""):this.output(0,e,"")}processNewContextEvent(e,n){if(!this.activeKeyboard)throw"No active keyboard for keystroke processing!";return this.process(this.activeKeyboard.processNewContextEvent.bind(this.activeKeyboard),e,n,!0)}processPostKeystroke(e,n){if(!this.activeKeyboard)throw"No active keyboard for keystroke processing!";return this.process(this.activeKeyboard.processPostKeystroke.bind(this.activeKeyboard),e,n,!0)}processKeystroke(e,n){if(!this.activeKeyboard)throw"No active keyboard for keystroke processing!";return this.process(this.activeKeyboard.process.bind(this.activeKeyboard),e,n,!1)}process(e,n,i,s){if(n)if(this.activeKeyboard){if(!e)throw"No callee for keystroke processing!"}else throw"No active keyboard for keystroke processing!";else throw"No target specified for keyboard output!";n.invalidateSelection(),n.deadkeys().resetMatched(),this.resetContextCache();let l=k.from(n,!0),r=this.activeKeyboard.variableStores;this.ruleBehavior=new ie,this.activeDevice=i.device,this.activeTargetOutput=n;var c=e(n,i);this.activeTargetOutput=null,this.ruleBehavior.transcription=n.buildTranscriptionFrom(l,i,s),this.ruleBehavior.variableStores=this.activeKeyboard.variableStores,this.activeKeyboard.variableStores=r,this.ruleBehavior.triggerKeyDefault=!c;let g=this.ruleBehavior;return this.ruleBehavior=null,g}applyVariableStores(e){this.activeKeyboard.variableStores=e}static __publishShorthandAPI(){let e=this.prototype;var n=o(function(i,s){e[s]&&(e[i]=e[s])},"exportKBCallback");n("KSF","saveFocus"),n("KBR","beepReset"),n("KT","insertText"),n("KR","registerKeyboard"),n("KRS","registerStub"),n("KC","context"),n("KN","nul"),n("KCM","contextMatch"),n("KFCM","fullContextMatch"),n("KIK","isKeypress"),n("KKM","keyMatch"),n("KSM","stateMatch"),n("KKI","keyInformation"),n("KDM","deadkeyMatch"),n("KB","beep"),n("KA","any"),n("KDC","deleteContext"),n("KO","output"),n("KDO","deadkeyOutput"),n("KCXO","contextExOutput"),n("KIO","indexOutput"),n("KIFS","ifStore"),n("KSETS","setStore"),n("KLOAD","loadStore"),n("KSAVE","saveStore")}};o(wn,"KeyboardInterface"),wn.GLOBAL_NAME="KeymanWeb";var Te=wn;(function(){Te.__publishShorthandAPI()})();var Zt=class Zt extends S.default{constructor(e,n){super();this.stateKeys={K_CAPS:!1,K_NUMLOCK:!1,K_SCROLL:!1};this.modStateFlags=0;n||(n=Zt.DEFAULT_OPTIONS),this.contextDevice=e,this.baseLayout=n.baseLayout||Zt.DEFAULT_OPTIONS.baseLayout,this.keyboardInterface=n.keyboardInterface||new Te(Et(),Rn),this.defaultRules=n.defaultOutputRules||Zt.DEFAULT_OPTIONS.defaultOutputRules}get activeKeyboard(){return this.keyboardInterface.activeKeyboard}set activeKeyboard(e){this.keyboardInterface.activeKeyboard=e,this.resetContext()}get layerStore(){return this.keyboardInterface.systemStores[33]}get newLayerStore(){return this.keyboardInterface.systemStores[42]}get oldLayerStore(){return this.keyboardInterface.systemStores[43]}get layerId(){return this.layerStore.value}set layerId(e){this.layerStore.set(e)}defaultRuleBehavior(e,n,i){let s=k.from(n,i),l=new ie,r=!1;var c="",g;if(e.isSynthetic||n.isSynthetic)if(r=!0,this.defaultRules.isCommand(e))l.triggersDefaultCommand=!0;else if((g=this.defaultRules.forSpecialEmulation(e))!=null)switch(g){case"\b":this.keyboardInterface.defaultBackspace(n);break;case` +`:n.handleNewlineAtCaret();break;default:l.errorLog="Unexpected 'special emulation' character (\\u"+g.kmwCharCodeAt(0).toString(16)+") went unhandled!"}else r=!1;let d=this.activeKeyboard&&this.activeKeyboard.isMnemonic;if(!r)if((c=this.defaultRules.forAny(e,d,l))!=null)if(g=this.defaultRules.forSpecialEmulation(e),g=="\b")this.keyboardInterface.defaultBackspace(n);else{if(g||this.defaultRules.isCommand(e))return null;this.keyboardInterface.output(0,n,c)}else return null;if(l.errorLog)return l;let u=n.buildTranscriptionFrom(s,e,i);return l.transcription=u,l}processNewContextEvent(e,n){return this.activeKeyboard?this.keyboardInterface.processNewContextEvent(n,this.activeKeyboard.constructNullKeyEvent(e,this.stateKeys)):null}processPostKeystroke(e,n){return this.activeKeyboard?this.keyboardInterface.processPostKeystroke(n,this.activeKeyboard.constructNullKeyEvent(e,this.stateKeys)):null}processKeystroke(e,n){var i;let s=n.getTextBeforeCaret().kmwLength()==0&&n.isSelectionEmpty();if(this.activeKeyboard&&e.Lcode!=0&&(i=this.keyboardInterface.processKeystroke(n,e)),s&&e.Lcode==C.keyCodes.K_BKSP&&i.triggerKeyDefault)i=this.defaultRuleBehavior(e,n,!1),i.triggerKeyDefault=!0,i.transcription.transform.deleteLeft=1;else if(!i||i.triggerKeyDefault){e.Lcode=e.vkCode||e.Lcode,this.keyboardInterface.activeTargetOutput=n;let l=this.defaultRuleBehavior(e,n,!1);l&&(i?i.mergeInDefaults(l):i=l,i.triggerKeyDefault=!1),this.keyboardInterface.activeTargetOutput=null}return i}_UpdateVKShift(e){let n=0,i=["CAPS","NUM_LOCK","SCROLL_LOCK"],s=["K_CAPS","K_NUMLOCK","K_SCROLL"],l=[m.CAPITALFLAG,m.NUMLOCKFLAG,m.SCROLLFLAG];if(!this.activeKeyboard)return!0;if(e){n=e.Lmodifiers,this.activeKeyboard.isChiral&&this.activeKeyboard.emulatesAltGr&&(this.modStateFlags&C.modifierBitmasks.ALT_GR_SIM)==C.modifierBitmasks.ALT_GR_SIM&&(n|=C.modifierBitmasks.ALT_GR_SIM,n&=~m.RALTFLAG);let r=!1;for(let c=0;c{l.scriptObject.KI=e,this.addKeyboard(l)}).catch(l=>{throw delete this.keyboardTable[e],l}),s}addStub(e){var s;let n=se(e.KI),i=this.stubSetTable[n]=(s=this.stubSetTable[n])!=null?s:{};i[e.KLC]=e,this.emit("stubadded",e)}findMatchingStub(e){return this.getStub(e.KI,e.KLC)}getStub(e,n){var r;let i,s=n||"---";e instanceof Y?i=e.id:i=e,i&&(i=se(i));let l=(r=this.stubSetTable[i])!=null?r:{};if(s!="---")return l[s];{let c=Object.keys(l);return c.length==0?null:l[c[0]]}}forgetKeyboard(e,n=!1){let i=e instanceof Y?e.id:se(e);this.stubSetTable[i]&&delete this.stubSetTable[i],n&&this.keyboardTable[i]&&delete this.keyboardTable[i]}getStubList(){let e=[],n=Object.keys(this.stubSetTable);for(let i of n){let s=this.stubSetTable[i],l=Object.keys(s);for(let r of l)e.push(s[r])}return e}};o(wl,"StubAndKeyboardCache");var _t=wl;var Ml=["World","Africa","Asia","Europe","South America","North America","Oceania","Central America","Middle East"],Kl=["un","af","as","eu","sa","na","oc","ca","me"],xa=RegExp("^(([\\.]/)|([\\.][\\.]/)|(/))|(:)");function po(a,t){return t=t||"",a&&!xa.test(a)?t+a:a}o(po,"configureFilePathing");var $i=class $i extends Ye{constructor(e,n,i){var t=(...args)=>{super(...args)};if(typeof e!="string")if(e.id!==void 0){let s=e;s.id=se(s.id),t(s,i),this.KF=po(s.filename,n),this.mapRegion(s.languages)}else{let s=e;s.KI=se(s.KI),t(s,i),this.KF=po(s.KF,n),this.KP=s.KP,this.KR=s.KR,this.KRC=s.KRC;return}else t(se(e),n)}mapRegion(e){let n=e.region,i=0;if(typeof n=="number")n<1||n>9?i=0:i=n-1;else if(typeof n=="string"){let s=n.length==2?Kl:Ml;for(let l=0;l{let g=A(y({},e),{languages:c,language:void 0}),d=new $i(g,n,i);r.push(d)}),r}merge(e){this.KL||(this.KL=e.KL),this.KR||(this.KR=e.KR),this.KRC||(this.KRC=e.KRC),this.KN||(this.KN=e.KN),this.KF||(this.KF=e.KF),this.KFont||(this.KFont=e.KFont),this.KOskFont||(this.KOskFont=e.KOskFont),e._displayName&&(this._displayName||(this._displayName=e._displayName))}validateForCustomKeyboard(){return super.validateForCustomKeyboard()||!this.KF||!this.KR?new Error("To use a custom keyboard, you must specify file name, keyboard id, keyboard name, language, language code, and region."):null}};o($i,"KeyboardStub");var $=$i;function Mn(a,t){if(t.length==0)return Promise.resolve(a);if(a.length==0)return Promise.reject(t);{let e=a;return Promise.resolve(e.concat(t))}}o(Mn,"mergeAndResolveStubPromises");var Co="The Cloud API request timed out.",zl="Could not find a keyboard with that ID.",Go="The Cloud API failed to find an appropriate keyboard.",Za="Error occurred while registering keyboards: ",Xa=o(function(a){return a+" keyboard not found."},"MISSING_KEYBOARD"),Kn=class Kn extends S.default{constructor(e,n){super();this.cloudResolutionPromises=new Map;this.languageFetchStarted=!1;this.registerFromCloud=o(e=>{let n=Number.parseInt(e.timerid),i;try{i=this._registerCore(e)}catch(s){i=new Error(Za+s)}if(n){let s=this.cloudResolutionPromises.get(n);if(s)try{i instanceof Error?s.reject(i):s.resolve(i)}finally{this.cloudResolutionPromises.delete(n)}else{this.emit("unboundregister",i);return}}else{this.emit("unboundregister",i);return}},"registerFromCloud");this.requestEngine=e,this.pathConfig=n,this._languageListPromise=new f}get languageListPromise(){return this.languageFetchStarted||(this.languageFetchStarted=!0,this.keymanCloudRequest("",!0).catch(e=>{this.languageFetchStarted=!1,this._languageListPromise.reject(e),this._languageListPromise=new f})),this._languageListPromise.corePromise}keymanCloudRequest(e,n){let i="https://api.keyman.com/cloud/4.0/"+(arguments.length>1&&n?"languages":"keyboards"),s="?jsonp=keyman.register&languageidtype=bcp47&version="+M.CURRENT.toString(),l=i+s+e,{promise:r,queryId:c}=this.requestEngine.request(l);return this.cloudResolutionPromises.set(c,r),r.finally(()=>{this.cloudResolutionPromises.delete(c)}),r.corePromise}_registerCore(e){let n=e.options,i=n.fontBaseUri;if(this.pathConfig.fonts!=""?i=this.pathConfig.fonts:this.pathConfig.updateFontPath(i),typeof e.error=="string"){var s="";if(typeof e.options.keyboardid=="string"){let r=e.options.keyboardid;s=r.substring(0,1).toUpperCase()+r.substring(1)}return new Error(Xa(s))}if(typeof n=="undefined"||typeof n.context=="undefined")return new Error(zl);let l=[];if(n.context=="keyboard"){let r,c=e.keyboard;if(Array.isArray(c))for(r=0;rg.KLC==c)])}}fetchCloudStubs(e){return W(this,null,function*(){if(e.length==0)return Promise.resolve([]);let n="&keyboardid=",i="";for(let l=0;l{Array.isArray(i)&&i.forEach(s=>{s instanceof $&&this.cache.addStub(s)})})}addKeyboardArray(t){let e=[],n=[],i=[],s=[],l=[];for(let g of t)if(typeof g=="string")g.length>0&&s.push(g);else if(g.KI||g.KL||g.KLC||g.KFont||g.KOskFont)i.push(new $(g));else{let d=g;if(typeof d.language!="undefined"&&console.warn("The 'language' property for keyboard stubs has been deprecated. Please use the 'languages' property instead."),d.languages||(d.languages=d.language),typeof d.languages=="undefined"){let u="To use keyboard '"+d.id+"', you must specify languages.";l.push(Dl(d,u))}else if(Array.isArray(d.languages)){let u=$.toStubs(d,this.pathConfig.keyboards,this.pathConfig.fonts);for(let I of u)I instanceof $?i.push(I):l.push(I)}else{let u=d;i.push(new $(u,this.pathConfig.keyboards,this.pathConfig.fonts))}}for(let g of i)if(g.KF){let d=g.validateForCustomKeyboard();d?l.push(Dl(g,d)):e.push(g)}else n.push(g);let r=[];for(let g of n){if(!g.KI&&!g.KLC){l.push(Dl(g,"Cannot fetch keyboard information without a keyboard ID or language code."));continue}let d=Qo(we(g.id),g.langId);Uo(this.cache,r,d)&&r.push(d)}for(let g of s){let d=g.split("@"),u=[""];d[0].toLowerCase()=="english"&&(d[0]="us"),d.length>1&&(u=d[1].split(","));for(let I=0;I0&&u[I]=="")continue;let B=Qo(d[0],u[I],d[2]);Uo(this.cache,r,B)&&r.push(B)}}return e.forEach(g=>this.cache.addStub(g)),this.cloudQueryEngine.fetchCloudStubs(r.map(g=>g.toString())).then(g=>{for(let d of g)d instanceof $?(this.cache.addStub(d),e.push(d)):l.push(d);return[].concat(l).concat(e)})}addLanguageKeyboards(t){return W(this,null,function*(){let e=[],n=[];try{n=yield this.cloudQueryEngine.languageListPromise}catch(l){return console.error(l),e.push({error:l}),e}let i=n,s="";for(let l=0;lW(this,null,function*(){let r=yield Mn(l,e);for(let c of r)typeof c.error=="undefined"&&this.cache.addStub(c);return r}),l=>{console.error(l);let r={error:l};return e.push(r),Promise.reject(e)})})}fetchCloudCatalog(){return W(this,null,function*(){try{let t=yield this.cloudQueryEngine.keymanCloudRequest("",!1);return t.forEach(e=>this.cache.addStub(e)),t}catch(t){return Promise.reject([{error:t}])}})}alertLanguageUnavailable(t){return"No keyboards are available for "+t+". Does it have another language name?"}};o(jl,"KeyboardRequisitioner");var $t=jl;var _l=class _l{constructor(){this.registeredModels={};this.languageModelMap={}}modelForLanguage(t){return this.languageModelMap[t]}register(t){if(t.id=t.id.toLowerCase(),JSON.stringify(t)==JSON.stringify(this.registeredModels[t.id]))return;this.registeredModels[t.id]=t;let e=this;t.languages.forEach(function(n){if(!n){console.warn("Null / undefined language codes are not permitted for registration.");return}e.languageModelMap[n]=t})}unregister(t){let e;if(t=t.toLowerCase(),this.registeredModels[t])e=this.registeredModels[t],delete this.registeredModels[t];else return null;let n=this;return e.languages.forEach(function(i){n.languageModelMap[i].id==t&&delete n.languageModelMap[i]}),e}isRegistered(t){return!!this.registeredModels[t.id.toLowerCase()]}};o(_l,"ModelManager");var en=_l;function es(a){let t=[];for(let e=0;e0?t=n[0]:t=document.body}this.linkNode=t,this.doCacheBusting=e||!1}get sheets(){return this.linkedSheets.map(t=>t.sheet)}linkStylesheet(t){if(!(t instanceof HTMLLinkElement)&&!t.innerHTML)return;let e=new f;t instanceof HTMLLinkElement?t.onload=()=>e.resolve():e.resolve(),this.linkedSheets.push({sheet:t,load:e}),this.linkNode.appendChild(t)}allLoadedPromise(){return W(this,null,function*(){let t=this.linkedSheets.map(e=>e.load.corePromise);Promise.allSettled?yield Promise.allSettled(t):yield Promise.all(t)})}addStyleSheetForFont(t,e,n){if(!t||typeof t.files=="undefined")return null;let i=t.family,s,l,r="",c="",g=[],d="";n||(n=V.OperatingSystem.Other);let u=this.fontStyleDefinitions[n]=this.fontStyleDefinitions[n]||{};if(u[i]){let G=u[i];return G.parentNode||this.linkStylesheet(G),null}for(typeof t.files=="string"?g[0]=t.files:g=t.files,l=0;l0&&(r=g[l]),g[l].toLowerCase().indexOf(".ttf")>0&&(r=g[l]),g[l].toLowerCase().indexOf(".woff")>0&&(c=g[l]);r!=""&&r.indexOf("/")<0&&(r=e+r),c!=""&&c.indexOf("/")<0&&(c=e+c);var I=`@font-face { +font-family:`+t.family+`; +font-style:normal; +font-weight:normal; +`;if(d){let U=d.substring(10,d.indexOf(";",10));I+=`src:url('${d}'), format('${U}');`}else n==V.OperatingSystem.iOS?r!=""&&(this.doCacheBusting&&(r=this.cacheBust(r)),s="url('"+encodeURI(r)+"') format('truetype')"):(c!=""&&(s="url('"+encodeURI(c)+"') format('woff')"),r!=""&&(s="url('"+encodeURI(r)+"') format('truetype')"));if(!s)return null;I+="src:"+s+";",I=I+` +} +`;let B=st(I);u[i]=B;let b=new FontFace(t.family,s).load(),F=o(()=>{this.fontPromises=this.fontPromises.filter(G=>G!=b)},"clearPromise");return this.fontPromises.push(b.then(F).catch(F)),this.linkStylesheet(B),B}cacheBust(t){return t+"?v="+new Date().getTime()}linkExternalSheet(t,e){try{if(!e&&document.querySelector("link[href="+JSON.stringify(t)+"]")!=null)return null}catch(i){return null}let n=document.createElement("link");return n.type="text/css",n.rel="stylesheet",n.href=t,this.linkStylesheet(n),n}unlink(t){let e=this.linkedSheets.findIndex(n=>n.sheet==t);return e>-1?(this.linkedSheets.splice(e,1)[0].load.resolve(),t.parentNode.removeChild(t),!0):!1}unlinkAll(){for(let t of this.linkedSheets){let e=t.sheet;e.parentNode&&e.parentNode.removeChild(e),t.load.resolve()}this.linkedSheets.splice(0,this.linkedSheets.length)}};o(ql,"StylesheetManager");var ge=ql;function st(a){var t=document.createElement("style");return t.type="text/css",t.appendChild(document.createTextNode(a)),t}o(st,"createStyleSheet");function be(){var a;return typeof window.orientation!="undefined"?a=window.orientation:typeof window.screen.orientation!="undefined"&&(a=window.screen.orientation.angle),a!==void 0?Math.abs(a/90)==1:!1}o(be,"landscapeView");var $l=class $l{constructor(t){this.name=t}load(t){return this.loadCookie(this.name,t||(e=>e))}save(t,e){this.saveCookie(this.name,t,e||(n=>n))}_loadRawCookies(){let t={};if(typeof document.cookie!="undefined"&&document.cookie!=""){let e=document.cookie.split(/;\s*/);for(let n=0;n1){let[g,d]=c;n[g]=e(d,g)}else n[c[0]]=""}}return n}saveCookie(t,e,n){let i="";for(let r in e)i+=r+"="+n(e[r],r)+";";let l=" path=/; expires="+new Date(new Date().valueOf()+1e3*60*60*24*30).toUTCString();document.cookie=`${t}=${encodeURIComponent(i)}; ${l}`}};o($l,"CookieSerializer");var le=$l;function K(a){var t;if(!a)return 0;var e=a.offsetLeft?a.offsetLeft:0;if(t=a,t.offsetParent){for(;t.offsetParent;)t=t.offsetParent,e+=t.offsetLeft;let i=t.ownerDocument;t.style.position=="fixed"&&i&&i.scrollingElement&&(e+=i.scrollingElement.scrollLeft)}if(t&&t.ownerDocument&&a.ownerDocument!=window.document){var n=t.ownerDocument;if(n&&n.defaultView&&n.defaultView.frameElement)return e+K(n.defaultView.frameElement)-n.documentElement.scrollLeft}return e}o(K,"getAbsoluteX");function z(a){var t;if(!a)return 0;var e=a.offsetTop?a.offsetTop:0;if(t=a,t.ownerDocument&&t instanceof t.ownerDocument.defaultView.HTMLElement){for(;t.offsetParent;)t=t.offsetParent,e+=t.offsetTop;let i=t.ownerDocument;t.style.position=="fixed"&&i&&i.scrollingElement&&(e+=i.scrollingElement.scrollTop)}if(t&&t.ownerDocument&&a.ownerDocument!=window.document){var n=t.ownerDocument;if(n&&n.defaultView&&n.defaultView.frameElement)return e+z(n.defaultView.frameElement)}return e}o(z,"getAbsoluteY");var er=class er extends le{constructor(t,e){super(`KeymanWeb_${t}_Option_${e}`)}load(){return super.load(decodeURIComponent)}save(t){super.save(t,encodeURIComponent)}};o(er,"VarStoreSerializer");var ts=er,tr=class tr{loadStore(t,e){return new ts(t,e).load()}saveStore(t,e,n){new ts(t,e).save(n)}};o(tr,"VariableStoreCookieSerializer");var zn=tr;var nr=class nr extends Te{constructor(e,n,i){super(e,n,new zn);this.insertText=o((e,n)=>{this.resetContextCache(),this.engine.contextManager.insertText(this,e,n)},"insertText");this.KT=this.insertText;this.engine=n,this.stubNamespacer=i}preserveID(e){var n;if(document.currentScript)n=document.currentScript.id;else{var i=document.getElementsByTagName("script"),s=i[i.length-1];n=s.id}if(n)n.indexOf(we(e.KI))!=-1?e.KI=n:console.error("Error when registering keyboard: current SCRIPT tag's ID does not match!");else return}registerKeyboard(e){super.registerKeyboard(e);let n=this.loadedKeyboard;this.preserveID(e),this.engine.config.deferForInitialization.then(()=>{this.engine.keyboardRequisitioner.cache.isFetchingKeyboard(n.id)||(this.engine.keyboardRequisitioner.cache.addKeyboard(n),this.loadedKeyboard=null)})}registerStub(e){var i;this.stubNamespacer&&this.stubNamespacer(e);let n=o(()=>{let s=this.engine.config.paths;return new $(e,s.keyboards,s.fonts)},"buildStub");if(!this.engine.config.deferForInitialization.isResolved)this.engine.config.deferForInitialization.then(()=>this.engine.keyboardRequisitioner.cache.addStub(n()));else{let s=n();if((i=this.engine.keyboardRequisitioner)!=null&&i.cache.findMatchingStub(s))return 1;this.engine.keyboardRequisitioner.cache.addStub(s)}return null}};o(nr,"KeyboardInterface");var Xt=nr;(function(){Xt.__publishShorthandAPI()})();var ir=class ir extends Kt{constructor(e,n){var t=(...args)=>{super(...args)};e&&e._jsGlobal!=window&&(e._jsGlobal.String=window.String),t(e||new yt(window,Rn)),this.performCacheBusting=n||!1}loadKeyboardInternal(e,n,i){let s=new f;this.performCacheBusting&&(e=this.cacheBust(e));try{let l=this.harness._jsGlobal.document,r=l.createElement("script");i&&(r.id=i),l.head.appendChild(r),r.onerror=c=>{s.reject(n.missingError(c))},r.onload=()=>{if(this.harness.loadedKeyboard){let c=this.harness.loadedKeyboard;this.harness.loadedKeyboard=null,s.resolve(c)}else s.reject(n.scriptError())},s.then(()=>{r.remove()}).catch(()=>{r.remove()}),r.src=e}catch(l){return Promise.reject(l)}return s.corePromise}cacheBust(e){return e+"?v="+new Date().getTime()}};o(ir,"DOMKeyboardLoader");var ns=ir;var is=class is{constructor(t,e,n){this.left=t.getTextBeforeCaret(),this.startOfBuffer=this.left._kmwLength()<=e.leftContextCodePoints,this.startOfBuffer||(this.left=this.left._kmwSubstr(-e.leftContextCodePoints)),this.right=t.getTextAfterCaret(),this.endOfBuffer=this.right._kmwLength()<=e.rightContextCodePoints,this.endOfBuffer||(this.right=this.right._kmwSubstr(0,e.rightContextCodePoints)),this.casingForm=n=="shift"?"initial":n=="caps"?"upper":null}toMock(){let t=this.left._kmwLength();return new k(this.left+(this.right||""),t)}};o(is,"ContextWindow"),is.ENGINE_RULE_WINDOW={leftContextCodePoints:64,rightContextCodePoints:32};var Ue=is;var sr=class sr{constructor(){this._promises=new Map}get length(){return this._promises.size}make(t,e,n){if(this._promises.has(t))return n(`Existing request with token ${t}`);this._promises.set(t,{reject:n,resolve:e})}keep(t,e){let n=this._promises.get(t);if(!n)throw new Error(`No promise associated with token: ${t}`);let i=n.resolve;return this._promises.delete(t),i(e)}break(t,e){let n=this._promises.get(t);if(!n)throw new Error(`No promise associated with token: ${t}`);this._promises.delete(t),n.reject(e)}};o(sr,"PromiseStore");var lt=sr;var lr=class lr{constructor(t,e,n){this._worker=e,this._worker.onmessage=this.onMessage.bind(this),this._declareLMLayerReady=null,this._predictPromises=new lt,this._wordbreakPromises=new lt,this._acceptPromises=new lt,this._revertPromises=new lt,this._nextToken=Number.MIN_SAFE_INTEGER,this.sendConfig(t,!!n)}sendConfig(t,e){this._worker.postMessage({message:"config",capabilities:t,testMode:e})}loadModel(t,e="file"){return new Promise((n,i)=>{this._declareLMLayerReady=n;let s={type:e};e=="file"?s.file=t:s.code=t,this._worker.postMessage({message:"load",source:s})})}unloadModel(){this._worker.postMessage({message:"unload"})}predict(t,e){let n=this._nextToken++;return new Promise((i,s)=>{this._predictPromises.make(n,i,s),this._worker.postMessage({message:"predict",token:n,transform:t,context:e})})}wordbreak(t){let e=this._nextToken++;return new Promise((n,i)=>{this._wordbreakPromises.make(e,n,i),this._worker.postMessage({message:"wordbreak",token:e,context:t})})}acceptSuggestion(t,e,n){let i=this._nextToken++;return new Promise((s,l)=>{this._acceptPromises.make(i,s,l),this._worker.postMessage({message:"accept",token:i,suggestion:t,context:e,postTransform:n})})}revertSuggestion(t,e){let n=this._nextToken++;return new Promise((i,s)=>{this._revertPromises.make(n,i,s),this._worker.postMessage({message:"revert",token:n,reversion:t,context:e})})}resetContext(t){this._worker.postMessage({message:"reset-context",context:t})}onMessage(t){let e=t.data;if(e.message==="error")console.error(e.log),e.error&&console.error(e.error);else if(e.message==="ready")this._declareLMLayerReady(t.data.configuration);else if(e.message==="suggestions")this._predictPromises.keep(e.token,e.suggestions);else if(e.message==="currentword")this._wordbreakPromises.keep(e.token,e.word);else if(e.message==="postaccept")this._acceptPromises.keep(e.token,e.reversion);else if(e.message==="postrevert")this._revertPromises.keep(e.token,e.suggestions);else throw new Error(`Message not implemented: ${e.message}`)}shutdown(){this._worker.terminate()}};o(lr,"LMLayer");var tn=lr;function ss(a){return a}o(ss,"unwrap");var xo=`"use strict";(()=>{var Oe=Object.defineProperty,Kt=Object.defineProperties;var Qt=Object.getOwnPropertyDescriptors;var ft=Object.getOwnPropertySymbols;var Ht=Object.prototype.hasOwnProperty,Vt=Object.prototype.propertyIsEnumerable;var gt=(o,e)=>{if(e=Symbol[o])return e;throw Error("Symbol."+o+" is not defined")};var Tt=(o,e,t)=>e in o?Oe(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t,A=(o,e)=>{for(var t in e||(e={}))Ht.call(e,t)&&Tt(o,t,e[t]);if(ft)for(var t of ft(e))Vt.call(e,t)&&Tt(o,t,e[t]);return o},Re=(o,e)=>Kt(o,Qt(e)),h=(o,e)=>Oe(o,"name",{value:e,configurable:!0});var _e=(o,e)=>{for(var t in e)Oe(o,t,{get:e[t],enumerable:!0})};var D=(o,e,t)=>new Promise((r,n)=>{var i=a=>{try{l(t.next(a))}catch(u){n(u)}},s=a=>{try{l(t.throw(a))}catch(u){n(u)}},l=a=>a.done?r(a.value):Promise.resolve(a.value).then(i,s);l((t=t.apply(o,e)).next())}),be=function(o,e){this[0]=o,this[1]=e},Ct=(o,e,t)=>{var r=(s,l,a,u)=>{try{var c=t[s](l),p=(l=c.value)instanceof be,f=c.done;Promise.resolve(p?l[0]:l).then(d=>p?r(s==="return"?s:"next",l[1]?{done:d.done,value:d.value}:d,a,u):a({value:d,done:f})).catch(d=>r("throw",d,a,u))}catch(d){u(d)}},n=s=>i[s]=l=>new Promise((a,u)=>r(s,l,a,u)),i={};return t=t.apply(o,e),i[Symbol.asyncIterator]=()=>i,n("next"),n("throw"),n("return"),i};var St=(o,e,t)=>(e=o[gt("asyncIterator")])?e.call(o):(o=o[gt("iterator")](),e={},t=(r,n)=>(n=o[r])&&(e[r]=i=>new Promise((s,l,a)=>(i=n.call(o,i),a=i.done,Promise.resolve(i.value).then(u=>s({value:u,done:a}),l)))),t("next"),t("return"),e);function O(){String.kmwFromCharCode=function(o){var e=[],t;for(t=0;t1114111||Math.floor(r)!==r)throw new RangeError("Invalid code point "+r);r<65536?e.push(r):(r-=65536,e.push((r>>10)+55296),e.push(r%1024+56320))}return String.fromCharCode.apply(void 0,e)},String.prototype.kmwCharCodeAt=function(o){var e=String(this),t=0;if(o<0||o>=e.length)return NaN;for(var r=0;r=55296&&n<=56319&&e.length>t+1){var i=e.charCodeAt(t+1);if(i>=56320&&i<=57343)return(n-55296<<10)+(i-56320)+65536}return n},String.prototype.kmwIndexOf=function(o,e){var t=String(this),r=t.indexOf(o,e);if(r<0)return r;for(var n=0,i=0;i!==null&&ie){var i=o;o=e,e=i}r=t.kmwCodePointToCodeUnit(o),n=t.kmwCodePointToCodeUnit(e)}return(isNaN(r)||r===null)&&(r=0),(isNaN(n)||n===null)&&(n=t.length),t.substring(r,n)},String.prototype.kmwNextChar=function(o){var e=String(this);if(o===null||o<0||o>=e.length-1)return null;var t=e.charCodeAt(o);if(t>=55296&&t<=56319&&e.length>o+1){var r=e.charCodeAt(o+1);if(r>=56320&&r<=57343)return o==e.length-2?null:o+2}return o+1},String.prototype.kmwPrevChar=function(o){var e=String(this);if(o==null||o<=0||o>e.length)return null;var t=e.charCodeAt(o-1);if(t>=56320&&t<=57343&&o>1){var r=e.charCodeAt(o-2);if(r>=55296&&r<=56319)return o-2}return o-1},String.prototype.kmwCodePointToCodeUnit=function(o){if(o===null)return null;var e=String(this),t=0;if(o<0){t=e.length;for(var r=0;r>o;r--)t=e.kmwPrevChar(t);return t}if(o==e.kmwLength())return e.length;for(var r=0;r=0?e.kmwSubstr(o,1):""},String.prototype.kmwBMPNextChar=function(o){var e=String(this);return o<0||o>=e.length-1?null:o+1},String.prototype.kmwBMPPrevChar=function(o){var e=String(this);return o<=0||o>e.length?null:o-1},String.prototype.kmwBMPCodePointToCodeUnit=function(o){return o},String.prototype.kmwBMPCodeUnitToCodePoint=function(o){return o},String.prototype.kmwBMPLength=function(){var o=String(this);return o.length},String.prototype.kmwBMPSubstr=function(o,e){var t=String(this);return o>-1?t.substr(o,e):t.substr(t.length+o,-o)},String.kmwEnableSupplementaryPlane=function(o){var e=String.prototype;String._kmwFromCharCode=o?String.kmwFromCharCode:String.fromCharCode,e._kmwCharAt=o?e.kmwCharAt:e.charAt,e._kmwCharCodeAt=o?e.kmwCharCodeAt:e.charCodeAt,e._kmwIndexOf=o?e.kmwIndexOf:e.indexOf,e._kmwLastIndexOf=o?e.kmwLastIndexOf:e.lastIndexOf,e._kmwSlice=o?e.kmwSlice:e.slice,e._kmwSubstring=o?e.kmwSubstring:e.substring,e._kmwSubstr=o?e.kmwSubstr:e.kmwBMPSubstr,e._kmwLength=o?e.kmwLength:e.kmwBMPLength,e._kmwNextChar=o?e.kmwNextChar:e.kmwBMPNextChar,e._kmwPrevChar=o?e.kmwPrevChar:e.kmwBMPPrevChar,e._kmwCodePointToCodeUnit=o?e.kmwCodePointToCodeUnit:e.kmwBMPCodePointToCodeUnit,e._kmwCodeUnitToCodePoint=o?e.kmwCodeUnitToCodePoint:e.kmwBMPCodeUnitToCodePoint},String._kmwFromCharCode||String.kmwEnableSupplementaryPlane(!1)}h(O,"extendString");O();var De=class De{constructor(e){this._isFulfilled=!1;this._isRejected=!1;this._promise=new Promise((t,r)=>{this._resolve=n=>{this._isFulfilled=!0,t(n)},this._reject=n=>{this._isRejected=!0,r(n)},e&&e(this._resolve,this._reject)})}get resolve(){return this._resolve}get reject(){return this._reject}get isFulfilled(){return this._isFulfilled}get isRejected(){return this._isRejected}get isResolved(){return this.isFulfilled||this.isRejected}then(e,t){return this._promise.then(e,t)}catch(e){return this._promise.catch(e)}finally(e){return this._promise.finally(e)}get corePromise(){return this._promise}};h(De,"ManagedPromise");var ne=De;var Fe=class Fe extends ne{constructor(t){let r=null;super(s=>{r=setTimeout(()=>{this.isResolved||s(!0)},t)});this.timerHandle=r;let n=this._resolve;this._resolve=s=>{clearTimeout(this.timerHandle),n(s)};let i=this._reject;this._reject=s=>{clearTimeout(this.timerHandle),i(s)}}};h(Fe,"TimeoutPromise");var ie=Fe,Ue=h(o=>new ie(o).corePromise,"timedPromise");var I=class I{constructor(e,t){if(typeof e!="function"){this.comparator=e.comparator,this.heap=[].concat(e.heap);return}let r=e;this.comparator=r,this.heap=(t!=null?t:[]).slice(0),this.heapify()}static leftChildIndex(e){return e*2+1}static rightChildIndex(e){return e*2+2}static parentIndex(e){return Math.floor((e-1)/2)}heapify(e,t){if(e==null||t==null){this.heapify(0,this.count-1);return}let r=[],n=-1;for(let i=t;i>=e;i--){let s=I.parentIndex(i);this.siftDown(i)&&s0;){let i=r.shift(),s=I.parentIndex(i);this.siftDown(i)&&s>=0&&n!=s&&(r.push(s),n=s)}}get count(){return this.heap.length}peek(){return this.heap[0]}enqueue(e){let t=this.heap.length;this.heap.push(e);let r=I.parentIndex,n=r(t);for(;t!==0&&this.comparator(this.heap[t],this.heap[n])<0;){let i=this.heap[t];this.heap[t]=this.heap[n],this.heap[n]=i,t=n,n=r(t)}}enqueueAll(e){if(e.length==0)return;let t=this.count;this.heap=this.heap.concat(e);let r=I.parentIndex(t);this.heapify(r>=0?r:0,I.parentIndex(this.count-1))}dequeue(){if(this.count==0)return;let e=this.heap[0],t=this.heap.pop();return this.heap.length>0&&(this.heap[0]=t,this.siftDown(0)),e}siftDown(e){let t=I.leftChildIndex(e),r=I.rightChildIndex(e),n=e;if(tEt,QuoteBehavior:()=>j,SENTINEL_CODE_UNIT:()=>N,TrieModel:()=>ue,applyTransform:()=>g,buildMergedTransform:()=>U,defaultApplyCasing:()=>we,getLastPreCaretToken:()=>ae,isHighSurrogate:()=>F,isLowSurrogate:()=>We,isSentinel:()=>ke,tokenize:()=>se,transformToSuggestion:()=>W,wordbreak:()=>X});O();var N="﷐";function g(o,e){var c,p;let t=e.left||"",r=t.kmwLength(),n=r=55296&&o<=56319}h(F,"isHighSurrogate");function We(o){return typeof o=="string"&&(o=o.charCodeAt(0)),o>=56320&&o<=57343}h(We,"isLowSurrogate");function ke(o){return o==N}h(ke,"isSentinel");function W(o,e){let t={transform:o,displayAs:o.insert};return o.id!==void 0&&(t.transformId=o.id),(e===0||e)&&(t.p=e),t}h(W,"transformToSuggestion");function we(o,e){switch(o){case"lower":return e.toLowerCase();case"upper":return e.toUpperCase();case"initial":let t=1;return e.length>1&&F(e.charAt(0))&&We(e.charCodeAt(1))&&(t=2),e.substring(0,t).toUpperCase().concat(e.substring(t))}}h(we,"defaultApplyCasing");var oe=(r=>(r.noQuotes="no-quotes",r.useQuotes="use-quotes",r.default="default-quotes",r))(oe||{});(e=>{function o(t,r,n,i){if(i=="default-quotes"||!i)throw"Specified quote behavior may be ambiguous - default behavior not specified (may not be .default)";switch(t=="default-quotes"&&(t=i),t){case"no-quotes":return r;case"use-quotes":let{open:s,close:l}=n.quotesForKeepSuggestion;return s+r+l;default:throw"Unsupported quote behavior state detected; implementation missing!"}}e.apply=o,h(o,"apply")})(oe||(oe={}));var j=oe;function se(o,e,t){let r=(t==null?void 0:t.rejoins)||["'"];e=e||{left:void 0,startOfBuffer:void 0,endOfBuffer:void 0};let n=o(e.left||"")||[],i=o(e.right||"")||[],s={left:[],right:[],caretSplitsToken:!1},l=0;for(;n.length>0;){let c=n[0];if(Math.max(c.start,l)!=l){let p=Math.max(l,c.start);s.left.push({text:e.left.substring(l,p),isWhitespace:!0}),l=p}else n.shift(),s.left.push({text:c.text}),l=Math.max(l,c.end)}if(e.left!=null&&l!=e.left.length){let c=Math.max(l,e.left.length);s.left.push({text:e.left.substring(l,c),isWhitespace:!0}),l=c}let a=s.left.length;if(a>1){let c=s.left[a-2],p=s.left[a-1];!c.isWhitespace&&!p.isWhitespace&&r.indexOf(p.text)!=-1&&(s.left.pop(),s.left.pop(),s.left.push({text:c.text+p.text}),a--)}l=0;let u=!0;for(;i.length>0;){let c=i[0];if(Math.max(c.start,l)!=l){let p=Math.max(l,c.start);s.right.push({text:e.right.substring(l,p),isWhitespace:!0}),l=p}else{let p=s.left[a-1];p&&u&&!p.isWhitespace&&o(p.text+c.text).length==1&&(s.caretSplitsToken=!0),i.shift(),s.right.push({text:c.text}),l=Math.max(l,c.end)}u=!1}if(e.right&&l!=e.right.length){let c=Math.max(l,e.right.length);s.right.push({text:e.right.substring(l,c),isWhitespace:!0}),l=c}return s}h(se,"tokenize");function ae(o,e){let t=se(o,e);if(t.left.length>0){let r=t.left.pop();return r.isWhitespace?"":r.text}return""}h(ae,"getLastPreCaretToken");function X(o,e){return ae(o,e)}h(X,"wordbreak");var le={};_e(le,{ascii:()=>Ke,default:()=>R,defaultWordbreaker:()=>R,placeholder:()=>qe});function qe(o){let e=0;return o.split(/\\s+/).map(t=>{let r={start:e,end:e+t.length,text:t,length:t.length};return e=r.end,r})}h(qe,"placeholder");function Ke(o){let e=/[A-Za-z0-9']+/g,t=[],r;for(;(r=e.exec(o))!==null;)t.push(new Be(r[0],r.index));return t}h(Ke,"ascii");var Qe=class Qe{constructor(e,t){this.text=e,this.start=t}get length(){return this.text.length}get end(){return this.start+this.text.length}};h(Qe,"RegExpDerivedSpan");var Be=Qe;var xt=["Other","LF","Newline","CR","WSegSpace","Double_Quote","Single_Quote","MidNum","MidNumLet","Numeric","MidLetter","ALetter","ExtendNumLet","Format","Extend","Hebrew_Letter","ZWJ","Katakana","Regional_Indicator","sot","eot"],bt=\`\\0 +!\\v" +# $! "%# '&( ,'- .(/ 0):*;'< A+[ _,\\\` a+{ …"† ª+« ­-® µ+¶ ·*¸ º+» À+× Ø+÷ ø+˘ ˞+̀.Ͱ+͵ Ͷ+͸ ͺ+;'Ϳ+΀ Ά+·*Έ+΋ Ό+΍ Ύ+΢ Σ+϶ Ϸ+҂ ҃.Ҋ+԰ Ա+՗ ՙ+՝ ՞+՟*ՠ+։'֊+֋ ֑.־ ֿ.׀ ׁ.׃ ׄ.׆ ׇ.׈ א/׫ ׯ/׳+״*׵ ؀)؆ ،'؎ ؐ.؛ ؜-؝ ؠ+ً.٠)٪ ٫)٬'٭ ٮ+ٰ.ٱ+۔ ە+ۖ.۝)۞ ۟.ۥ+ۧ.۩ ۪.ۮ+۰)ۺ+۽ ۿ+܀ ܏+ܑ.ܒ+ܰ.݋ ݍ+ަ.ޱ+޲ ߀)ߊ+߫.ߴ+߶ ߸'߹ ߺ+߻ ߽.߾ ࠀ+ࠖ.ࠚ+ࠛ.ࠤ+ࠥ.ࠨ+ࠩ.࠮ ࡀ+࡙.࡜ ࡠ+࡫ ࡰ+࢈ ࢉ+࢏ ࢐)࢒ ࢗ.ࢠ+࣊.࣢)ࣣ.ऄ+ऺ.ऽ+ा.ॐ+॑.क़+ॢ.। ०)॰ ॱ+ঁ.঄ অ+঍ এ+঑ ও+঩ প+঱ ল+঳ শ+঺ ়.ঽ+া.৅ ে.৉ ো.ৎ+৏ ৗ.৘ ড়+৞ য়+ৢ.৤ ০)ৰ+৲ ৼ+৽ ৾.৿ ਁ.਄ ਅ+਋ ਏ+਑ ਓ+਩ ਪ+਱ ਲ+਴ ਵ+਷ ਸ+਺ ਼.਽ ਾ.੃ ੇ.੉ ੋ.੎ ੑ.੒ ਖ਼+੝ ਫ਼+੟ ੦)ੰ.ੲ+ੵ.੶ ઁ.઄ અ+઎ એ+઒ ઓ+઩ પ+઱ લ+઴ વ+઺ ઼.ઽ+ા.૆ ે.૊ ો.૎ ૐ+૑ ૠ+ૢ.૤ ૦)૰ ૹ+ૺ.଀ ଁ.଄ ଅ+଍ ଏ+଑ ଓ+଩ ପ+଱ ଲ+଴ ଵ+଺ ଼.ଽ+ା.୅ େ.୉ ୋ.୎ ୕.୘ ଡ଼+୞ ୟ+ୢ.୤ ୦)୰ ୱ+୲ ஂ.ஃ+஄ அ+஋ எ+஑ ஒ+஖ ங+஛ ஜ+஝ ஞ+஠ ண+஥ ந+஫ ம+஺ ா.௃ ெ.௉ ொ.௎ ௐ+௑ ௗ.௘ ௦)௰ ఀ.అ+఍ ఎ+఑ ఒ+఩ ప+఺ ఼.ఽ+ా.౅ ె.౉ ొ.౎ ౕ.౗ ౘ+౛ ౝ+౞ ౠ+ౢ.౤ ౦)౰ ಀ+ಁ.಄ ಅ+಍ ಎ+಑ ಒ+಩ ಪ+಴ ವ+಺ ಼.ಽ+ಾ.೅ ೆ.೉ ೊ.೎ ೕ.೗ ೝ+೟ ೠ+ೢ.೤ ೦)೰ ೱ+ೳ.೴ ഀ.ഄ+഍ എ+഑ ഒ+഻.ഽ+ാ.൅ െ.൉ ൊ.ൎ+൏ ൔ+ൗ.൘ ൟ+ൢ.൤ ൦)൰ ൺ+඀ ඁ.඄ අ+඗ ක+඲ ඳ+඼ ල+඾ ව+෇ ්.෋ ා.෕ ූ.෗ ෘ.෠ ෦)෰ ෲ.෴ ั.า ิ.฻ ็.๏ ๐)๚ ັ.າ ິ.ຽ ່.໏ ໐)໚ ༀ+༁ ༘.༚ ༠)༪ ༵.༶ ༷.༸ ༹.༺ ༾.ཀ+཈ ཉ+཭ ཱ.྅ ྆.ྈ+ྍ.྘ ྙ.྽ ࿆.࿇ ါ.ဿ ၀)၊ ၖ.ၚ ၞ.ၡ ၢ.ၥ ၧ.ၮ ၱ.ၵ ႂ.ႎ ႏ.႐)ႚ.႞ Ⴀ+჆ Ⴧ+჈ Ⴭ+჎ ა+჻ ჼ+቉ ቊ+቎ ቐ+቗ ቘ+቙ ቚ+቞ በ+኉ ኊ+኎ ነ+኱ ኲ+኶ ኸ+኿ ዀ+዁ ዂ+዆ ወ+዗ ዘ+጑ ጒ+጖ ጘ+፛ ፝.፠ ᎀ+᎐ Ꭰ+᏶ ᏸ+᏾ ᐁ+᙭ ᙯ+ $ᚁ+᚛ ᚠ+᛫ ᛮ+᛹ ᜀ+ᜒ.᜖ ᜟ+ᜲ.᜵ ᝀ+ᝒ.᝔ ᝠ+᝭ ᝮ+᝱ ᝲ.᝴ ឴.។ ៝.៞ ០)៪ ᠋.᠎-᠏.᠐)᠚ ᠠ+᡹ ᢀ+ᢅ.ᢇ+ᢩ.ᢪ+᢫ ᢰ+᣶ ᤀ+᤟ ᤠ.᤬ ᤰ.᤼ ᥆)ᥐ ᧐)᧛ ᨀ+ᨗ.᨜ ᩕ.᩟ ᩠.᩽ ᩿.᪀)᪊ ᪐)᪚ ᪰.᫏ ᬀ.ᬅ+᬴.ᭅ+᭍ ᭐)᭚ ᭫.᭴ ᮀ.ᮃ+ᮡ.ᮮ+᮰)ᮺ+᯦.᯴ ᰀ+ᰤ.᰸ ᱀)᱊ ᱍ+᱐)ᱚ+᱾ ᲀ+᲋ Ა+᲻ Ჽ+᳀ ᳐.᳓ ᳔.ᳩ+᳭.ᳮ+᳴.ᳵ+᳷.ᳺ+᳻ ᴀ+᷀.Ḁ+἖ Ἐ+἞ ἠ+὆ Ὀ+὎ ὐ+὘ Ὑ+὚ Ὓ+὜ Ὕ+὞ Ὗ+὾ ᾀ+᾵ ᾶ+᾽ ι+᾿ ῂ+῅ ῆ+῍ ῐ+῔ ῖ+῜ ῠ+῭ ῲ+῵ ῶ+´  $   $​ ‌.‍0‎-‐ ‘(‚ ․(‥ ‧*\\u2028"‪- ,‰ ‿,⁁ ⁄'⁅ ⁔,⁕  $⁠-⁥ ⁦-⁰ ⁱ+⁲ ⁿ+₀ ₐ+₝ ⃐.⃱ ℂ+℃ ℇ+℈ ℊ+℔ ℕ+№ ℙ+℞ ℤ+℥ Ω+℧ ℨ+℩ K+℮ ℯ+℺ ℼ+⅀ ⅅ+⅊ ⅎ+⅏ Ⅰ+↉ Ⓐ+⓪ Ⰰ+⳥ Ⳬ+⳯.Ⳳ+⳴ ⴀ+⴦ ⴧ+⴨ ⴭ+⴮ ⴰ+⵨ ⵯ+⵰ ⵿.ⶀ+⶗ ⶠ+⶧ ⶨ+⶯ ⶰ+⶷ ⶸ+⶿ ⷀ+⷇ ⷈ+⷏ ⷐ+⷗ ⷘ+⷟ ⷠ.⸀ ⸯ+⸰  $、 々+〆 〪.〰 〱1〶 〻+〽 ゙.゛1ゝ ゠1・ ー1㄀ ㄅ+㄰ ㄱ+㆏ ㆠ+㇀ ㇰ1㈀ ㋐1㋿ ㌀1㍘ ꀀ+꒍ ꓐ+꓾ ꔀ+꘍ ꘐ+꘠)ꘪ+꘬ Ꙁ+꙯.꙳ ꙴ.꙾ ꙿ+ꚞ.ꚠ+꛰.꛲ ꜈+꟎ Ꟑ+꟒ ꟓ+꟔ ꟕ+꟝ ꟲ+ꠂ.ꠃ+꠆.ꠇ+ꠋ.ꠌ+ꠣ.꠨ ꠬.꠭ ꡀ+꡴ ꢀ.ꢂ+ꢴ.꣆ ꣐)꣚ ꣠.ꣲ+꣸ ꣻ+꣼ ꣽ+ꣿ.꤀)ꤊ+ꤦ.꤮ ꤰ+ꥇ.꥔ ꥠ+꥽ ꦀ.ꦄ+꦳.꧁ ꧏ+꧐)꧚ ꧥ.ꧦ ꧰)ꧺ ꨀ+ꨩ.꨷ ꩀ+ꩃ.ꩄ+ꩌ.꩎ ꩐)꩚ ꩻ.ꩾ ꪰ.ꪱ ꪲ.ꪵ ꪷ.ꪹ ꪾ.ꫀ ꫁.ꫂ ꫠ+ꫫ.꫰ ꫲ+ꫵ.꫷ ꬁ+꬇ ꬉ+꬏ ꬑ+꬗ ꬠ+꬧ ꬨ+꬯ ꬰ+꭪ ꭰ+ꯣ.꯫ ꯬.꯮ ꯰)꯺ 가+힤 ힰ+퟇ ퟋ+퟼ ff+﬇ ﬓ+﬘ יִ/ﬞ.ײַ/﬩ שׁ/﬷ טּ/﬽ מּ/﬿ נּ/﭂ ףּ/﭅ צּ/ﭐ+﮲ ﯓ+﴾ ﵐ+﶐ ﶒ+﷈ ﷰ+﷼ ︀.︐ ︓*︔ ︠.︰ ︳,︵ ﹍,﹐'﹑ ﹒(﹓ ﹔'﹕*﹖ ﹰ+﹵ ﹶ+﻽ \\uFEFF-＀ '(( ,'- .(/ 0):*;'< A+[ _,` a+{ ヲ1゙.ᅠ+﾿ ᅡ+￈ ᅧ+￐ ᅭ+￘ ᅳ+￝ - ￿ \`,kt="𐀀+𐀌 𐀍+𐀧 𐀨+𐀻 𐀼+𐀾 𐀿+𐁎 𐁐+𐁞 𐂀+𐃻 𐅀+𐅵 𐇽.𐇾 𐊀+𐊝 𐊠+𐋑 𐋠.𐋡 𐌀+𐌠 𐌭+𐍋 𐍐+𐍶.𐍻 𐎀+𐎞 𐎠+𐏄 𐏈+𐏐 𐏑+𐏖 𐐀+𐒞 𐒠)𐒪 𐒰+𐓔 𐓘+𐓼 𐔀+𐔨 𐔰+𐕤 𐕰+𐕻 𐕼+𐖋 𐖌+𐖓 𐖔+𐖖 𐖗+𐖢 𐖣+𐖲 𐖳+𐖺 𐖻+𐖽 𐗀+𐗴 𐘀+𐜷 𐝀+𐝖 𐝠+𐝨 𐞀+𐞆 𐞇+𐞱 𐞲+𐞻 𐠀+𐠆 𐠈+𐠉 𐠊+𐠶 𐠷+𐠹 𐠼+𐠽 𐠿+𐡖 𐡠+𐡷 𐢀+𐢟 𐣠+𐣳 𐣴+𐣶 𐤀+𐤖 𐤠+𐤺 𐦀+𐦸 𐦾+𐧀 𐨀+𐨁.𐨄 𐨅.𐨇 𐨌.𐨐+𐨔 𐨕+𐨘 𐨙+𐨶 𐨸.𐨻 𐨿.𐩀 𐩠+𐩽 𐪀+𐪝 𐫀+𐫈 𐫉+𐫥.𐫧 𐬀+𐬶 𐭀+𐭖 𐭠+𐭳 𐮀+𐮒 𐰀+𐱉 𐲀+𐲳 𐳀+𐳳 𐴀+𐴤.𐴨 𐴰)𐴺 𐵀)𐵊+𐵦 𐵩.𐵮 𐵯+𐶆 𐺀+𐺪 𐺫.𐺭 𐺰+𐺲 𐻂+𐻅 𐻼.𐼀+𐼝 𐼧+𐼨 𐼰+𐽆.𐽑 𐽰+𐾂.𐾆 𐾰+𐿅 𐿠+𐿷 𑀀.𑀃+𑀸.𑁇 𑁦)𑁰.𑁱+𑁳.𑁵+𑁶 𑁿.𑂃+𑂰.𑂻 𑂽)𑂾 𑃂.𑃃 𑃍)𑃎 𑃐+𑃩 𑃰)𑃺 𑄀.𑄃+𑄧.𑄵 𑄶)𑅀 𑅄+𑅅.𑅇+𑅈 𑅐+𑅳.𑅴 𑅶+𑅷 𑆀.𑆃+𑆳.𑇁+𑇅 𑇉.𑇍 𑇎.𑇐)𑇚+𑇛 𑇜+𑇝 𑈀+𑈒 𑈓+𑈬.𑈸 𑈾.𑈿+𑉁.𑉂 𑊀+𑊇 𑊈+𑊉 𑊊+𑊎 𑊏+𑊞 𑊟+𑊩 𑊰+𑋟.𑋫 𑋰)𑋺 𑌀.𑌄 𑌅+𑌍 𑌏+𑌑 𑌓+𑌩 𑌪+𑌱 𑌲+𑌴 𑌵+𑌺 𑌻.𑌽+𑌾.𑍅 𑍇.𑍉 𑍋.𑍎 𑍐+𑍑 𑍗.𑍘 𑍝+𑍢.𑍤 𑍦.𑍭 𑍰.𑍵 𑎀+𑎊 𑎋+𑎌 𑎎+𑎏 𑎐+𑎶 𑎷+𑎸.𑏁 𑏂.𑏃 𑏅.𑏆 𑏇.𑏋 𑏌.𑏑+𑏒.𑏓+𑏔 𑏡.𑏣 𑐀+𑐵.𑑇+𑑋 𑑐)𑑚 𑑞.𑑟+𑑢 𑒀+𑒰.𑓄+𑓆 𑓇+𑓈 𑓐)𑓚 𑖀+𑖯.𑖶 𑖸.𑗁 𑗘+𑗜.𑗞 𑘀+𑘰.𑙁 𑙄+𑙅 𑙐)𑙚 𑚀+𑚫.𑚸+𑚹 𑛀)𑛊 𑛐)𑛤 𑜝.𑜬 𑜰)𑜺 𑠀+𑠬.𑠻 𑢠+𑣠)𑣪 𑣿+𑤇 𑤉+𑤊 𑤌+𑤔 𑤕+𑤗 𑤘+𑤰.𑤶 𑤷.𑤹 𑤻.𑤿+𑥀.𑥁+𑥂.𑥄 𑥐)𑥚 𑦠+𑦨 𑦪+𑧑.𑧘 𑧚.𑧡+𑧢 𑧣+𑧤.𑧥 𑨀+𑨁.𑨋+𑨳.𑨺+𑨻.𑨿 𑩇.𑩈 𑩐+𑩑.𑩜+𑪊.𑪚 𑪝+𑪞 𑪰+𑫹 𑯀+𑯡 𑯰)𑯺 𑰀+𑰉 𑰊+𑰯.𑰷 𑰸.𑱀+𑱁 𑱐)𑱚 𑱲+𑲐 𑲒.𑲨 𑲩.𑲷 𑴀+𑴇 𑴈+𑴊 𑴋+𑴱.𑴷 𑴺.𑴻 𑴼.𑴾 𑴿.𑵆+𑵇.𑵈 𑵐)𑵚 𑵠+𑵦 𑵧+𑵩 𑵪+𑶊.𑶏 𑶐.𑶒 𑶓.𑶘+𑶙 𑶠)𑶪 𑻠+𑻳.𑻷 𑼀.𑼂+𑼃.𑼄+𑼑 𑼒+𑼴.𑼻 𑼾.𑽃 𑽐)𑽚.𑽛 𑾰+𑾱 𒀀+𒎚 𒐀+𒑯 𒒀+𒕄 𒾐+𒿱 𓀀+𓐰-𓑀.𓑁+𓑇.𓑖 𓑠+𔏻 𔐀+𔙇 𖄀+𖄞.𖄰)𖄺 𖠀+𖨹 𖩀+𖩟 𖩠)𖩪 𖩰+𖪿 𖫀)𖫊 𖫐+𖫮 𖫰.𖫵 𖬀+𖬰.𖬷 𖭀+𖭄 𖭐)𖭚 𖭣+𖭸 𖭽+𖮐 𖵀+𖵭 𖵰)𖵺 𖹀+𖺀 𖼀+𖽋 𖽏.𖽐+𖽑.𖾈 𖾏.𖾓+𖾠 𖿠+𖿢 𖿣+𖿤.𖿥 𖿰.𖿲 𚿰1𚿴 𚿵1𚿼 𚿽1𚿿 𛀀1𛀁 𛄠1𛄣 𛅕1𛅖 𛅤1𛅨 𛰀+𛱫 𛱰+𛱽 𛲀+𛲉 𛲐+𛲚 𛲝.𛲟 𛲠-𛲤 𜳰)𜳺 𜼀.𜼮 𜼰.𜽇 𝅥.𝅪 𝅭.𝅳-𝅻.𝆃 𝆅.𝆌 𝆪.𝆮 𝉂.𝉅 𝐀+𝑕 𝑖+𝒝 𝒞+𝒠 𝒢+𝒣 𝒥+𝒧 𝒩+𝒭 𝒮+𝒺 𝒻+𝒼 𝒽+𝓄 𝓅+𝔆 𝔇+𝔋 𝔍+𝔕 𝔖+𝔝 𝔞+𝔺 𝔻+𝔿 𝕀+𝕅 𝕆+𝕇 𝕊+𝕑 𝕒+𝚦 𝚨+𝛁 𝛂+𝛛 𝛜+𝛻 𝛼+𝜕 𝜖+𝜵 𝜶+𝝏 𝝐+𝝯 𝝰+𝞉 𝞊+𝞩 𝞪+𝟃 𝟄+𝟌 𝟎)𝠀 𝨀.𝨷 𝨻.𝩭 𝩵.𝩶 𝪄.𝪅 𝪛.𝪠 𝪡.𝪰 𝼀+𝼟 𝼥+𝼫 𞀀.𞀇 𞀈.𞀙 𞀛.𞀢 𞀣.𞀥 𞀦.𞀫 𞀰+𞁮 𞂏.𞂐 𞄀+𞄭 𞄰.𞄷+𞄾 𞅀)𞅊 𞅎+𞅏 𞊐+𞊮.𞊯 𞋀+𞋬.𞋰)𞋺 𞓐+𞓬.𞓰)𞓺 𞗐+𞗮.𞗰+𞗱)𞗻 𞟠+𞟧 𞟨+𞟬 𞟭+𞟯 𞟰+𞟿 𞠀+𞣅 𞣐.𞣗 𞤀+𞥄.𞥋+𞥌 𞥐)𞥚 𞸀+𞸄 𞸅+𞸠 𞸡+𞸣 𞸤+𞸥 𞸧+𞸨 𞸩+𞸳 𞸴+𞸸 𞸹+𞸺 𞸻+𞸼 𞹂+𞹃 𞹇+𞹈 𞹉+𞹊 𞹋+𞹌 𞹍+𞹐 𞹑+𞹓 𞹔+𞹕 𞹗+𞹘 𞹙+𞹚 𞹛+𞹜 𞹝+𞹞 𞹟+𞹠 𞹡+𞹣 𞹤+𞹥 𞹧+𞹫 𞹬+𞹳 𞹴+𞹸 𞹹+𞹽 𞹾+𞹿 𞺀+𞺊 𞺋+𞺜 𞺡+𞺤 𞺥+𞺪 𞺫+𞺼 🄰+🅊 🅐+🅪 🅰+🆊 🇦2🈀 🏻.🐀 🯰)🯺 󠀁-󠀂 󠀠.󠂀 󠄀.󠇰 ";function wt(o){let e=o<=65535?2:3,t=e==2?bt:kt;return He(t,o,e,0,t.length/e-1)-32}h(wt,"searchForProperty");function He(o,e,t,r,n){if(n=a?He(o,e,t,i+1,n):o.charCodeAt(t*(i+1)-1)}h(He,"_searchForProperty");function R(o,e){let t=jt(o,e);if(t.length==0)return[];let r=[];for(let n=0;n=this.text.length?20:yt(this.text[e])?ze(this.text[e]+this.text[e+1]):ze(this.text[e],this.options)}match(e,t,r,n){var s,l,a,u;let i=(s=e==null?void 0:e.includes(this.lookbehind))!=null?s:!0;return i=i&&((l=t==null?void 0:t.includes(this.left))!=null?l:!0),i=i&&((a=r==null?void 0:r.includes(this.right))!=null?a:!0),i&&((u=n==null?void 0:n.includes(this.lookahead))!=null?u:!0)}propertyMatch(e,t,r,n){let i=h(s=>vt(s,this.options),"propMapper");return this.match(e==null?void 0:e.map(i),t==null?void 0:t.map(i),r==null?void 0:r.map(i),n==null?void 0:n.map(i))}};h(Y,"BreakerContext");var Ve=Y;function Gt(o,e){return!o.split("").map(t=>ze(t,e)).every(t=>t===3||t===1||t===2||t===4)}h(Gt,"isNonSpace");function jt(o,e){if(o.length===0)return[];e&&!e.rules&&(e.rules=[]);let t=[],r,n=0,i=new Ve(o,e,n),s=0;do{if(r=n,n=l(n),i=i.next(n),i.match(null,[19],null,null)){t.push(r);continue}if(i.match(null,null,[20],null)){t.push(r);break}if(i.match(null,[3],[1],null))continue;let a=[2,3,1];if(i.match(null,a,null,null)){t.push(r);continue}if(i.match(null,null,a,null)){t.push(r);continue}if(i.match(null,[4],[4],null))continue;let u=[13,14,16];for(;i.match(null,null,u,null);)[r,n]=[n,l(n)],i=i.ignoringRight(n);if(i.right===20){t.push(r);break}for(;i.match(null,null,null,u);)n=l(n),i=i.ignoringLookahead(n);let c=[11,15],p=[8,6];if(e!=null&&e.rules){let x=!1;for(let m of e.rules)if(x=m.match(i),x){m.breakIfMatch&&t.push(r);break}if(x)continue}if(i.match(null,c,c,null))continue;let f=[10].concat(p);if(i.match(null,c,f,c)||i.match(c,f,c,null)||i.match(null,[15],[6],null)||i.match(null,[15],[5],[15])||i.match([15],[5],[15],null)||i.match(null,[9],[9],null)||i.match(null,c,[9],null)||i.match(null,[9],c,null))continue;let d=[7].concat(p);if(i.match([9],d,[9],null)||i.match(null,[9],d,[9])||i.match(null,[17],[17],null))continue;let C=[17,9].concat(c);if(!i.match(null,C,[12],null)&&!i.match(null,[12],[12],null)&&!i.match(null,[12],C,null)){if(i.right===18){if(s+=1,s%2==1)continue}else s=0;t.push(r)}}while(r=o.length?o.length:yt(o[a])?a+2:a+1}}h(jt,"findBoundaries");function yt(o){let e=o.charCodeAt(0);return e>=55296&&e<=56319}h(yt,"isStartOfSurrogatePair");function ze(o,e){if(e!=null&&e.propertyMapping){let r=e.propertyMapping(o);if(r)return vt(r,e)}let t=o.codePointAt(0);return wt(t)}h(ze,"property");function vt(o,e){var n,i;let t=h(s=>s.toLowerCase()==o.toLowerCase(),"matcher"),r=(i=(n=e==null?void 0:e.customProperties)==null?void 0:n.findIndex(t))!=null?i:-1;return r!=-1?-r-1:xt.findIndex(t)}h(vt,"propertyVal");O();var Lt=12,_=class _{constructor(e,t,r){this.root=e,this.prefix=t,this.totalWeight=r}child(e){if(e=="")return this;let t=e.split(""),r=this;for(;t.length>0&&r;){let n=t.shift();r=r._child(n)}return r}_child(e){let t=this.root,r=this.totalWeight,n=this.prefix+e;if(t.type=="internal"){let i=t.children[e];return i?new _(i,n,r):void 0}else return t.entries.filter(function(s){return s.key.indexOf(n)==0}).length?new _(t,n,r):void 0}*children(){let e=this.root,t=this.totalWeight;if(e.type=="internal"){for(let r of e.values){let n=e.children[r];if(F(r))if(n.type=="internal"){let i=n;for(let s of i.values){let l=this.prefix+r+s;yield{char:r+s,traversal:function(){return new _(i.children[s],l,t)}}}}else{let i=n.entries[0].key;r=r+i[this.prefix.length+1];let s=this.prefix+r;yield{char:r,traversal:function(){return new _(n,s,t)}}}else{if(ke(r))continue;if(r){let i=this.prefix+r;yield{char:r,traversal:function(){return new _(n,i,t)}}}else continue}}return}else{let r=this.prefix,n=e.entries.filter(function(i){return i.key!=r&&r.length({text:t.content,p:t.weight/this.totalWeight}),"entryMapper");if(this.root.type=="leaf"){let t=this.prefix;return this.root.entries.filter(function(n){return n.key==t}).map(e)}else{let t=this.root.children[N];return t&&t.type=="leaf"?t.entries.map(e):[]}}get p(){return this.root.weight/this.totalWeight}};h(_,"Traversal");var je=_,Ye=class Ye{constructor(e,t={}){this.languageUsesCasing=t.languageUsesCasing,this.applyCasing=t.applyCasing,this._trie=new Xe(e.root,e.totalWeight,t.searchTermToKey||Xt),this.breakWords=t.wordBreaker||R,this.punctuation=t.punctuation}configure(e){var t;return this.configuration={leftContextCodePoints:e.maxLeftContextCodePoints,rightContextCodePoints:(t=e.maxRightContextCodePoints)!=null?t:0}}toKey(e){return this._trie.toKey(e)}predict(e,t){if(!e.insert&&!t.left&&!t.right&&t.startOfBuffer&&t.endOfBuffer)return s(this._trie.firstN(Lt).map(({text:l,p:a})=>({transform:{insert:l,deleteLeft:0},displayAs:l,p:a})));let r=g(e,t),n=e.deleteLeft-e.insert.kmwLength(),i=ae(this.breakWords,r);return s(this._trie.lookup(i).map(({text:l,p:a})=>W({insert:l,deleteLeft:n+i.kmwLength()},a)));function s(l){let a=[];for(let u of l)a.push({sample:u,p:u.p});return a}}get wordbreaker(){return this.breakWords}traverseFromRoot(){return this._trie.traverseFromRoot()}};h(Ye,"TrieModel");var ue=Ye,$e=class $e{constructor(e,t,r){this.root=e,this.toKey=r,this.totalWeight=t}traverseFromRoot(){return new je(this.root,"",this.totalWeight)}lookup(e){let t=this.toKey(e),r=this.traverseFromRoot().child(t);if(!r)return[];let n=r.entries,i={};for(let a of n)i[a.text]=a.text;let l=Mt(r).filter(a=>!i[a.text]);return n.concat(l)}firstN(e){return Mt(this.traverseFromRoot(),e)}};h($e,"Trie");var Xe=$e;function Mt(o,e=Lt){let t=new k(function(n,i){return(i?i.p:0)-(n?n.p:0)}),r=[];for(t.enqueue(o);t.count>0;){let n=t.dequeue();if(n.text!==void 0){let i=n;if(r.push(i),r.length>=e)return r}else{let i=n;t.enqueueAll(i.entries);let s=[];for(let l of i.children())s.push(l.traversal());t.enqueueAll(s)}}return r}h(Mt,"getSortedResults");function Xt(o){return o.normalize("NFD").replace(/[\\u0300-\\u036f]/g,"").toLowerCase()}h(Xt,"defaultSearchTermToKey");var et=class et{constructor(e){e=e||{},this._futureSuggestions=e.futureSuggestions?e.futureSuggestions.slice():[],e.punctuation&&(this.punctuation=e.punctuation),this.toKey=e.toKey,this.wordbreaker=e.wordbreaker,this.applyCasing=e.applyCasing,this.languageUsesCasing=e.languageUsesCasing}configure(e){return this.configuration={leftContextCodePoints:e.maxLeftContextCodePoints,rightContextCodePoints:e.maxRightContextCodePoints},this.configuration}predict(e,t,r){let n=h(function(s){let l=[];for(let a of s)l.push({sample:a,p:a.p!==void 0?a.p:1});return l},"makeUniformDistribution");if(r)return n(r);let i=this._futureSuggestions.shift();return i?n(i):[]}};h(et,"DummyModel");var Je=et,Et=Je;var Ce={};_e(Ce,{ClassicalDistanceCalculation:()=>K,ContextTracker:()=>Te,ExecutionBucket:()=>ce,ExecutionSpan:()=>Q,ExecutionTimer:()=>he,QUEUE_NODE_COMPARATOR:()=>Le,STANDARD_TIME_BETWEEN_DEFERS:()=>Me,SearchNode:()=>Ee,SearchResult:()=>de,SearchSpace:()=>v,TrackedContextState:()=>V,TrackedContextSuggestion:()=>lt,TrackedContextToken:()=>z});var T=class T{constructor(e){this.diagonalWidth=2;this.inputSequence=[];this.matchSequence=[];if(e){let t=e.resolvedDistances.length;this.resolvedDistances=Array(t);for(let r=0;r2*r)&&(n.sparse=!0),n}getCostAt(e,t,r=this.diagonalWidth){if(e<0||t<0)return e==-1&&t>=-1?t+1:t==-1&&e>=-1?e+1:Number.MAX_VALUE;let n=this.getTrueIndex(e,t,r);return n.sparse?Number.MAX_VALUE:this.resolvedDistances[n.row][n.col]}getFinalCost(){let e=this,t=e.getHeuristicFinalCost();for(;t>e.diagonalWidth;)e=e.increaseMaxDistance(),t=e.getHeuristicFinalCost();return t}getHeuristicFinalCost(){return this.getCostAt(this.inputSequence.length-1,this.matchSequence.length-1)}hasFinalCostWithin(e){let t=this,r=t.getHeuristicFinalCost(),n=this.diagonalWidth;do{if(r<=e)return!0;if(n=0&&c>=0){let p=1;if(n=["transpose-start"],u!=e-1){let f=e-u-1;n=n.concat(Array(f).fill("transpose-delete")),p+=f}else{let f=t-c-1;n=n.concat(Array(f).fill("transpose-insert")),p+=f}n.push("transpose-end"),this.getCostAt(u-1,c-1)!=r-p&&(n=null),i=[u-1,c-1]}return n||(a==r-1?(n=["substitute"],i=[e-1,t-1]):s==r-1?(n=["insert"],i=[e,t-1]):l==r-1?(n=["delete"],i=[e-1,t]):(n=["match"],i=[e-1,t-1])),i[0]>=0&&i[1]>=0?this.editPath(i[0],i[1]).concat(n):i[0]>-1?Array(i[0]+1).fill("delete").concat(n):i[1]>-1?Array(i[1]+1).fill("insert").concat(n):n}static getTransposeParent(e,t,r){if(t<0||r<0||e.inputSequence[t].key==e.matchSequence[r].key)return[-1,-1];let n=-1;for(let s=t-1;s>=0;s--)if(e.inputSequence[s].key==e.matchSequence[r].key){n=s;break}let i=-1;for(let s=r-1;s>=0;s--)if(e.matchSequence[s].key==e.inputSequence[t].key){i=s;break}return[n,i]}static initialCostAt(e,t,r,n,i){var s=e.inputSequence[t].key==e.matchSequence[r].key?0:1,l=e.getCostAt(t-1,r-1)+s,a=n||e.getCostAt(t,r-1)+1,u=i||e.getCostAt(t-1,r)+1,c=Number.MAX_VALUE;if(t>0&&r>0){let[p,f]=T.getTransposeParent(e,t,r);c=e.getCostAt(p-1,f-1)+(t-p-1)+1+(r-f-1)}return Math.min(l,u,a,c)}getSubset(e,t){let r=new T(this);if(e>this.inputSequence.length||t>this.matchSequence.length)throw"Invalid dimensions specified for trim operation";r.inputSequence.splice(e),r.matchSequence.splice(t),r.resolvedDistances.splice(e);let n=this.getTrueIndex(e-1,t-1,this.diagonalWidth);for(let i=n.col;i<=2*this.diagonalWidth;i++){let s=n.row-(i-n.col);if(s<0)break;if(i<0)r.resolvedDistances[s]=Array(2*r.diagonalWidth+1).fill(Number.MAX_VALUE);else{let l=2*this.diagonalWidth-i,a=r.resolvedDistances[s].splice(0,i+1),u=Array(l).fill(Number.MAX_VALUE);r.resolvedDistances[s]=a.concat(u)}}return r}static forDiagonalOfAxis(e,t,r,n){let i=r-t=0){let a=s==0?n+2:Number.MAX_VALUE;i=T.initialCostAt(e,n,s,a,void 0);let u=i;if(s0&&s){let a=n+1;this.propagateUpdateFrom(e,t+1,r,a,i-1)}if(s&&l){let a=n+(e.inputSequence[t+1].key==e.matchSequence[r+1].key?0:1);this.propagateUpdateFrom(e,t+1,r+1,a,i);let u=-1;for(let p=t+2;p0&&c>0){let p=n+(u-t-2)+1+(c-r-2);this.propagateUpdateFrom(e,u,c,p,e.diagonalWidth-1+c-u)}}}get mapKey(){let e=this.inputSequence.map(r=>r.key).join(""),t=this.matchSequence.map(r=>r.key).join("");return e+N+t+N+this.diagonalWidth}get lastInputEntry(){return this.inputSequence[this.inputSequence.length-1]}get lastMatchEntry(){return this.matchSequence[this.matchSequence.length-1]}static computeDistance(e,t,r=1){let n=new T;r=r||1,n.diagonalWidth=r;for(let i=0;i=Yt&&(this.nearOutliers.lengthr-t)),this.checkForOutlier()}checkForOutlier(){if(this.preventOutliers||this.eventCount<4||this.nearOutliers.length==0)return;let e=this.nearOutliers[0];this.timeSpent-=e,this.timeSquared-=e*e,this.eventCount--;let t=this.average,r=e-t,n=this.variance,i=r*r/n;i>=49||this.eventCount>=8&&i>=9?(this.nearOutliers.shift(),this.outliers.push(e)):(this.timeSpent+=e,this.timeSquared+=e*e,this.eventCount++)}get average(){return this.timeSpent/this.eventCount}get variance(){let e=this.eventCount;return e<=1?NaN:this.timeSquared/e-this.timeSpent*this.timeSpent/(e*e)}get outlierTime(){let e=0;for(let t=0;tthis.activeSpan=null),yield Ue(e),this.activeSpan.end(),this.spanSinceLastDefer=new Q})}get timeSinceLastDefer(){return this.spanSinceLastDefer.duration}start(e){this.validateStart();let t=this.getBucket(e);return this.activeSpan=new Q(t,()=>{this.activeSpan=null}),this.activeSpan}terminate(){this.maxTrueTime=0}get elapsed(){return performance.now()-this.trueStart>=this.maxTrueTime?!0:this.executionTime>=this.maxExecutionTime}};h(nt,"ExecutionTimer");var he=nt;var Le=h(function(o,e){return o.currentCost-e.currentCost},"QUEUE_NODE_COMPARATOR");var H=class H{constructor(e,t){this.toKey=h(e=>e,"toKey");if(t=t||(r=>r),e instanceof H){let r=e;this.calculation=r.calculation,this.currentTraversal=r.currentTraversal,this.priorInput=r.priorInput,this.toKey=r.toKey}else this.calculation=new K,this.currentTraversal=e,this.priorInput=[],this.toKey=t}get knownCost(){return this.calculation.getHeuristicFinalCost()}get inputSamplingCost(){if(this._inputCost!==void 0)return this._inputCost;{let e=v.MIN_KEYSTROKE_PROBABILITY;return this._inputCost=this.priorInput.map(t=>t.p>e?t.p:e).reduce((t,r)=>t-Math.log(r),0),this._inputCost}}get currentCost(){return v.EDIT_DISTANCE_COST_SCALE*this.knownCost+this.inputSamplingCost}buildInsertionEdges(){let e=[];for(let t of this.currentTraversal.children()){let r=t.traversal(),n={key:t.char,traversal:r},i=this.calculation.addMatchChar(n),s=new H(this);s.calculation=i,s.priorInput=this.priorInput,s.currentTraversal=r,e.push(s)}return e}buildDeletionEdges(e){let t=[];for(let r of e){if(r.p"+"+r.sample.insert+"-"+r.sample.deleteLeft).join(""),t=this.calculation.matchSequence.map(r=>r.key).join("");return e+N+t}get resultKey(){return this.calculation.matchSequence.map(e=>e.key).join("")}get isFullReplacement(){return this.knownCost&&this.knownCost==this.priorInput.length}};h(H,"SearchNode");var Ee=H,it=class it{constructor(e,t){this.processed=[];if(typeof e=="number"){this.index=e,this.correctionQueue=new k(Le,t);return}else this.index=e.index,this.processed=[].concat(e.processed),this.correctionQueue=new k(e.correctionQueue)}increaseMaxEditDistance(){let e=this.correctionQueue.toArray();e.forEach(function(t){t.calculation=t.calculation.increaseMaxDistance()}),this.correctionQueue=new k(Le,e)}};h(it,"SearchSpaceTier");var pe=it,ot=class ot{constructor(e){this.resultNode=e}get inputSequence(){return this.resultNode.priorInput}get matchSequence(){return this.resultNode.calculation.matchSequence}get matchString(){return this.resultNode.resultKey}get knownCost(){return this.resultNode.knownCost}get inputSamplingCost(){return this.resultNode.inputSamplingCost}get totalCost(){return this.resultNode.currentCost}get finalTraversal(){return this.resultNode.currentTraversal}};h(ot,"SearchResult");var de=ot,q=class q{constructor(e){this.tierOrdering=[];this.inputSequence=[];this.minInputCost=[];this.returnedValues={};this.processedEdgeSet={};if(this.buildQueueSpaceComparator(),e instanceof q){this.inputSequence=[].concat(e.inputSequence),this.minInputCost=[].concat(e.minInputCost),this.rootNode=e.rootNode,this.completedPaths=[].concat(e.completedPaths),this.returnedValues=A({},e.returnedValues),this.processedEdgeSet=A({},e.processedEdgeSet),this.tierOrdering=e.tierOrdering.map(n=>new pe(n)),this.selectionQueue=new k(this.QUEUE_SPACE_COMPARATOR,this.tierOrdering);return}let t=e;if(t){if(!t.traverseFromRoot)throw"The provided model does not implement the \`traverseFromRoot\` function, which is needed to support robust correction searching."}else throw"The LexicalModel parameter must not be null / undefined.";this.selectionQueue=new k(this.QUEUE_SPACE_COMPARATOR),this.rootNode=new Ee(t.traverseFromRoot(),t.toKey?t.toKey.bind(t):null),this.completedPaths=[this.rootNode];let r=new pe(0,[this.rootNode]);this.tierOrdering.push(r),this.selectionQueue.enqueue(r)}buildQueueSpaceComparator(){let e=this;this.QUEUE_SPACE_COMPARATOR=function(t,r){let n=t.correctionQueue.peek(),i=r.correctionQueue.peek(),s=t.index,l=r.index,a=0,u=1;if(l0:!1}handleNextNode(){if(!this.hasNextMatchEntry())return{type:"none"};let e=this.selectionQueue.dequeue(),t=e.correctionQueue.dequeue(),r={type:"intermediate",cost:t.currentCost};if(this.processedEdgeSet[t.pathKey])return this.selectionQueue.enqueue(e),r;this.processedEdgeSet[t.pathKey]=!0;let n=!1;if(t.knownCost>2)return r;t.knownCost==2&&(n=!0);let i=0;for(let s=0;s<=e.index;s++)i+=this.minInputCost[s];if(t.currentCost>i+2.5*q.EDIT_DISTANCE_COST_SCALE)return r;if(!n){let s=t.buildInsertionEdges();e.correctionQueue.enqueueAll(s)}if(e.index==this.tierOrdering.length-1)return this.completedPaths.push(t),this.selectionQueue.enqueue(e),{type:"complete",cost:t.currentCost,finalNode:t};{let s=this.tierOrdering[e.index+1],l=s.index,a=[];n||(a=t.buildDeletionEdges(this.inputSequence[l-1]));let u=t.buildSubstitutionEdges(this.inputSequence[l-1]);s.correctionQueue.enqueueAll(a.concat(u)),this.selectionQueue=new k(this.QUEUE_SPACE_COMPARATOR,this.tierOrdering)}return r}getBestMatches(e){return Ct(this,null,function*(){let t={},r=Object.values(this.returnedValues);if(r.length>0){let n=new k(Le,r);for(;n.count>0;){let i=e.time(()=>{let s=n.dequeue();return s.isFullReplacement?null:(t[s.resultKey]=s,new de(s))},0);if(i){let s=e.start(1);yield i,s.end(),e.timeSinceLastDefer>Me&&(yield new be(e.defer()))}}}do{let n=e.time(()=>{var s,l;let i=this.handleNextNode();if(i.type=="none")return null;if(i.type=="complete"){if(i.finalNode.isFullReplacement)return null;let u=i.finalNode;if(((l=(s=t[u.resultKey])==null?void 0:s.currentCost)!=null?l:Number.MAX_VALUE)>u.currentCost)return t[u.resultKey]=u,this.returnedValues[u.resultKey]=u,new de(u)}return null},2);if(n){let i=e.start(1);yield n,i.end()}e.timeSinceLastDefer>Me&&(yield new be(e.defer()))}while(!e.elapsed&&this.hasNextMatchEntry());return null})}};h(q,"SearchSpace"),q.EDIT_DISTANCE_COST_SCALE=5,q.MIN_KEYSTROKE_PROBABILITY=1e-4,q.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL=33;var v=q;var st=class st{static isWhitespace(e){let t=/^[\\u0009\\u000A\\u000D\\u0020\\u00a0\\u1680\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u200b\\u2028\\u2029\\u202f\\u205f\\u3000]+$/i;return e.insert.match(t)!=null}static isBackspace(e){return e.insert==""&&e.deleteLeft>0&&!e.deleteRight}static isEmpty(e){return e.insert==""&&e.deleteLeft==0&&!e.deleteRight}};h(st,"TransformUtils");var w=st;var $t={quotesForKeepSuggestion:{open:"“",close:"”"},insertAfterWord:" "};function me(o){let e=$t;if(!o.punctuation)return e;let t=o.punctuation,r=t.insertAfterWord;r!==""&&!r&&(r=e.insertAfterWord);let n=t.quotesForKeepSuggestion;n||(n=e.quotesForKeepSuggestion);let i=t.isRTL;return{insertAfterWord:r,quotesForKeepSuggestion:n,isRTL:i}}h(me,"determinePunctuationFromModel");function B(o){return e=>{if(o.wordbreaker||!o.wordbreak){let t=o.wordbreaker||R;return X(t,e)}else return o.wordbreak(e)}}h(B,"determineModelWordbreaker");function $(o){return e=>o.wordbreaker?se(o.wordbreaker,e):null}h($,"determineModelTokenizer");function It(o,e){var i;let t=o,n=B(o)(e);if(!t.languageUsesCasing)throw"Invalid attempt to detect casing: languageUsesCasing is set to false";if(!t.applyCasing)throw"Invalid LMLayer state: languageUsesCasing is set to true, but no applyCasing function exists";return e.casingForm=="upper"||e.casingForm=="initial"?e.casingForm:t.applyCasing("lower",n)==n?"lower":t.applyCasing("upper",n)==n?n.kmwLength()>1?"upper":"initial":t.applyCasing("initial",n)==n?"initial":(i=e.casingForm)!=null?i:null}h(It,"detectCurrentCasing");function at(o,e,t){let r=g(t,e),n=o(r).left,i=t.insert,s=[];for(let l=n.length-1;l>=0;l--){let a=n[l],u=a.text.length;if(u({sample:at(o,e,r.sample),p:r.p}))}h(Nt,"tokenizeTransformDistribution");function At(o,e){let t=[];for(let r=0;r0&&this.transformDistributions.push(e),this.raw=t}};h(ht,"TrackedContextToken");var z=ht,Pe=class Pe{constructor(e){this.searchSpace=[];if(e instanceof Pe){let t=e;this.tokens=t.tokens.map(function(n){let i=new z;return i.raw=n.raw,i.replacements=[].concat(n.replacements),i.activeReplacementId=n.activeReplacementId,i.transformDistributions=[].concat(n.transformDistributions),n.replacementText&&(i.replacementText=n.replacementText),i}),this.indexOffset=0;let r=this.model=e.model;this.taggedContext=e.taggedContext,r!=null&&r.traverseFromRoot&&(this.searchSpace=e.searchSpace.map(n=>new v(n)))}else{let t=e;this.tokens=[],this.indexOffset=Number.MIN_SAFE_INTEGER,this.model=t,t&&t.traverseFromRoot&&(this.searchSpace=[new v(t)])}}get head(){return this.tokens[0]}get tail(){return this.tokens[this.tokens.length-1]}popHead(){this.tokens.splice(0,1),this.indexOffset-=1}pushTail(e){this.model&&this.model.traverseFromRoot?this.searchSpace=[new v(this.model)]:this.searchSpace=[],this.tokens.push(e);let t=this;t.searchSpace.length>0&&e.transformDistributions.forEach(r=>t.searchSpace[0].addInput(r))}toRawTokenization(){let e=[];for(let t of this.tokens)t.currentText!==null&&e.push(t.currentText);return e}};h(Pe,"TrackedContextState");var V=Pe,fe=class fe{constructor(e=fe.DEFAULT_ARRAY_SIZE){this.currentHead=0;this.currentTail=0;this.circle=Array(e)}get count(){let e=this.currentHead-this.currentTail;return e<0&&(e=e+this.circle.length),e}get maxCount(){return this.circle.length}get oldest(){if(this.count!=0)return this.item(0)}get newest(){if(this.count!=0)return this.item(this.count-1)}enqueue(e){var t=null;let r=(this.currentHead+1)%this.maxCount;return r==this.currentTail&&(t=this.circle[this.currentTail],this.currentTail=(this.currentTail+1)%this.maxCount),this.circle[this.currentHead]=e,this.currentHead=r,t}dequeue(){if(this.currentTail==this.currentHead)return null;{let e=this.circle[this.currentTail];return this.currentTail=(this.currentTail+1)%this.maxCount,e}}popNewest(){if(this.currentTail==this.currentHead)return null;{let e=this.circle[this.currentHead];return this.currentHead=(this.currentHead-1+this.maxCount)%this.maxCount,e}}item(e){if(e>=this.count)return;let t=(this.currentTail+e)%this.maxCount;return this.circle[t]}};h(fe,"CircularArray"),fe.DEFAULT_ARRAY_SIZE=5;var ut=fe,ge=class ge extends ut{static attemptMatchContext(e,t,r){var C,x;let n=t.toRawTokenization(),s=K.computeDistance(n.map(m=>({key:m})),e.map(m=>({key:m.text})),3).editPath();s.length==2&&s[0]=="insert"&&s[1]=="substitute"&&(s[0]="substitute",s[1]="insert");let l=s.indexOf("match"),a=s.lastIndexOf("match");if(s.length>=2&&s[s.length-2]=="substitute"&&s[s.length-1]=="match"&&(a=s.lastIndexOf("match",s.length-2)),l){for(let m=l+1;m({sample:G.sample[y],p:G.p})),M=f?(C=L[0])==null?void 0:C.sample:null;M&&M.insert==""&&M.deleteLeft==0&&!M.deleteRight&&(M=null),b||(d=d?U(d,M):M);let qt=M&&w.isBackspace(M),te=e[m-p];switch(s[m]){case"substitute":b&&(u=new V(u));let G=u.tokens[m-p],S=t.tokens[m];qt?(G.updateWithBackspace(te.text,M.id),b&&(u.tokens.pop(),u.pushTail(G))):(G.update(L,te.text),b&&((x=u.searchSpace[0])==null||x.addInput(L))),u!=t&&!b&&(S.replacementText=te.text);break;case"insert":if(c&&c!="substitute"&&c!="match"&&c!="insert")return null;d||(d={insert:"",deleteLeft:0}),u==t&&(u=new V(u));let E=new z;E.raw=te.text,M&&(E.transformDistributions=L?[L]:[]),E.isWhitespace=te.isWhitespace,u.pushTail(E);break;case"match":if(c=="substitute"&&e[e.length-1].text=="")continue;default:return null}c=s[m]}return{state:u,baseState:t,preservationTransform:d}}static modelContextState(e,t){let r=e.map(function(i){let s=new z;return s.raw=i.text,i.isWhitespace&&(s.isWhitespace=!0),s.raw?s.transformDistributions=At(s.raw).map(function(l){return[{sample:l,p:1}]}):s.transformDistributions=[],s}),n=new V(t);for(;r.length>0;)r.length==1&&r[0].updateWithBackspace(r[0].raw,null),n.pushTail(r.splice(0,1)[0]);if(n.tokens.length==0){let i=new z;i.raw="",n.pushTail(i)}return n}analyzeState(e,t,r){if(!e.traverseFromRoot)throw"This lexical model does not provide adequate data for correction algorithms and context reuse";let n=$(e),i=r==null?void 0:r[0],s=0,l=null;i&&(s=at(n,t,i.sample).length,l=Nt(n,t,r),t=g(i.sample,t),l=l.filter(c=>c.sample.length==s));let a=n(t);if(a.left.length>0)for(let c=this.count-1;c>=0;c--){let p=this.item(c),f=p.taggedContext;if(f&&r&&r.length>0){if(g(r[0].sample,f).left!=t.left)continue}else if((f==null?void 0:f.left)!=t.left)continue;let d=ge.attemptMatchContext(a.left,this.item(c),l);if(d!=null&&d.state)return this.newest!=d.state&&this.newest!=p&&this.enqueue(p),d.state.taggedContext=t,d.state!=this.item(c)&&this.enqueue(d.state),d}let u=ge.modelContextState(a.left,e);return u.taggedContext=t,this.enqueue(u),{state:u,baseState:null}}clearCache(){for(;this.count>0;)this.dequeue()}};h(ge,"ContextTracker");var Te=ge;var Zt=.66,Ot={MAX_SEARCH_THRESHOLD:8,REPLACEMENT_SEARCH_THRESHOLD:4};function pt(o,e){let t=e.matchLevel-o.matchLevel;return t!=0?t:e.totalProb-o.totalProb}h(pt,"tupleDisplayOrderSort");function _t(o,e,t,r,n){return D(this,null,function*(){let i=B(e),s=r[0].sample,l=g(s,n),a=[];if(!o){let S,E=w.isWhitespace(s),Ae=w.isBackspace(s);return E?S=[{sample:s,p:1}]:S=r.map(P=>{let Se=P.sample;return w.isWhitespace(Se)&&!E||w.isBackspace(Se)&&!Ae?null:P}),S=S.filter(P=>!!P),a=Rt(e,S,n),E&&a.forEach(P=>P.preservationTransform=s),{postContextState:null,rawPredictions:a}}let{state:u}=o.analyzeState(e,n,null),c=o.analyzeState(e,n,w.isEmpty(s)?null:r),p=c.state,f=p.searchSpace[0],d=0,C=p.tokens,x=C.length-u.tokens.length;c.preservationTransform?(d=0,n=g(c.preservationTransform,n)):x<0?d=i(l).kmwLength()+s.deleteLeft:d=i(n).kmwLength();let m=C[C.length-1];m.raw==""&&(d=0);let b=m.transformDistributions.length<=1,y,L={};try{for(var M=St(f.getBestMatches(t)),qt,te,G;qt=!(te=yield M.next()).done;qt=!1){let S=te.value;let E=S.matchString;if(S.matchSequence.length==0&&S.inputSequence.length!=0||S.matchSequence.length!=0&&S.matchSequence.length==S.knownCost)continue;let Ae={insert:E,deleteLeft:d,id:s.id},P=S.totalCost;b&&(P*=Z.SINGLE_CHAR_KEY_PROB_EXPONENT);let Se={sample:Ae,p:Math.exp(-P)},xe=Rt(e,[Se],n);xe.forEach(re=>re.preservationTransform=c.preservationTransform),xe.length>0&&y===void 0&&(y=P);let mt=L[S.matchString];if(mt&&(a=a.filter(re=>!mt.find(Bt=>re.prediction.sample==Bt.sample))),L[S.matchString]=xe.map(re=>re.prediction),a=a.concat(xe),Jt(y,S.totalCost,a))break}}catch(te){G=[te]}finally{try{qt&&(te=M.return)&&(yield te.call(M))}finally{if(G)throw G[0]}}return{postContextState:p,rawPredictions:a}})}h(_t,"correctAndEnumerate");function Jt(o,e,t){if(e>=o+Ot.MAX_SEARCH_THRESHOLD)return!0;if(t.length>=Z.MAX_SUGGESTIONS){if(e>=o+Ot.REPLACEMENT_SEARCH_THRESHOLD)return!0;if(t.sort(pt),t[Z.MAX_SUGGESTIONS-1].totalProb>Math.exp(-e))return!0}return!1}h(Jt,"shouldStopSearchingEarly");function Rt(o,e,t){let r=[],n=B(o);for(let i of e){let s=o.predict(i.sample,t),{sample:l,p:a}=i,u=n(g(i.sample,t)),c=s.map(p=>(l.id!==void 0&&(p.sample.transformId=l.id),{prediction:p,correction:{sample:u,p:a},totalProb:p.p*a,matchLevel:0}));r=r.concat(c)}return r}h(Rt,"predictFromCorrections");function Dt(o,e,t){let r=B(o),n={},i=[];for(let s of e){let l=r(g(s.prediction.sample.transform,t)),a=n[l];a?a.totalProb+=s.totalProb:n[l]=s}for(let s in n){let l=n[s];i.push(l)}return i}h(Dt,"dedupeSuggestions");function Ut(o,e,t,r){let{sample:n,p:i}=r,s=B(o),l=g(n,t),a=s(l),u=h(m=>o.toKey?o.toKey(m):m,"keyed"),c=h(m=>o.applyCasing?o.applyCasing("lower",m):m,"keyCased"),p=u(a),f=c(a),d;for(let m of e){n.id!==void 0&&(m.prediction.sample.transformId=n.id);let b=s(g(m.prediction.sample.transform,t));u(m.correction.sample)==p?b==a?(m.matchLevel=3,d=Ie(o,m.prediction.sample,"keep",j.noQuotes),d.matchesModel=!0,Object.assign(m.prediction.sample,d),d=m.prediction.sample):c(b)==f?m.matchLevel=2:u(b)==p?m.matchLevel=1:m.matchLevel=0:m.matchLevel=0}if(d||a=="")return;let C=A({},n),x=W(C,1);x.displayAs=a,d=Ie(o,x,"keep"),n.id!==void 0&&(d.transformId=n.id),d.matchesModel=!1,e.unshift({totalProb:d.p,prediction:{sample:d,p:d.p},correction:{sample:a,p:i},matchLevel:3})}h(Ut,"processSimilarity");function Ft(o){if(o.length==0)return;let e=o[0].prediction.sample;if(e.tag=="keep"&&e.matchesModel){e.autoAccept=!0;return}else if(o.length==1)return;if(o=o.slice(1),o.length==1){o[0].prediction.sample.autoAccept=!0;return}let t=o[0];if(t.correction.sample.length==0||o.reduce((a,u)=>(a==null?void 0:a.correction.p)>u.correction.p?a:u,null).correction.p>t.correction.p)return;let i=t.matchLevel,s=o.reduce((a,u)=>a+(u.matchLevel==i?u.totalProb:0),0);t.totalProb/s{let u=a.prediction;if(a.preservationTransform){let c=U(a.preservationTransform,u.sample.transform);c.id=u.sample.transformId;let p=u.sample;p.transform=c}return n?Re(A({},u.sample),{p:a.totalProb,"lexical-p":u.p,"correction-p":a.correction.p}):Re(A({},u.sample),{p:a.totalProb})});return l.forEach(a=>{let u=s(t);u&&u.caretSplitsToken?a.transform.insert+=i.insertAfterWord:t.right?i.insertAfterWord!=""&&t.right.indexOf(i.insertAfterWord)!=0&&(a.transform.insert+=i.insertAfterWord):a.transform.insert+=i.insertAfterWord}),l}h(Wt,"finalizeSuggestions");function Ie(o,e,t,r=j.default){let n=j,i=me(o),s=n.noQuotes;(t=="keep"||t=="revert")&&(s=n.useQuotes);let l={transform:e.transform,displayAs:n.apply(r,e.displayAs,i,s),tag:t,p:e.p};return e.transformId!==void 0&&(l.transformId=e.transformId),l}h(Ie,"toAnnotatedSuggestion");var J=class J{constructor(e,t){this.SUGGESTION_ID_SEED=0;this.testMode=!1;this.verbose=!0;this.lexicalModel=e,e.traverseFromRoot&&(this.contextTracker=new Te),this.punctuation=me(e),this.testMode=!!t}predict(e,t){return D(this,null,function*(){var b;let r=this.lexicalModel;(b=this.activeTimer)==null||b.terminate(),e instanceof Array?e.length==0&&e.push({sample:{insert:"",deleteLeft:0},p:1}):e=[{sample:e,p:1}],e.sort(function(y,L){return L.p-y.p});let n=e[0].sample,i=w.isBackspace(n),s=w.isWhitespace(n),l=g(n,t),a=this.wordbreak(l),u=i||s?a:this.wordbreak(t),c=r.languageUsesCasing?It(r,l):null,p=v.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL,f=this.activeTimer=new he(this.testMode?Number.MAX_VALUE:p,this.testMode?Number.MAX_VALUE:p*1.5),{postContextState:d,rawPredictions:C}=yield _t(this.contextTracker,this.lexicalModel,f,e,t);this.activeTimer==f&&(this.activeTimer=null);for(let y of C)c&&c!="lower"&&this.applySuggestionCasing(y.prediction.sample,u,c);let x=Dt(this.lexicalModel,C,t);Ut(this.lexicalModel,x,t,e[0]),x.sort(pt),Ft(x);let m=Wt(this.lexicalModel,x.splice(0,J.MAX_SUGGESTIONS),t,n,this.verbose);return m.forEach(y=>{y.id=this.SUGGESTION_ID_SEED,this.SUGGESTION_ID_SEED++}),d&&(d.tail.replacements=m.map(function(y){return{suggestion:y,tokenWidth:1}})),m})}applySuggestionCasing(e,t,r){let n=t.kmwLength()-e.transform.deleteLeft;n>0&&(e.transform.deleteLeft+=n,e.transform.insert=t.kmwSubstr(0,n)+e.transform.insert),e.transform.insert=this.lexicalModel.applyCasing(r,e.transform.insert),e.displayAs=this.lexicalModel.applyCasing(r,e.displayAs)}acceptSuggestion(e,t,r){let n=e.transform,i=t.left.kmwSubstr(-n.deleteLeft,n.deleteLeft),s=n.insert.kmwLength(),l={insert:i,deleteLeft:s},a=t;r&&(l=U(l,r),a=g(r,a));let u,c=this.tokenize(a);if(c){let d=c.left[c.left.length-1];u=d&&!d.isWhitespace?d.text:"",u+=c.caretSplitsToken?c.right[0].text:""}else u=this.wordbreak(a);let p=W(l);p.displayAs=u;let f=Ie(this.lexicalModel,p,"revert");if(e.transformId!=null&&(f.transformId=-e.transformId),e.id!=null?f.id=-e.id:(f.id=-this.SUGGESTION_ID_SEED,this.SUGGESTION_ID_SEED++),this.contextTracker){let d=this.contextTracker.newest;d||(d=this.contextTracker.analyzeState(this.lexicalModel,t).state),d.tail.activeReplacementId=e.id;let C=g(e.transform,t);this.contextTracker.analyzeState(this.lexicalModel,C)}return f}applyReversion(e,t){return D(this,null,function*(){let r=this,n=h(function(){return D(this,null,function*(){let l=g(e.transform,t),a=yield r.predict({insert:"",deleteLeft:0},l);return a.forEach(function(u){u.transformId=-e.transformId,u.autoAccept=!1}),a})},"fallbackSuggestions");if(!this.contextTracker)return n();let i=!1;for(let l=this.contextTracker.count-1;l>=0;l--)if(this.contextTracker.item(l).tail.activeReplacementId==-e.id){i=!0;break}if(!i)return n();for(;this.contextTracker.newest.tail.activeReplacementId!=-e.id;)this.contextTracker.popNewest();this.contextTracker.newest.tail.revert();let s=this.contextTracker.newest.tail.replacements.map(function(l){return l.suggestion});return s.forEach(function(l){l.transformId=-e.transformId,l.autoAccept=!1}),s})}wordbreak(e){return B(this.lexicalModel)(e)}tokenize(e){return $(this.lexicalModel)(e)}resetContext(e){var t;(t=this.activeTimer)==null||t.terminate(),this.contextTracker&&(this.contextTracker.clearCache(),this.contextTracker.analyzeState(this.lexicalModel,e,null))}};h(J,"ModelCompositor"),J.MAX_SUGGESTIONS=12,J.SINGLE_CHAR_KEY_PROB_EXPONENT=16;var dt=J,Z=dt;O();var Ne=class Ne{constructor(e={importScripts:null,postMessage:null}){this._testMode=!1;this._postMessage=e.postMessage||postMessage,this._importScripts=e.importScripts||importScripts,this.setupConfigState()}error(e,t){this.cast("error",{log:e,error:t&&t.stack?t.stack:void 0})}onMessage(e){let{message:t}=e.data;if(!t)throw new Error(\`Missing required 'message' property: \${e.data}\`);let r=e.data;if(r.message=="load"){let n=r,i=!1;if(this._currentModelSource&&n.source.type==this._currentModelSource.type&&(n.source.type=="file"&&n.source.file==this._currentModelSource.file||n.source.type=="raw"&&n.source.code==this._currentModelSource.code)&&(i=!0),i){typeof console!="undefined"&&console.warn("Duplicate model load message detected - squashing!");return}else this._currentModelSource=n.source}else r.message=="unload"&&(this._currentModelSource=null);this.state.handleMessage(r)}cast(e,t){let r=this._postMessage;r(A({message:e},t))}loadModel(e){try{let t=e.configure(this._platformCapabilities);t.leftContextCodePoints||(t.leftContextCodePoints=t.leftContextCodeUnits),t.rightContextCodePoints||(t.rightContextCodePoints=t.rightContextCodeUnits),t.leftContextCodePoints||(t.leftContextCodePoints=this._platformCapabilities.maxLeftContextCodePoints),t.rightContextCodePoints||(t.rightContextCodePoints=this._platformCapabilities.maxRightContextCodePoints||0),e.languageUsesCasing&&!e.applyCasing&&(e.applyCasing=we);let r=this.transitionToReadyState(e);t.wordbreaksAfterSuggestions===void 0&&(t.wordbreaksAfterSuggestions=r.punctuation.insertAfterWord!=""),this.cast("ready",{configuration:t})}catch(t){this.error("loadModel failed!",t)}}loadModelFile(e){try{this._importScripts(e)}catch(t){this.error("Error occurred when attempting to load dictionary",t)}}unloadModel(){this.transitionToLoadingState()}setupConfigState(){this.state={name:"unconfigured",handleMessage:e=>{if(e.message!=="config")throw new Error(\`invalid message; expected 'config' but got \${e.message}\`);this._platformCapabilities=e.capabilities,this._testMode=!!e.testMode,this.transitionToLoadingState()}}}transitionToLoadingState(){let e=this;this.state={name:"modelless",handleMessage:t=>{if(t.message!=="load")throw new Error(\`invalid message; expected 'load' but got \${t.message}\`);if(t.source.type=="file")e.loadModelFile(t.source.file);else{let r=t.source.code;new Function("LMLayerWorker","models","correction","wordBreakers",r)(e,ve,Ce,le)}}}}transitionToReadyState(e){let t=new Z(e,this._testMode);return this.state={name:"ready",handleMessage:r=>{switch(r.message){case"predict":var{transform:n,context:a}=r;t.predict(n,a).then(c=>{this.cast("suggestions",{token:r.token,suggestions:c})});break;case"wordbreak":let u=X(e.wordbreaker||R,r.context);this.cast("currentword",{token:r.token,word:u});break;case"unload":this.unloadModel();break;case"accept":var{suggestion:i,context:a,postTransform:s}=r,l=t.acceptSuggestion(i,a,s);this.cast("postaccept",{token:r.token,reversion:l});break;case"revert":var{reversion:l,context:a}=r;t.applyReversion(l,a).then(c=>{this.cast("postrevert",{token:r.token,suggestions:c})});break;case"reset-context":var{context:a}=r;t.resetContext(a);break;default:throw new Error(\`invalid message; expected one of {'predict', 'wordbreak', 'accept', 'revert', 'reset-context', 'unload'} but got \${r.message}\`)}},compositor:t},t}static install(e){let t=new Ne({postMessage:e.postMessage,importScripts:e.importScripts.bind(e)});return e.onmessage=t.onMessage.bind(t),t.self=e,e.LMLayerWorker=t,e.models=ve,e.correction=Ce,e.wordBreakers=le,t}};h(Ne,"LMLayerWorker");var ee=Ne;typeof self!="undefined"&&"postMessage"in self&&"importScripts"in self?ee.install(self):window.LMLayerWorker=ee;})(); +//# sourceMappingURL=worker-main.min.js.map +`,Zo="";var rr=class rr{static constructInstance(){return new Worker(this.asBlobURI(xo))}static asBlobURI(t){let e=ss(t);e+=` +`+Zo;let n=new Blob([e],{type:"text/javascript"});return URL.createObjectURL(n)}};o(rr,"DefaultWorker");var nn=rr;var cr=class cr extends S.default{constructor(e,n,i=!1){super();this._mayPredict=!0;this._mayCorrect=!0;this._state="inactive";this.recentTranscriptions=n;let s={maxLeftContextCodePoints:64,maxRightContextCodePoints:i?0:64};e&&(this.lmEngine=new tn(s,e))}get activeModel(){return this.currentModel}get isConfigured(){return!!this.configuration}get state(){return this._state}unloadModel(){this.lmEngine.unloadModel(),delete this.currentModel,delete this.configuration,this._state="inactive",this.emit("statechange","inactive")}loadModel(e){if(!e)throw new Error("Null reference not allowed.");let n=e.path?"file":"raw",i=n=="file"?e.path:e.code;return this.currentModel=e,this.mayPredict&&(this._state="active",this.emit("statechange","active")),this.lmEngine.loadModel(i,n).then(s=>{this.configuration=s,this.mayPredict&&(this._state="configured",this.emit("statechange","configured"))}).catch(s=>{let l;s instanceof Error?l=s.message:l=String(s),console.error("Could not load model '"+e.id+"': "+l),this.currentModel=null,this._state="inactive",this.emit("statechange","inactive")})}invalidateContext(e,n){if(this.emit("invalidatesuggestions","context"),!this.currentModel||!this.configuration)return Promise.resolve([]);if(this.isActive)if(e){let i=e.buildTranscriptionFrom(e,null,!1);return this.predict_internal(i,!0,n)}else return Promise.resolve([]);else return Promise.resolve([])}wordbreak(e,n){if(!this.isActive)return null;let i=new Ue(k.from(e,!1),this.configuration,n);return this.lmEngine.wordbreak(i)}predict(e,n){return!this.isActive||!this.currentModel||!this.configuration?null:(this.emit("invalidatesuggestions","new"),this.predict_internal(e,!1,n))}applySuggestion(e,n,i){if(!n)throw"Accepting suggestions requires a destination OutputTarget instance.";if(!this.isConfigured)return console.warn("Could not apply suggestion; the corresponding model has been unloaded"),null;let s=this.getPredictionState(e.transformId);if(s){let l=k.from(s.preInput,!1);l.apply(e.transform);let r=l.buildTransformFrom(n);n.apply(r),this.emit("suggestionapplied",n),k.from(s.preInput,!1).apply(s.transform);let g=new Ue(s.preInput,this.configuration,i()),d=this.lmEngine.acceptSuggestion(e,g,s.transform);return d=d.then(u=>{let I={transform:s.transform,transformId:-s.token,displayAs:u.displayAs,id:u.id,tag:u.tag};return this.predictFromTarget(n,i()),I}),d}else return console.warn("Could not apply the Suggestion!"),null}applyReversion(e,n){if(!n)throw"Accepting suggestions requires a destination OutputTarget instance.";let i=this.getPredictionState(-e.transformId);if(!i)return console.warn("Could not apply the Suggestion!"),Promise.resolve([]);let s=k.from(i.preInput,!1);s.apply(e.transform);let l=s.buildTransformFrom(n);n.apply(l);let r=this.currentPromise=this.lmEngine.revertSuggestion(e,new Ue(i.preInput,this.configuration,null));return r.then(()=>this.currentPromise=this.currentPromise==r?null:this.currentPromise),r}predictFromTarget(e,n){if(!e)return null;let i=e.buildTranscriptionFrom(e,null,!1);return this.predict(i,n)}predict_internal(e,n,i){if(!e)return null;let s=new Ue(e.preInput,this.configuration,i);this.recordTranscription(e),n&&this.lmEngine.resetContext(s);let l=e.alternates;(!l||l.length==0)&&(l=[{sample:e.transform,p:1}]);let r=e.transform;var c=this.currentPromise=this.lmEngine.predict(l,s);return c.then(g=>{if(c==this.currentPromise){let d=new Ct(g,r.id);this.emit("suggestionsready",d),this.currentPromise=null}return g})}recordTranscription(e){this.recentTranscriptions.save(e)}getPredictionState(e){return this.recentTranscriptions.get(e)}shutdown(){this.lmEngine.shutdown(),this.removeAllListeners()}get isActive(){return this.canEnable()?(this.activeModel||!1)&&this._mayPredict:(this._mayPredict=!1,!1)}canEnable(){return!!this.lmEngine}get mayPredict(){return this._mayPredict}set mayPredict(e){if(!this.canEnable())return;let n=this._mayPredict;if(this._mayPredict=e,n!=e&&this.activeModel){let i=e?"active":"inactive";this._state=i,this.emit("statechange",i),e&&this.isConfigured&&(this._state="configured",this.emit("statechange","configured"))}}get mayCorrect(){return this._mayCorrect}set mayCorrect(e){this._mayCorrect=e}get wordbreaksAfterSuggestions(){return this.configuration.wordbreaksAfterSuggestions}tryAcceptSuggestion(e){var i;let n={shouldSwallow:!1};return this.emit("tryaccept",e,n),(i=n.shouldSwallow)!=null?i:!1}tryRevertSuggestion(){return this.emit("tryrevert"),!1}};o(cr,"LanguageProcessor");var Dn=cr;var Sa=10,or=class or{constructor(){this.map=new Map}get(t){let e=this.map.get(t);return e&&this.save(e),e}save(t){let e=t.token>=0?t.token:-t.token;this.map.delete(e),this.map.set(e,t),this.map.size>Sa&&this.map.delete(this.map.keys().next().value)}};o(or,"TranscriptionCache");var On=or;var Pn=class Pn{constructor(t,e,n){this.contextCache=new On;if(!t)throw new Error("device must be defined");n||(n=Pn.DEFAULT_OPTIONS),this.contextDevice=t,this.kbdProcessor=new jt(t,n),this.lngProcessor=new Dn(e,this.contextCache)}get languageProcessor(){return this.lngProcessor}get keyboardProcessor(){return this.kbdProcessor}get keyboardInterface(){return this.keyboardProcessor.keyboardInterface}get activeKeyboard(){return this.keyboardInterface.activeKeyboard}set activeKeyboard(t){this.keyboardInterface.activeKeyboard=t,this.resetContext()}get activeModel(){return this.languageProcessor.activeModel}processKeyEvent(t,e){let n=t.srcKeyboard&&this.activeKeyboard!=t.srcKeyboard,i=this.activeKeyboard;try{if(n&&(this.keyboardInterface.activeKeyboard=t.srcKeyboard),t.baseTranscriptionToken){let s=this.contextCache.get(t.baseTranscriptionToken);s?(!xt(s.transform)||!s.preInput.isEqual(k.from(e)))&&e.restoreTo(s.preInput):console.warn("The base context for the multitap could not be found")}return this._processKeyEvent(t,e)}finally{n&&(this.keyboardInterface.activeKeyboard=i)}}_processKeyEvent(t,e){var I;let n=t.device.formFactor,i=t.isSynthetic;if((n==V.FormFactor.Desktop||!this.activeKeyboard||this.activeKeyboard.usesDesktopLayoutOnDevice(t.device))&&i&&this.keyboardProcessor.selectLayer(t))return new ie;if(this.keyboardProcessor.doModifierPress(t,e,!i)&&!i)return new ie;if(this.languageProcessor.isActive){if((t.kName=="K_BKSP"||t.Lcode==C.keyCodes.K_BKSP)&&this.languageProcessor.tryRevertSuggestion())return new ie;if((t.kName=="K_SPACE"||t.Lcode==C.keyCodes.K_SPACE)&&this.languageProcessor.tryAcceptSuggestion("space"))return new ie}let s=k.from(e,!0),l=this.keyboardProcessor.layerId,r=this.keyboardProcessor.processKeystroke(t,e);t.kNextLayer&&this.keyboardProcessor.selectLayer(t);let c=C.isFrameKey(t.kName);xt((I=r==null?void 0:r.transcription)==null?void 0:I.transform)&&t.kNextLayer&&(c=!0);let g=r!=null;if(g){let B=c?null:this.buildAlternates(r,t,s);r.finalize(this.keyboardProcessor,e,!1),B&&B.length>0&&(r.transcription.alternates=B)}else r=new ie,r.transcription=e.buildTranscriptionFrom(e,null,!1),r.triggersDefaultCommand=!0;this.contextCache.save(r.transcription);let d=r.setStore[33]||t.kNextLayer;this.keyboardProcessor.newLayerStore.set(d?this.keyboardProcessor.layerId:""),this.keyboardProcessor.oldLayerStore.set(d?l:"");let u=this.keyboardProcessor.processPostKeystroke(this.contextDevice,e);return u&&u.finalize(this.keyboardProcessor,e,!0),r.predictionPromise=this.languageProcessor.predict(r.transcription,this.keyboardProcessor.layerId),r.triggersDefaultCommand||e.doInputEvent(),g?r:null}buildAlternates(t,e,n){let i;if(this.languageProcessor.isActive&&!t.triggersDefaultCommand){let s=e.keyDistribution,r=new Ue(n,Ue.ENGINE_RULE_WINDOW,this.keyboardProcessor.layerId).toMock();if(this.languageProcessor.isActive&&s&&e.kbdLayer){let c=Number.MAX_VALUE,g=Et(),d;g.performance&&g.performance.now&&(d=o(function(){return g.performance.now()},"timer"),c=d()+16);let u=Math.exp(-5);s.sort((B,h)=>h.p-B.p),i=[];let I=0;for(let B of s){if(B.p=c)break;let h=k.from(r,!1),b=B.keySpec;if(!b){console.warn("Internal error: failed to properly filter set of keys for corrections");continue}let F=this.keyboardProcessor.activeKeyboard.constructKeyEvent(b,e.device,this.keyboardProcessor.stateKeys),G=this.keyboardProcessor.processKeystroke(F,h);if(G&&!G.beep&&B.p>0){let U=G.transcription.transform;U.id=t.transcription.token,i.push({sample:U,p:B.p}),I+=B.p}}i.forEach(function(B){B.p/=I})}}return i}resetContext(t){this.keyboardProcessor.resetContext(t),this.languageProcessor.invalidateContext(t,this.keyboardProcessor.layerId)}};o(Pn,"InputProcessor"),Pn.DEFAULT_OPTIONS={baseLayout:"us"};var jn=Pn;var ar=class ar{constructor(t,e,n,i){this.Pelem=t,this.Peventname=e.toLowerCase(),this.Phandler=n,this.PuseCapture=i}equals(t){return this.Pelem==t.Pelem&&this.Peventname==t.Peventname&&this.Phandler==t.Phandler&&this.PuseCapture==t.PuseCapture}};o(ar,"DomEventTracking");var ls=ar,gr=class gr{constructor(){this.domEvents=[]}attachDOMEvent(t,e,n,i){this.detachDOMEvent(t,e,n,i),t.addEventListener(e,n,!!i);var s=new ls(t,e,n,i);this.domEvents.push(s)}detachDOMEvent(t,e,n,i){t.removeEventListener(e,n,i);for(var s=new ls(t,e,n,i),l=0;l{let l=n.apply(e,[i,s]);return this.emit(t,i),l}}};o(dr,"EmitterListenerSpy");var sn=dr;var ur=class ur{constructor(){this.events={};this.currentEvents=[]}addEventListener(t,e){return this._removeEventListener(t,e),this.events[t].push(e),!0}removeEventListener(t,e){return this._removeEventListener(t,e)}_removeEventListener(t,e){typeof this.events[t]=="undefined"&&(this.events[t]=[]);for(var n=0;n{e.reject(new Error(Co))},1e4),i="&timerid="+n,s=t+i,l=document.createElement("script");l.onload=r=>{window.clearTimeout(n),e.isResolved||e.reject(new Error(Go))},l.onerror=(r,c,g,d,u)=>{window.clearTimeout(n);let I=zl;u&&(I=I+": "+u.message),e.reject(new Error(I))},this.fileLocal?l.src=t:l.src=s;try{document.body.appendChild(l)}catch(r){document.getElementsByTagName("head")[0].appendChild(l)}return e.finally(()=>{clearTimeout(n)}),{promise:e,queryId:n}}};o(Br,"DOMCloudRequester");var _n=Br;function Va(){return typeof window.KeymanWeb_BaseLayout!="undefined"?window.KeymanWeb_BaseLayout:"us"}o(Va,"determineBaseLayout");var Ir=class Ir{constructor(t,e,n,i){this.legacyAPIEvents=new ln;this.keyEventListener=o((t,e)=>{var s;let n=this.contextManager.activeTarget;if(!this.contextManager.activeKeyboard||!n){e&&e(null,null);return}if(this.core.languageProcessor.mayCorrect||(t.keyDistribution=[]),this.keyEventRefocus&&this.keyEventRefocus(),n.invalidateSelection(),n.deadkeys().deleteMatched(),t.isSynthetic){let l=this.osk.vkbd.layerId;l&&l!=this.core.keyboardProcessor.layerId&&(this.core.keyboardProcessor.layerId=l)}let i=this.core.processKeyEvent(t,n);i&&((s=i.transcription)!=null&&s.transform)&&this.config.onRuleFinalization(i,this.contextManager.activeTarget),e&&e(i,null)},"keyEventListener");this.config=e,this.contextManager=n;let s=i(this);s.baseLayout=Va(),this.interface=s.keyboardInterface,this.core=new jn(e.hostDevice,t,s),this.core.languageProcessor.on("statechange",l=>{var r,c;(r=this.osk)==null||r.bannerController.selectBanner(l),(c=this.osk)==null||c.refreshLayout()}),this.core.keyboardProcessor.on("statekeychange",l=>{var r,c;(c=(r=this.osk)==null?void 0:r.vkbd)==null||c.updateStateKeys(l)}),this.contextManager.on("beforekeyboardchange",l=>{this.legacyAPIEvents.callEvent("beforekeyboardchange",{internalName:l==null?void 0:l.id,languageCode:l==null?void 0:l.langId})}),this.contextManager.on("keyboardchange",l=>{l||this.osk.startHide(!1);let r=o(()=>{var c,g;this.refreshModel(),this.core.activeKeyboard=l==null?void 0:l.keyboard,this.legacyAPIEvents.callEvent("keyboardchange",{internalName:(c=l==null?void 0:l.metadata.id)!=null?c:"",languageCode:(g=l==null?void 0:l.metadata.langId)!=null?g:""})},"prepareKeyboardSwap");this.osk?this.osk.batchLayoutAfter(()=>{r(),this.osk.activeKeyboard=l,this.contextManager.resetContext(),this.osk.present()}):(r(),this.contextManager.resetContext())}),this.contextManager.on("keyboardasyncload",l=>{var r,c;this.config.hostDevice.touchable&&((r=this.osk)!=null&&r.activationModel)&&(this.osk.activationModel.enabled=!0),(c=this.osk)==null||c.startHide(!1)})}init(t){return W(this,null,function*(){let e=this.config;if(e.deferForInitialization.isResolved)return Promise.resolve();e.initialize(t),String.kmwEnableSupplementaryPlane(!0);let n=new ns(this.interface,e.applyCacheBusting);this.keyboardRequisitioner=new $t(n,new _n,this.config.paths),this.modelCache=new en;let i=this.keyboardRequisitioner.cache,s=this.core.keyboardProcessor,l=new Gt(this.core.languageProcessor,()=>s.layerId);this.contextManager.configure({resetContext:r=>{this.osk?this.osk.batchLayoutAfter(()=>{this.core.resetContext(r)}):this.core.resetContext(r)},predictionContext:l,keyboardCache:this.keyboardRequisitioner.cache}),this.core.languageProcessor.on("suggestionapplied",()=>{var r;s.newLayerStore.set(""),s.oldLayerStore.set(""),(r=s.processPostKeystroke(s.contextDevice,l.currentTarget))==null||r.finalize(s,l.currentTarget,!0)}),this.config.on("spacebartext",()=>{var r;(r=this.osk)==null||r.refreshLayout()}),i.on("stubadded",r=>{let c=o(()=>{this.legacyAPIEvents.callEvent("keyboardregistered",{internalName:r.KI,language:r.KL,keyboardName:r.KN,languageCode:r.KLC,package:r.KP}),this.config.activateFirstKeyboard&&this.keyboardRequisitioner.cache.defaultStub==r&&this.contextManager.activateKeyboard(r.id,r.langId,!0)},"eventRaiser");this.config.deferForInitialization.isResolved?c():this.config.deferForInitialization.then(c)}),i.on("keyboardadded",r=>{let c=o(()=>{this.legacyAPIEvents.callEvent("keyboardloaded",{keyboardName:r.id})},"eventRaiser");this.config.deferForInitialization.isResolved?c():this.config.deferForInitialization.then(c)}),this.keyboardRequisitioner.cache.on("keyboardadded",r=>{this.legacyAPIEvents.callEvent("keyboardloaded",{keyboardName:r.id})})})}get build(){return Number.parseInt(et.VERSION_PATCH,10)}get version(){return et.VERSION_RELEASE}get hardKeyboard(){return this._hardKeyboard}set hardKeyboard(t){this._hardKeyboard&&this._hardKeyboard.off("keyevent",this.keyEventListener),this._hardKeyboard=t,t.on("keyevent",this.keyEventListener)}get osk(){return this._osk}set osk(t){var e;this._osk&&(this._osk.off("keyevent",this.keyEventListener),this.core.keyboardProcessor.layerStore.handler=this.osk.layerChangeHandler),this._osk=t,this.core.keyboardProcessor.contextDevice=(e=t==null?void 0:t.targetDevice)!=null?e:this.config.softDevice,t&&(this.contextManager.activeKeyboard&&(t.activeKeyboard=this.contextManager.activeKeyboard),t.on("keyevent",this.keyEventListener),this.core.keyboardProcessor.layerStore.handler=t.layerChangeHandler)}getDebugInfo(){var n,i,s,l,r,c,g,d,u,I,B,h,b,F;let t=(n=this.contextManager)==null?void 0:n.activeKeyboard;return{configReport:(i=this.config)==null?void 0:i.debugReport(),keyboard:{id:we((l=(s=t==null?void 0:t.metadata)==null?void 0:s.id)!=null?l:""),langId:((r=t==null?void 0:t.metadata)==null?void 0:r.langId)||"",version:(g=(c=t==null?void 0:t.keyboard)==null?void 0:c.version)!=null?g:""},model:{id:((u=(d=this.core)==null?void 0:d.activeModel)==null?void 0:u.id)||""},osk:{banner:(h=(B=(I=this.osk)==null?void 0:I.banner)==null?void 0:B.banner.type)!=null?h:"",layer:((F=(b=this.osk)==null?void 0:b.vkbd)==null?void 0:F.layerId)||""}}}refreshModel(){let t=this.contextManager.activeKeyboard,e=this.modelCache.modelForLanguage(t==null?void 0:t.metadata.langId);return this.core.activeModel!=e&&(this.core.activeModel&&this.core.languageProcessor.unloadModel(),e)?this.core.languageProcessor.loadModel(e).then(()=>e):Promise.resolve(e)}addEventListener(t,e){this.legacyAPIEvents.addEventListener(t,e)}removeEventListener(t,e){this.legacyAPIEvents.removeEventListener(t,e)}shutdown(){var t;this.legacyAPIEvents.shutdown(),(t=this.osk)==null||t.shutdown()}addModel(t){var n;this.modelCache.register(t);let e=(n=this.contextManager.activeKeyboard)==null?void 0:n.metadata;return e&&t.languages.indexOf(e.langId)!=-1?this.refreshModel().then(()=>{}):Promise.resolve()}removeModel(t){this.modelCache.unregister(t),this.core.activeModel&&this.core.activeModel.id==t&&this.core.languageProcessor.unloadModel()}setActiveKeyboard(t,e){return W(this,null,function*(){return this.contextManager.activateKeyboard(t,e,!0)})}getActiveKeyboard(){var t,e;return(e=(t=this.contextManager.activeKeyboard)==null?void 0:t.metadata.id)!=null?e:""}getActiveLanguage(t){var n,i,s;let e=(n=this.contextManager.activeKeyboard)==null?void 0:n.metadata;return t?(s=e==null?void 0:e.langName)!=null?s:"":(i=e==null?void 0:e.langId)!=null?i:""}isChiral(t){let e;if(t){if(typeof t=="string"){let n=this.keyboardRequisitioner.cache.getKeyboard(t);if(n)t=n;else throw new Error(`Keyboard '${t}' has not been loaded.`)}e=t}else e=this.core.activeKeyboard;return e.isChiral}resetContext(){this.contextManager.resetContext()}setNumericLayer(){this.core.keyboardProcessor.setNumericLayer(this.config.softDevice)}};o(Ir,"KeymanEngine");var rn=Ir;var rt=class rt{get height(){return this._height}set height(t){this._height=t>0?t:0,this.update()}get width(){return this._width}set width(t){this._width=t,this.update()}update(){let t=this.div.style,e=t.height,n=t.display;return this._height>0?(t.height=this._height+"px",t.display="block"):(t.height="0px",t.display="none"),e!==t.height||n!==t.display}constructor(t){let e=H("div");e.id=rt.BANNER_ID,e.className=rt.BANNER_CLASS,this.div=e,this.height=t,this.update()}appendStyleSheet(){}getDiv(){return this.div}configureForKeyboard(t,e){}shutdown(){}};o(rt,"Banner"),rt.DEFAULT_HEIGHT=37,rt.BANNER_CLASS="kmw-banner-bar",rt.BANNER_ID="kmw-banner-bar";var de=rt;var Me=class Me{constructor(t){let e=typeof t=="string"?Me.parseLengthStyle(t):t;this.val=e.val,this.absolute=e.absolute,e.special&&(this.special=e.special)}get styleString(){return this.absolute?this.val+"px":this.special?this.val+this.special:this.val*100+"%"}scaledBy(t){return new Me({val:t*this.val,absolute:this.absolute})}static inPixels(t){return new Me({val:t,absolute:!0})}static inPercent(t){return new Me({val:t/100,absolute:!1})}static forScalar(t){return new Me({val:t,absolute:!1})}static special(t,e){return new Me({val:t,absolute:!1,special:e})}static parseLengthStyle(t){if(t=="")return Xo;let e=parseFloat(t);return isNaN(e)?(console.error("Could not properly parse specified length style info: '"+t+"'."),Xo):t.indexOf("px")!=-1?{val:e,absolute:!0}:t.indexOf("pt")!=-1?{val:4*e/3,absolute:!0}:t.indexOf("%")!=-1?{val:e/100,absolute:!1}:t.indexOf("rem")!=-1?{val:e,absolute:!1,special:"rem"}:t.indexOf("em")!=-1?{val:e,absolute:!1,special:"em"}:{val:4*e/3,absolute:!0}}};o(Me,"ParsedLengthStyle");var x=Me,Xo=new x("1em");var br=class br extends de{constructor(){super(0);this.type="blank"}};o(br,"BlankBanner");var ct=br;var hr=class hr{constructor(){this._activeBannerHeight=de.DEFAULT_HEIGHT;this.events=new S.default;this.constructContainer()}constructContainer(){let t=H("div");return t.id="keymanweb_banner_container",t.className="kmw-banner-container",this.bannerContainer=t}get element(){return this.bannerContainer}appendStyles(){this.currentBanner&&this.currentBanner.appendStyleSheet()}get banner(){return this.currentBanner}set banner(t){if(this.currentBanner){if(t==this.currentBanner)return;{let e=this.currentBanner;this.currentBanner=t,this.bannerContainer.replaceChild(t.getDiv(),e.getDiv()),e.shutdown()}}else this.currentBanner=t,t&&this.bannerContainer.appendChild(t.getDiv());t instanceof ct||(t.height=this.activeBannerHeight),this.events.emit("bannerchange")}get height(){return this.currentBanner?this.currentBanner.height:0}get activeBannerHeight(){return this._activeBannerHeight}set activeBannerHeight(t){this._activeBannerHeight=t,this.currentBanner&&!(this.currentBanner instanceof ct)&&(this.currentBanner.height=t)}get layoutHeight(){return x.inPixels(this.height)}get width(){var t;return(t=this.currentBanner)==null?void 0:t.width}set width(t){this.currentBanner&&(this.currentBanner.width=t)}refreshLayout(){var t,e;(e=(t=this.currentBanner).refreshLayout)==null||e.call(t)}};o(hr,"BannerView");var rs=hr;var Fr=class Fr extends de{constructor(e,n){var t=(...args)=>{super(...args)};e.length>0?(t(),n&&(this.height=n)):t(0),this.type="image",e.indexOf("base64")>=0?console.log("Loading img from base64 data"):console.log("Loading img with src '"+e+"'"),this.img=document.createElement("img"),this.img.setAttribute("src",e);let i=this.img.style;i.width="100%",i.height="100%",this.getDiv().appendChild(this.img),console.log("Image loaded.")}setImagePath(e){this.img&&this.img.setAttribute("src",e)}};o(Fr,"ImageBanner");var cs=Fr;function Le(a,t){t instanceof Error?console.error(`${a}: ${t.message} + +${t.stack}`):(console.error(a),console.error(t))}o(Le,"reportError");var mr=class mr{constructor(t){this.queue=[],this.defaultWaitFactory=t||(()=>ae(0))}get defaultWait(){return this.defaultWaitFactory()}get ready(){return this.queue.length==0&&!this.waitLock}triggerNextClosure(){return W(this,null,function*(){if(this.queue.length==0)return;let t=this.queue.shift();this.waitLock=Promise.resolve();let e;try{e=t()}catch(n){Le("Error from queued closure",n)}e=e!=null?e:this.defaultWaitFactory(),this.waitLock=e;try{yield e}catch(n){Le("Async error from queued closure",n)}this.waitLock=null,this.triggerNextClosure()})}runAsync(t){let e=this.ready;this.queue.push(t),e&&this.triggerNextClosure()}};o(mr,"AsyncClosureDispatchQueue");var os=mr;function So(a){return"targetX"in a&&"targetY"in a&&"t"in a}o(So,"isAnInputSample");var ot=class ot{constructor(t){this.rawLinearSums={x:0,y:0,t:0};this.coordArcSum=0;this._sampleCount=0;if(t)if(t instanceof ot)Object.assign(this,t),this.rawLinearSums=y({},t.rawLinearSums);else if(So(t))Object.assign(this,this.extend(t));else throw new Error("A constructor for this input pattern has not yet been implemented")}extend(t){return this._extend(new ot(this),t)}_extend(t,e){t._initialSample||(t._initialSample=e,t.baseSample=e);let n=t.baseSample;this.followingSample=e;let i=e.targetX-n.targetX,s=e.targetY-n.targetY,l=e.t-n.t;if(t.rawLinearSums.x+=i,t.rawLinearSums.y+=s,t.rawLinearSums.t+=l,this.lastSample){let r=e.targetX-this.lastSample.targetX,c=e.targetY-this.lastSample.targetY,g=r*r+c*c,d=Math.sqrt(g);t.coordArcSum+=d}return t._lastSample=e,t.sampleCount=this.sampleCount+1,t}deaccumulate(t){let e=new ot(this);return this._deaccumulate(e,t)}_deaccumulate(t,e){if(!e)return t;if(!e.followingSample||!e.lastSample)throw"Invalid argument: stats missing necessary tracking variable.";for(let n in t.rawLinearSums){let i=n;t.rawLinearSums[i]-=e.rawLinearSums[i]}if(e.followingSample&&e.lastSample){let n=e.followingSample.targetX-e.lastSample.targetX,i=e.followingSample.targetY-e.lastSample.targetY,s=n*n+i*i,l=Math.sqrt(s);t.coordArcSum-=l,t.coordArcSum-=e.coordArcSum}return t.sampleCount-=e.sampleCount,t._initialSample=e.followingSample,t}translateCoordSystem(t){let e=new ot(this);return this._translateCoordSystem(e,t)}_translateCoordSystem(t,e){if(this.sampleCount==0)return t;let n=t.initialSample==t.lastSample;return t._initialSample=e(t.initialSample),t.baseSample=e(t.baseSample),t._lastSample=n?t._initialSample:e(t.lastSample),t}replaceInitialSample(t){let e=new ot(this);return this._replaceInitialSample(e,t)}_replaceInitialSample(t,e){if(this.sampleCount==0)throw new Error("no sample available to replace");let n=t.initialSample;if(t._initialSample=e,this.sampleCount>1){let i=e.targetX-n.targetX,s=e.targetY-n.targetY,l=e.t-n.t;t.rawLinearSums.x+=i,t.rawLinearSums.y+=s,t.rawLinearSums.t+=l;let r=i*i+s*s,c=Math.sqrt(r);t.coordArcSum+=c}else t._lastSample=e;return t}get lastSample(){return this._lastSample}get lastTimestamp(){var t;return(t=this.lastSample)==null?void 0:t.t}get sampleCount(){return this._sampleCount}set sampleCount(t){this._sampleCount=t}get initialSample(){return this._initialSample}mappingConstant(t){if(this.baseSample)return t=="t"?this.baseSample.t:t=="x"?this.baseSample.targetX:t=="y"?this.baseSample.targetY:0}mean(t){return this.rawLinearSums[t]/this.sampleCount+this.mappingConstant(t)}get netDistance(){if(!this.lastSample||!this.initialSample)return 0;let t=this.lastSample.targetX-this.initialSample.targetX,e=this.lastSample.targetY-this.initialSample.targetY;return Math.sqrt(t*t+e*e)}get duration(){return!this.lastSample||!this.initialSample?0:this.lastSample.t-this.initialSample.t}get angle(){if(this.sampleCount==1||!this.lastSample||!this.initialSample)return;if(this.netDistance<1)return;let t=this.lastSample.targetX-this.initialSample.targetX,e=this.lastSample.targetY-this.initialSample.targetY,n=Math.acos(-e/this.netDistance);return t<0?2*Math.PI-n:n}get angleInDegrees(){return this.angle*180/Math.PI}get cardinalDirection(){if(this.sampleCount==1||!this.lastSample||!this.initialSample||isNaN(this.angle)||this.angle===null||this.angle===void 0)return;let t=["n","ne","e","se","s","sw","w","nw","n"],e=Math.ceil((this.angleInDegrees-22.5)/45);return t[e]}get speed(){return this.duration?this.netDistance/this.duration:0}get rawDistance(){return this.coordArcSum}toJSON(){return{angle:this.angle,cardinal:this.cardinalDirection,netDistance:this.netDistance,duration:this.duration,sampleCount:this.sampleCount,rawDistance:this.rawDistance}}};o(ot,"CumulativePathStats");var Re=ot;function St(a,t){let e=a.gestures.find(n=>n.id==t);if(!e)throw new Error(`Could not find spec for gesture with id '${t}'`);return e}o(St,"getGestureModel");function yr(a,t){let e=a.sets[t];if(!e)throw new Error(`Could not find a defined gesture-set with id '${t}'`);let n=a.gestures.filter(s=>!!e.find(l=>s.id==l)),i=e.filter(s=>!n.find(l=>l.id==s));if(i.length>0)throw new Error(`Set '${t}' cannot find definitions for gestures with ids ${i}`);return n}o(yr,"getGestureModelSet");var pr={gestures:[],sets:{default:[]}};var Cr=class Cr{constructor(){}getBoundingClientRect(){return new DOMRect(0,0,Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),Math.max(document.documentElement.clientHeight||0,window.innerHeight||0))}};o(Cr,"ViewportZoneSource");var as=Cr;var Gr=class Gr{get edgePadding(){return this._edgePadding}constructor(t,e){Array.isArray(t)&&(e=t,t=new as),this.root=t,e=e||[0,0,0,0],this.updatePadding(e)}updatePadding(t){switch(t.length){case 1:let e=t[0];this._edgePadding={x:e,y:e,w:2*e,h:2*e};break;case 2:this._edgePadding={x:t[1],y:t[0],w:2*t[1],h:2*t[0]};break;case 3:this._edgePadding={x:t[1],y:t[0],w:2*t[1],h:t[0]+t[2]};break;case 4:this._edgePadding={x:t[3],y:t[0],w:t[1]+t[3],h:t[0]+t[2]};break;default:throw new Error("Invalid values for PaddedZoneSource's edgePadding - must be between 1 to 4 `number` values.")}}getBoundingClientRect(){let t=this.root.getBoundingClientRect();return new DOMRect(t.left+this.edgePadding.x,t.top+this.edgePadding.y,t.width-this.edgePadding.w,t.height-this.edgePadding.h)}};o(Gr,"PaddedZoneSource");var ue=Gr;function gs(a){var e,n,i,s,l,r,c;let t=y({},a);if(t.mouseEventRoot=(e=t.mouseEventRoot)!=null?e:t.targetRoot,t.touchEventRoot=(n=t.touchEventRoot)!=null?n:t.targetRoot,t.inputStartBounds=(i=t.inputStartBounds)!=null?i:t.targetRoot,t.maxRoamingBounds=(s=t.maxRoamingBounds)!=null?s:t.targetRoot,t.safeBounds=(l=t.safeBounds)!=null?l:new ue([2]),t.itemIdentifier=(r=t.itemIdentifier)!=null?r:()=>null,t.recordingMode=!!t.recordingMode,t.historyLength=((c=t.historyLength)!=null?c:0)>0?t.historyLength:0,a.paddedSafeBounds)delete t.safeBoundPadding;else{let g=a.safeBoundPadding;typeof g=="number"&&(g=[g]),g=g!=null?g:[3],t.paddedSafeBounds=new ue(t.safeBounds,g)}return t}o(gs,"preprocessRecognizerConfig");var ds=class ds extends S.default{constructor(){super();this._isComplete=!1;this._stats=new Re}get stats(){return this._stats}clone(){let e=new ds;return e._isComplete=this._isComplete,e._wasCancelled=this._wasCancelled,e._stats=new Re(this._stats),e}get isComplete(){return this._isComplete}get wasCancelled(){return this._wasCancelled}translateCoordSystem(e){this._stats=this._stats.translateCoordSystem(e)}replaceInitialSample(e){this._stats=this._stats.replaceInitialSample(e)}extend(e){if(this._isComplete)throw new Error("Invalid state: this GesturePath has already terminated.");this._stats=this._stats.extend(e),this.emit("step",e)}terminate(e=!1){this._isComplete||(this._wasCancelled=e,this._isComplete=!0,e?this.emit("invalidated"):this.emit("complete"),this.removeAllListeners())}toJSON(){return{stats:this.stats,wasCancelled:this.wasCancelled}}};o(ds,"GesturePath");var cn=ds;var qn=class qn extends cn{constructor(){super(...arguments);this.samples=[]}clone(){let e=new qn;return e.samples=[].concat(this.samples),e._isComplete=this._isComplete,e._wasCancelled=this._wasCancelled,e._stats=new Re(this._stats),e}static deserialize(e){let n=new qn;n.samples=[].concat(e.coords.map(s=>y({},s))),n._isComplete=!0,n._wasCancelled=e.wasCancelled;let i=n.samples.reduce((s,l)=>s.extend(l),new Re);return n._stats=i,n}extend(e){if(this.isComplete)throw new Error("Invalid state: this GesturePath has already terminated.");this.samples.push(e),super.extend(e)}translateCoordSystem(e){super.translateCoordSystem(e);for(let n=0;n({targetX:n.targetX,targetY:n.targetY,t:n.t,item:n.item}))),wasCancelled:this.wasCancelled,stats:this.stats};for(let n of e.coords)delete n.clientX,delete n.clientY,n.item===void 0&&delete n.item;return e}};o(qn,"GestureDebugPath");var at=qn;var Qr=class Qr{constructor(t,e,n,i){this.stateToken=null;this.rawIdentifier=t,this.isFromTouch=n,this._path=i?new i:new cn,this.recognizerConfigStack=Array.isArray(e)?e:[e]}get path(){return this._path}setGestureMatchInspector(t){if(this._matchInspectionClosure)throw new Error("Invalid state: the match-inspection closure has already been set");this._matchInspectionClosure=t}update(t){this.path.extend(t),this._baseItem||(this._baseItem=t.item)}get baseItem(){return this._baseItem}set baseItem(t){this._baseItem=t}get currentSample(){return this.path.stats.lastSample}get potentialModelMatchIds(){return this._matchInspectionClosure(this)}constructSubview(t,e,n){return new re(this,this.recognizerConfigStack,t,e,n)}terminate(t){this.path.terminate(t)}get isPathComplete(){return this.path.isComplete}get identifier(){return`${this.isFromTouch?"touch":"mouse"}:${this.rawIdentifier}`}pushRecognizerConfig(t){let e=A(y({},t),{mouseEventRoot:this.recognizerConfigStack[0].mouseEventRoot,touchEventRoot:this.recognizerConfigStack[0].touchEventRoot});this.recognizerConfigStack.push(gs(e))}popRecognizerConfig(){if(this.recognizerConfigStack.length==1)throw new Error("Cannot 'pop' the original recognizer-configuration for this GestureSource.");return this.recognizerConfigStack.pop()}get currentRecognizerConfig(){return this.recognizerConfigStack[this.recognizerConfigStack.length-1]}toJSON(){return{identifier:this.identifier,isFromTouch:this.isFromTouch,path:this.path.toJSON(),stateToken:this.stateToken}}};o(Qr,"GestureSource");var he=Qr,$n=class $n extends he{constructor(e,n,i,s,l){let r=0,c=e.path.stats.sampleCount;e instanceof $n&&(r=e._baseStartIndex);super(e.rawIdentifier,n,e.isFromTouch,Object.getPrototypeOf(e.path).constructor);let g=this._baseSource=e instanceof $n?e._baseSource:e;this.stateToken=l!=null?l:e.stateToken;let d=o(b=>{let F=this.recognizerTranslation,G=A(y({},b),{targetX:b.targetX-F.x,targetY:b.targetY-F.y});return this.stateToken&&(G.stateToken=this.stateToken),(this.stateToken!=g.stateToken||this.stateToken!=e.stateToken)&&(G.item=this.currentRecognizerConfig.itemIdentifier(G,null)),G},"translateSample"),u=e.path.stats.lastSample;i?(this._baseStartIndex=r=Math.max(r+c-1,0),c=c>0?1:0):this._baseStartIndex=r,i?e.path.stats.sampleCount&&this._path.extend(e.path.stats.lastSample):this._path=e.path.clone(),this._path.translateCoordSystem(d),s?this._baseItem=e.baseItem:this._baseItem=u==null?void 0:u.item;let I=o(()=>this.path.terminate(!1),"completeHook"),B=o(()=>this.path.terminate(!0),"invalidatedHook"),h=o(b=>{super.update(d(b))},"stepHook");g.path.on("complete",I),g.path.on("invalidated",B),g.path.on("step",h),this.subviewDisconnector=()=>{g.path.off("complete",I),g.path.off("invalidated",B),g.path.off("step",h)},g.isPathComplete&&(this.path.terminate(g.path.wasCancelled),this.disconnect())}get recognizerTranslation(){if(this.recognizerConfigStack.length==1||!this.currentRecognizerConfig)return{x:0,y:0};let n=this.currentRecognizerConfig.targetRoot.getBoundingClientRect(),i=this.recognizerConfigStack[0].targetRoot.getBoundingClientRect();return{x:n.left-i.left,y:n.top-i.top}}get baseSource(){return this._baseSource}disconnect(){this.subviewDisconnector&&(this.subviewDisconnector(),this.subviewDisconnector=null)}pushRecognizerConfig(e){throw new Error("Pushing and popping of recognizer configurations should only be called on the base GestureSource")}popRecognizerConfig(){throw new Error("Pushing and popping of recognizer configurations should only be called on the base GestureSource")}update(e){throw new Error("Updates should be provided through the base GestureSource.")}terminate(e){this.baseSource.terminate(e)}};o($n,"GestureSourceSubview");var re=$n;var Bs=class Bs extends he{constructor(e,n,i){super(e,n,i,at);this.stateToken=null}initPath(){return new at}static deserialize(e,n){let i=n!==void 0?n:this._jsonIdSeed++,s=e.isFromTouch,l=at.deserialize(e.path),r=new Bs(i,null,s);return r._path=l,r}};o(Bs,"GestureDebugSource");var us=Bs;var ei=class ei extends S.default{constructor(e){var n;super();this._activeTouchpoints=[];this.identifierMap={};this.config=e,this.sourceConstructor=(n=e==null?void 0:e.recordingMode)==null||n?us:he}createTouchpoint(e,n){let i=ei.IDENTIFIER_SEED++;this.identifierMap[e]=i;let s=new this.sourceConstructor(i,this.config,n);return s.stateToken=this.stateToken,s}fulfillInputStart(e){}maintainTouchpoints(e){e||(e=[]),this._activeTouchpoints.filter(n=>!e.includes(n)).forEach(n=>n.terminate(!0))}hasActiveTouchpoint(e){return this.identifierMap[e]!==void 0}getTouchpointWithId(e){let n=this.identifierMap[e];return this._activeTouchpoints.find(i=>i.rawIdentifier==n)}getConfigForId(e){return this.getTouchpointWithId(e).currentRecognizerConfig}getStateTokenForId(e){var n;return(n=this.getTouchpointWithId(e).stateToken)!=null?n:null}dropTouchpoint(e){let n=e.rawIdentifier;this._activeTouchpoints=this._activeTouchpoints.filter(i=>e!=i);for(let i of Object.keys(this.identifierMap)){let s=Number.parseInt(i,10);this.identifierMap[s]==n&&delete this.identifierMap[s]}}addTouchpoint(e){this._activeTouchpoints.push(e)}get activeSources(){return[].concat(this._activeTouchpoints)}};o(ei,"InputEngineBase"),ei.IDENTIFIER_SEED=0;var Is=ei;function Ur(a,t,e){let n=a.targetRoot.getBoundingClientRect();return{clientX:t,clientY:e,targetX:t-n.left,targetY:e-n.top}}o(Ur,"processSampleClientCoords");var xr=class xr extends Is{buildSampleFor(t,e,n,i,s){var g,d;let l=A(y({},Ur(this.config,t,e)),{t:i,stateToken:(g=s==null?void 0:s.stateToken)!=null?g:this.stateToken}),c=((d=s==null?void 0:s.currentRecognizerConfig.itemIdentifier)!=null?d:this.config.itemIdentifier)(l,n);return l.item=c,l}onInputStart(t,e,n,i){let s=this.createTouchpoint(t,i);s.update(e),this.addTouchpoint(s),s.path.on("invalidated",()=>{this.dropTouchpoint(s)}),s.path.on("complete",()=>{this.dropTouchpoint(s)});try{this.emit("pointstart",s)}catch(l){Le("Engine-internal error while initializing gesture matching for new source",l)}return s}onInputMove(t,e,n){if(t)try{t.update(e)}catch(i){Le("Error occurred while updating source",i)}}onInputMoveCancel(t,e,n){if(t)try{t.update(e),t.path.terminate(!0)}catch(i){Le("Error occurred while cancelling further input for source",i)}}onInputEnd(t,e){if(t)try{t.path.terminate(!1)}catch(n){Le("Error occurred while finalizing input for source",n)}}};o(xr,"InputEventEngine");var on=xr;var Ze=class Ze{constructor(){}static getCoordZoneBitmask(t,e){let n=e.getBoundingClientRect(),i=0;return i|=t.clientXn.right?Ze.FAR_RIGHT:0,i|=t.clientYn.bottom?Ze.FAR_BOTTOM:0,i}static inputStartOutOfBoundsCheck(t,e){return!!this.getCoordZoneBitmask(t,e.inputStartBounds)}static inputStartSafeBoundProximityCheck(t,e){return this.getCoordZoneBitmask(t,e.paddedSafeBounds)}static inputMoveCancellationCheck(t,e,n){return n=n||0,this.getCoordZoneBitmask(t,e.maxRoamingBounds)?!0:!!(this.getCoordZoneBitmask(t,e.safeBounds)&~n)}};o(Ze,"ZoneBoundaryChecker"),Ze.FAR_TOP=8,Ze.FAR_LEFT=4,Ze.FAR_BOTTOM=2,Ze.FAR_RIGHT=1;var ve=Ze;var Zr=class Zr extends on{constructor(e){super(e);this.hasActiveClick=!1;this.disabledSafeBounds=0;this.currentSource=null;this.activeIdentifier=0;this._mouseStart=n=>this.onMouseStart(n),this._mouseMove=n=>this.onMouseMove(n),this._mouseEnd=n=>this.onMouseEnd(n)}get eventRoot(){return this.config.mouseEventRoot}registerEventHandlers(){this.eventRoot.addEventListener("mousedown",this._mouseStart,!0),this.eventRoot.addEventListener("mousemove",this._mouseMove,!1),this.eventRoot.addEventListener("mouseup",this._mouseEnd,!0)}unregisterEventHandlers(){this.eventRoot.removeEventListener("mousedown",this._mouseStart,!0),this.eventRoot.removeEventListener("mousemove",this._mouseMove,!1),this.eventRoot.removeEventListener("mouseup",this._mouseEnd,!0)}preventPropagation(e){e.preventDefault(),e.cancelBubble=!0,e.returnValue=!1,typeof e.stopImmediatePropagation=="function"?e.stopImmediatePropagation():typeof e.stopPropagation=="function"&&e.stopPropagation()}buildSampleFromEvent(e){return this.buildSampleFor(e.clientX,e.clientY,e.target,performance.now(),this.currentSource)}onMouseStart(e){if(!this.config.targetRoot.contains(e.target))return;this.preventPropagation(e);let n=this.buildSampleFromEvent(e);ve.inputStartOutOfBoundsCheck(n,this.config)||(this.disabledSafeBounds=ve.inputStartSafeBoundProximityCheck(n,this.config));let i=this.onInputStart(this.activeIdentifier,n,e.target,!1);this.currentSource=i;let s=o(()=>{this.currentSource=null},"cleanup");i.path.on("complete",s),i.path.on("invalidated",s)}onMouseMove(e){let n=this.currentSource;if(!n)return;let i=this.buildSampleFromEvent(e);if(!e.buttons){this.hasActiveClick&&(this.hasActiveClick=!1,this.onInputMoveCancel(n,i,e.target));return}this.preventPropagation(e);let s=n.currentRecognizerConfig;ve.inputMoveCancellationCheck(i,s,this.disabledSafeBounds)?this.onInputMoveCancel(n,i,e.target):this.onInputMove(n,i,e.target)}onMouseEnd(e){let n=this.currentSource;n&&(e.buttons||(this.hasActiveClick=!1),this.onInputEnd(n,e.target))}};o(Zr,"MouseEventEngine");var bs=Zr;function Vo(a){let t=[];for(let e=0;ethis.onTouchStart(n),this._touchMove=n=>this.onTouchMove(n),this._touchEnd=n=>this.onTouchEnd(n)}get eventRoot(){return this.config.touchEventRoot}registerEventHandlers(){this.eventRoot.addEventListener("touchstart",this._touchStart,{capture:!0,passive:!1}),this.eventRoot.addEventListener("touchmove",this._touchMove,{capture:!1,passive:!1}),this.eventRoot.addEventListener("touchend",this._touchEnd,{capture:!0,passive:!1})}unregisterEventHandlers(){this.eventRoot.removeEventListener("touchstart",this._touchStart,!0),this.eventRoot.removeEventListener("touchmove",this._touchMove,!1),this.eventRoot.removeEventListener("touchend",this._touchEnd,!0)}preventPropagation(e){e.cancelable&&e.preventDefault(),typeof e.stopImmediatePropagation=="function"?e.stopImmediatePropagation():typeof e.stopPropagation=="function"&&e.stopPropagation()}dropTouchpoint(e){super.dropTouchpoint(e);for(let n of Object.keys(this.safeBoundMaskMap)){let i=Number.parseInt(n,10);this.getTouchpointWithId(i)==e&&delete this.safeBoundMaskMap[i]}}fulfillInputStart(e){let n=this.inputStartSignalMap.get(e);n&&(this.inputStartSignalMap.delete(e),n.resolve())}hasActiveTouchpoint(e){return super.hasActiveTouchpoint(e)||!!this.pendingSourcePromises.has(e)}buildSampleFromTouch(e,n,i){return this.buildSampleFor(e.clientX,e.clientY,e.target,n,i)}onTouchStart(e){if(!this.config.targetRoot.contains(e.target))return;this.preventPropagation(e);let n=Vo(e.touches),i=Vo(e.changedTouches),l=n.filter(c=>i.findIndex(g=>c.identifier==g.identifier)==-1).map(c=>this.pendingSourcePromises.get(c.identifier));this.eventDispatcher.runAsync(()=>W(this,null,function*(){let c=yield Promise.all(l);return this.maintainTouchpoints(c),this.eventDispatcher.defaultWait}));let r=new Map;for(let c=0;c{let c=performance.now(),g=null;for(let d=0;d{this.pendingSourcePromises.get(I)==h&&this.pendingSourcePromises.delete(I)},"cleanup");g.path.on("complete",b),g.path.on("invalidated",b)}if(g){let d=new f;return this.inputStartSignalMap.set(g,d),d.corePromise}else return Promise.resolve()})}onTouchMove(e){var i;for(let s=0;sW(this,null,function*(){let s=yield Promise.all(n.values());return this.maintainTouchpoints(s),this.eventDispatcher.defaultWait})),this.eventDispatcher.runAsync(()=>W(this,null,function*(){let s=performance.now();for(let l=0;lW(this,null,function*(){for(let s=0;s{this.timerPromise.resolve(!1)}),this.timerPromise.then(s=>{let l=e instanceof re?e.baseSource:e,r=performance.now();!l.isPathComplete&&l.currentSample.t!=r&&l.path.extend(A(y({},l.currentSample),{t:r})),s!=t.timer.expectedResult&&this.finalize(!1,"timer"),this.finalize(!0,"timer")})}}finalize(t,e){if(this.publishedPromise.isFulfilled)return this._result;let n=this.model;n.validateItem&&t&&(t=n.validateItem(this.source.path.stats.lastSample.item,this.baseItem));let i;return t?i={type:n.pathResolutionAction,cause:e}:i={type:"reject",cause:e},this.publishedPromise.resolve(i),this._result=i,i}get stats(){return this.source.path.stats}get baseItem(){return this.source.baseItem}get lastItem(){return this.source.currentSample.item}update(){let t=this.model,e=this.source;if(e.path.wasCancelled)return this.finalize(!1,"path");if(t.itemChangeAction&&e.path.stats.sampleCount>0&&e.currentSample.item!=e.baseItem){let n=t.itemChangeAction=="resolve";return this.finalize(n,"item")}else{let n=t.pathModel.evaluate(e.path,this.lastStats,e.baseItem,this.inheritedStats)||"continue";return this.lastStats=e.path.stats,n!="continue"?this.finalize(n=="resolve","path"):e.path.isComplete?this.finalize(!1,"path"):{type:"continue"}}}};o(Sr,"PathMatcher");var an=Sr;var Vr=class Vr{constructor(t,e){this._isCancelled=!1;var r;if(!t||!e)throw new Error("Construction of GestureMatcher requires a gesture-model spec and a source for related contact points.");if(!t.sustainTimer&&!e)throw new Error("If the provided gesture-model spec lacks a sustain timer, there must be an active contact point.");let n=e instanceof he?null:e,i=n?null:e;this.predecessor=n,this.publishedPromise=new f,this.model=t,t.sustainTimer&&(this.sustainTimerPromise=new Ee(t.sustainTimer.duration),this.sustainTimerPromise.then(c=>{let g=t.sustainTimer.expectedResult==c;this.finalize(g,"timer")})),this.pathMatchers=[];let l=(i?[i]:n.sources).map(c=>i&&c==i?i:c.isPathComplete?null:c).reduce((c,g)=>g?c.concat(g):c,[]);if(t.sustainTimer&&l.length>0){this.finalize(!1,"path");return}else!t.sustainTimer&&l.length==0&&this.finalize(!1,"path");for(let c=0;c{if(!this.model.contacts[e].resetOnInstantFulfill)return t.source}).filter(t=>!!t)}get promise(){return this.publishedPromise.corePromise}cancel(){this._isCancelled=!0,this._result||this.finalize(!1,"cancelled")}get isCancelled(){return this._isCancelled}finalize(t,e){var n,i;if(this.publishedPromise.isFulfilled)return this._result;try{let s;t?s=this.model.resolutionAction:(e!="cancelled"&&((n=this.model.rejectionActions)!=null&&n[e])&&(s=this.model.rejectionActions[e],s.item="none"),s=s||{type:"none",item:"none"});let l;switch((i=s.item)!=null?i:"current"){case"none":l=null;break;case"base":l=this.primaryPath.baseItem;break;case"current":l=this.primaryPath.currentSample.item;break}let c={matched:t,action:A(y({},s),{item:l})};return this.publishedPromise.resolve(c),this._result=c,c}catch(s){return this.publishedPromise.reject(s),{matched:!1,action:{type:"none",item:null}}}}finalizeSources(){if(!this._result)throw Error("Invalid state for source-finalization - the matcher's evaluation of the gesture model is not yet complete");let t=this._result.matched;for(let e=0;ee&&(e=n.model.itemPriority,t=n);return!t&&this.predecessor?this.predecessor.primaryPath:t==null?void 0:t.source}get baseItem(){return this.primaryPath.baseItem}get currentItem(){return this.primaryPath.currentSample.item}get allSourceIds(){let t=this.sources.map(n=>n.identifier),e=this.predecessor?this.predecessor.allSourceIds:[];return t=t.filter(n=>e.indexOf(n)==-1),t.concat(e)}mayAddContact(){return this.pathMatchers.length{this.finalize(u.type=="resolve",u.cause)})}update(){this.pathMatchers.forEach(t=>{try{t.update()}catch(e){console.error(e),this.finalize(!1,"cancelled")}})}};o(Vr,"GestureMatcher");var Vt=Vr;var Ar=class Ar extends S.default{constructor(e){super();this._sourceSelector=[];this.potentialMatchers=[];this.sustainMode=!1;this.attemptSynchronousUpdate=o(()=>{let n=this._sourceSelector.filter(s=>!s.source.isPathComplete).map(s=>s.source.currentSample.t),i=n[0];n.find(s=>i!=s)||this.potentialMatchers.forEach(s=>s.update())},"attemptSynchronousUpdate");this.baseGestureSetId=e||"default"}potentialMatchersForSource(e){return this.potentialMatchers.filter(n=>n.allSourceIds.find(i=>i==e.identifier))}cascadeTermination(){let e=this.potentialMatchers,n=e.filter(r=>!r.model.sustainWhenNested),i=e.filter(r=>r.model.sustainWhenNested);this.potentialMatchers=i;let s=i.map(r=>r.allSourceIds).reduce((r,c)=>{for(let g of c)r.indexOf(g)==-1&&r.push(g);return r},[]);return this._sourceSelector.filter(r=>!s.find(c=>c==r.source.identifier)).forEach(r=>{r.matchPromise.resolve({matcher:null,result:{matched:!1,action:{type:"complete",item:null}}});let c=this._sourceSelector.indexOf(r);c>-1&&this._sourceSelector.splice(c,1)}),n.forEach(r=>r.cancel()),this.sustainMode=!0,this._sourceSelector.map(r=>r.source)}matchGesture(e,n){return W(this,null,function*(){let i=e instanceof he,s=o(B=>{let h=B.sources.map(b=>b.baseSource);return h&&h.length>0?h:B.predecessor?s(B.predecessor):[]},"determinePredecessorSources"),l=i?[e instanceof re?e.baseSource:e]:s(e),r=i?e:null,c=i?null:e;if(this.pendingMatchSetup){let B=this.pendingMatchSetup,h=new f;this.pendingMatchSetup=h.corePromise,yield B,this.pendingMatchSetup==h.corePromise&&(this.pendingMatchSetup=null),h.resolve()}i&&r.path.on("invalidated",()=>{this.dropSourcesWithIds([r.identifier])});let g=new f,d=l.map(B=>{let h={source:B,matchPromise:g,preserve:!0};return this._sourceSelector.push(h),h}),u=d.map(B=>B.matchPromise);if(i){let B=this.potentialMatchers.filter(h=>h.mayAddContact());if(B.forEach(h=>{h.addContact(r),h.promise.then(this.matcherSelectionFilter(h,u))}),B.length>0){let h=this.stateToken,b=new f;this.pendingMatchSetup=b.corePromise,yield ae(0),yield ae(0),this.pendingMatchSetup==b.corePromise&&(this.pendingMatchSetup=null),b.resolve();let F=this.stateToken;if(h!=F){let U=r.currentSample;r.stateToken=F,U.stateToken=F,U.item=e.currentRecognizerConfig.itemIdentifier(U,null),r.baseItem=U.item}let G=B.find(U=>U.result);if(G&&G.allSourceIds.includes(e.identifier))return g.resolve({matcher:null,result:{matched:!1,action:{type:"complete",item:null}}}),{selectionPromise:g.corePromise}}}if(d.forEach(B=>{B.preserve=!1}),this.sustainMode&&r)return g.resolve({matcher:null,result:{matched:!1,action:{type:"complete",item:null}}}),{selectionPromise:g.corePromise,sustainModeWithoutMatch:!0};let I=n.map(B=>{try{return new Vt(B,r||c)}catch(h){return console.error(h),null}}).filter(B=>!!B);I=I.filter(B=>!B.result||B.result.matched!==!1);for(let B of I)B.promise.then(this.matcherSelectionFilter(B,u));return I.length>0?this.potentialMatchers=this.potentialMatchers.concat(I):g.resolve({matcher:null,result:{matched:!1,action:{type:"complete",item:null}}}),this.potentialMatchers.sort((B,h)=>h.model.resolutionPriority-B.model.resolutionPriority),this.resetSourceHooks(),{selectionPromise:g.corePromise}})}resetSourceHooks(){let e=o(n=>{let i=n;i.path.off("step",this.attemptSynchronousUpdate),i.path.off("complete",this.attemptSynchronousUpdate),i.path.off("invalidated",this.attemptSynchronousUpdate),i.path.on("step",this.attemptSynchronousUpdate),i.path.on("complete",this.attemptSynchronousUpdate),i.path.on("invalidated",this.attemptSynchronousUpdate)},"resetHooks");this._sourceSelector.forEach(n=>e(n.source))}dropSourcesWithIds(e){for(let n of e){let i=this._sourceSelector.findIndex(s=>s.source.identifier==n);i>-1&&this._sourceSelector.splice(i,1)[0].matchPromise.resolve({matcher:null,result:{matched:!1,action:{type:"none",item:null}}})}}matchersForSource(e){return this.potentialMatchers.filter(n=>!!n.sources.find(i=>i.identifier==e.identifier))}matcherSelectionFilter(e,n){return i=>W(this,null,function*(){e.isCancelled?i={matched:!1,action:{type:"none",item:null}}:e.finalizeSources();let l=e.allSourceIds.map(c=>this._sourceSelector.find(g=>g.source.identifier==c)).filter(c=>!!c),r=this.potentialMatchers.indexOf(e);if(r!=-1){if(this.potentialMatchers.splice(r,1),i.action.type=="none"){this.finalizeMatcherlessTrackers(l);return}if(i.matched)for(let c of l){let g=this.matchersForSource(c.source);this.potentialMatchers=this.potentialMatchers.filter(d=>!g.find(u=>d==u)),g.forEach(d=>{d.cancel()}),this._sourceSelector=this._sourceSelector.filter(d=>!l.find(u=>d==u)),c.matchPromise.resolve({matcher:e,result:i})}else{let c=o(g=>{if(this.sustainMode&&!g.sustainWhenNested){this.finalizeMatcherlessTrackers(l);return}let d=new Vt(g,e);if(d.result&&d.result.matched==!1){this.finalizeMatcherlessTrackers(l);return}d.promise.then(this.matcherSelectionFilter(d,l.map(u=>u.matchPromise))),this.potentialMatchers.push(d),this.resetSourceHooks()},"replacer");this.emit("rejectionwithaction",{matcher:e,result:i},c);return}}})}finalizeMatcherlessTrackers(e){let n=e.map(i=>({tracker:i,pendingCount:this.potentialMatchers.filter(s=>!!s.allSourceIds.find(l=>i.source.identifier==l)).length}));for(let i of n)i.pendingCount==0&&!i.tracker.preserve&&i.tracker.matchPromise.resolve({matcher:null,result:{matched:!1,action:{type:"complete",item:null}}})}};o(Ar,"MatcherSelector");var gt=Ar;var fr=class fr{constructor(t,e){var s,l;let{matcher:n,result:i}=t;this.gestureSetId=e,this.matchedId=n==null?void 0:n.model.id,this.linkType=i.action.type,this.item=i.action.item,this.sources=n==null?void 0:n.sources,(s=this.sources)==null||s.forEach(r=>r.disconnect()),(l=this.sources)==null||l.sort((r,c)=>(n==null?void 0:n.primaryPath)==r?-1:(n==null?void 0:n.primaryPath)==c?1:0),this.allSourceIds=(n==null?void 0:n.allSourceIds)||[]}};o(fr,"GestureStageReport");var ti=fr,Lr=class Lr extends S.default{constructor(e,n,i,s){super();this.markedComplete=!1;this.selectionHandler=o(e=>W(this,null,function*(){var d,u,I,B,h,b,F,G,U,Q;let n=((d=this.pushedSelector)==null?void 0:d.baseGestureSetId)||((u=this.selector)==null?void 0:u.baseGestureSetId),i=new ti(e,n);e.matcher&&this.stageReports.push(i);let s=(I=e.matcher)!=null?I:this.stageReports[this.stageReports.length-1],l=(B=s==null?void 0:s.sources.map(p=>p instanceof re?p.baseSource:p))!=null?B:[],r=e.result.action.type;if((r=="complete"||r=="none")&&(l.forEach(p=>{p.isPathComplete||p.terminate(r=="none")}),!e.result.matched)){this.markedComplete||(this.markedComplete=!0,this.emit("complete"));return}if(r=="complete"&&this.touchpointCoordinator&&this.pushedSelector){let X=((h=this.touchpointCoordinator)==null?void 0:h.sustainSelectorSubstack(this.pushedSelector)).map(R=>{let L=new f;return R.path.on("invalidated",()=>L.resolve()),R.path.on("complete",()=>L.resolve()),L.corePromise});X.length>0&&e.result.action.awaitNested&&(yield Promise.all(X),yield ae(0)),(b=this.touchpointCoordinator)==null||b.popSelector(this.pushedSelector),this.pushedSelector=null}this.emit("stage",i,p=>{p.type=="pop"?l.forEach(X=>X.popRecognizerConfig()):l.forEach(X=>X.pushRecognizerConfig(p.config))});let c=!1;this.touchpointCoordinator&&(c=!this.touchpointCoordinator.selectorStackIncludes(this.selector));let g=Wr(e.result.action,this.gestureConfig,this.baseGestureSetId);if(c&&(g=g.filter(p=>p.sustainWhenNested)),g.length>0){if(!(r=="chain"&&e.result.action.selectionMode==((F=this.pushedSelector)==null?void 0:F.baseGestureSetId))){if(this.pushedSelector&&(this.pushedSelector.off("rejectionwithaction",this.modelResetHandler),(G=this.touchpointCoordinator)==null||G.popSelector(this.pushedSelector),this.pushedSelector=null),r=="chain"){let R=e.result.action.selectionMode;if(R){let L=new gt(R);L.on("rejectionwithaction",this.modelResetHandler),this.pushedSelector=L,(U=this.touchpointCoordinator)==null||U.pushSelector(L)}}}((Q=this.pushedSelector)!=null?Q:this.selector).matchGesture(e.matcher,g).then(R=>W(this,null,function*(){return this.selectionHandler(yield R.selectionPromise)}))}else this.markedComplete||(this.markedComplete=!0,this.emit("complete"))}),"selectionHandler");this.modelResetHandler=o((e,n)=>{let i=e.matcher.allSourceIds;if(!this.allSourceIds.find(s=>i.indexOf(s)==-1))if(e.result.action.type=="replace")n(St(this.gestureConfig,e.result.action.replace));else throw new Error("Missed a case in implementation!")},"modelResetHandler");this.stageReports=[],this.selector=i,this.selector.on("rejectionwithaction",this.modelResetHandler),this.once("complete",()=>{var l;this.pushedSelector&&((l=this.touchpointCoordinator)==null||l.popSelector(this.pushedSelector),this.pushedSelector=null),this.selector.off("rejectionwithaction",this.modelResetHandler),this.selector.dropSourcesWithIds(this.allSourceIds),this.selector=null}),this.gestureConfig=n,this.touchpointCoordinator=s,Promise.resolve().then(()=>this.selectionHandler(e))}get allSourceIds(){var e,n;return(n=(e=this.stageReports[this.stageReports.length-1])==null?void 0:e.allSourceIds)!=null?n:[]}get baseGestureSetId(){var e,n;return(n=(e=this.selector)==null?void 0:e.baseGestureSetId)!=null?n:null}get potentialModelMatchIds(){if(!this.selector)return[];let e=[this.selector];return this.pushedSelector&&e.push(this.pushedSelector),this.stageReports[this.stageReports.length-1].sources.map(l=>e.map(r=>r.potentialMatchersForSource(l).map(c=>c.model.id))).reduce((l,r)=>l.concat(r)).reduce((l,r)=>{for(let c of r)l.indexOf(c)==-1&&l.push(c);return l},[])}cancel(){this.stageReports[this.stageReports.length-1].sources.forEach(n=>n.baseSource.isPathComplete||n.baseSource.terminate(!0)),this.markedComplete||(this.markedComplete=!0,this.emit("complete"))}toJSON(){return this.stageReports}};o(Lr,"GestureSequence");var gn=Lr;function Wr(a,t,e){switch(a.type){case"none":case"complete":return[];case"replace":return[St(t,a.replace)];case"chain":return[St(t,a.next)];default:throw new Error("Unexpected case arose within `processGestureAction` method")}}o(Wr,"modelSetForAction");var Rr=class Rr extends S.default{constructor(e,n,i){super();this.selectorStack=[new gt];this._activeSources=[];this._activeGestures=[];this._history=[];this.modelResetHandler=o((e,n)=>{let i=e.matcher.allSourceIds;if(!this.activeGestures.find(s=>s.allSourceIds.find(l=>i.indexOf(l)!=-1)))if(e.result.action.type=="replace")n(St(this.gestureModelDefinitions,e.result.action.replace));else throw new Error("Missed a case in implementation!")},"modelResetHandler");this.onNewTrackedPath=o(e=>W(this,null,function*(){this.addSimpleSourceHooks(e);let n=this.gestureModelDefinitions,i,s;do{i=this.currentSelector;let I=yield i.matchGesture(e,yr(n,i.baseGestureSetId));if(I.sustainModeWithoutMatch){let B=o(h=>{h.stateToken=this.stateToken,h.item=e.currentRecognizerConfig.itemIdentifier(h,null)},"correctSample");e.path instanceof at&&e.path.coords.forEach(B),e.stateToken=this.stateToken,e.baseItem=e.path.stats.initialSample.item,B(e.path.stats.initialSample),B(e.path.stats.lastSample);continue}else{s=I.selectionPromise;break}}while(i!=this.currentSelector);let l=this.currentSelector;e.setGestureMatchInspector(this.buildGestureMatchInspector(l));let r=o(()=>{this.recordHistory(e)},"preGestureScribe");try{e.path.on("invalidated",r),this.emit("inputstart",e)}catch(u){Le("Error from 'inputstart' event listener",u)}this.inputEngines.forEach(u=>{u.fulfillInputStart(e)});let c=yield s;if(!c||c.result.matched==!1)return;let g=c.matcher.allSourceIds;for(let u of this._activeGestures)if(u.allSourceIds.find(I=>!!g.find(B=>I==B)))return;let d=new gn(c,n,this.currentSelector,this);this._activeGestures.push(d),d.on("complete",()=>{let u=this._activeGestures.indexOf(d);u!=-1&&this._activeGestures.splice(u,1)}),e.path.wasCancelled||(e.path.off("invalidated",r),d.on("complete",()=>this.recordHistory(d))),this.emit("recognizedgesture",d)}),"onNewTrackedPath");if(this.historyMax=i>0?i:0,this.gestureModelDefinitions=e,this.inputEngines=[],n)for(let s of n)this.addEngine(s);this.selectorStack[0].on("rejectionwithaction",this.modelResetHandler)}pushSelector(e){this.selectorStack.push(e),e.on("rejectionwithaction",this.modelResetHandler)}sustainSelectorSubstack(e){if(!e)return[];let n=this.selectorStack.indexOf(e);if(n==-1)return[];if(this.selectorStack.length<=1)throw new Error("May not force the original, base gesture selector into sustain mode.");let i=[];for(let s=n;s{let i=this.selectorStack.indexOf(e);return this.selectorStack.slice(i).map(l=>l.potentialMatchersForSource(n).map(r=>r.model.id)).reduce((l,r)=>l.concat(r))}}addEngine(e){e.on("pointstart",this.onNewTrackedPath),this.inputEngines.push(e)}recordHistory(e){let n=this.historyMax;n>0&&(this._history.length==n&&this._history.shift(),this._history.push(e))}get activeGestures(){return[].concat(this._activeGestures)}get activeSources(){return[].concat(this.inputEngines.map(e=>e.activeSources).reduce((e,n)=>e.concat(n),[]))}get history(){return this._history}get historyJSON(){let e=o(function(n,i){return n=="item"?i==null?void 0:i.id:i},"sanitizingReplacer");return JSON.stringify(this.history,e,2)}get stateToken(){return this._stateToken}set stateToken(e){this._stateToken=e,this.inputEngines.forEach(n=>n.stateToken=e),this.currentSelector.stateToken=e}addSimpleSourceHooks(e){e.path.on("invalidated",()=>{let n=this.activeGestures.find(s=>s.allSourceIds.includes(e.identifier));n&&n.cancel();let i=this._activeSources.indexOf(e);this._activeSources=this._activeSources.splice(i,1)}),e.path.on("complete",()=>{let n=this._activeSources.indexOf(e);this._activeSources=this._activeSources.splice(n,1)})}};o(Rr,"TouchpointCoordinator");var Fs=Rr;var ms={};Sn(ms,{EMPTY_GESTURE_DEFS:()=>pr,getGestureModel:()=>St,getGestureModelSet:()=>yr});var vr=class vr extends Fs{constructor(e,n){let i=gs(n);e=e||pr;super(e,null,i.historyLength);this.config=i,this.mouseEngine=new bs(this.config),this.touchEngine=new hs(this.config),this.mouseEngine.registerEventHandlers(),this.touchEngine.registerEventHandlers(),this.addEngine(this.mouseEngine),this.addEngine(this.touchEngine)}destroy(){this.activeGestures.forEach(e=>e.cancel()),this.activeSources.forEach(e=>e.terminate(!0)),this.mouseEngine.unregisterEventHandlers(),this.touchEngine.unregisterEventHandlers(),this.mouseEngine=null,this.touchEngine=null}};o(vr,"GestureRecognizer");var At=vr;var Hr={};Sn(Hr,{matchers:()=>ys,specs:()=>ms});var ys={};Sn(ys,{GestureMatcher:()=>Vt,GestureSequence:()=>gn,GestureStageReport:()=>ti,MatcherSelector:()=>gt,PathMatcher:()=>an,modelSetForAction:()=>Wr});function ps(a,t){let e=new Map;return t.keys.forEach(n=>{let i=Math.abs(a.x-n.centerX),s=Math.abs(a.y-n.centerY),l,r;i>.5*n.width?(l=i-.5*n.width,i=.5):(l=0,i/=n.width),s>.5*n.height?(r=s-.5*n.height,s=.5):(r=0,s/=n.height),l*=t.kbdScaleRatio,l+=i*n.height,r+=s*n.height;let c=l*l+r*r;e.set(n.keySpec,c)}),e}o(ps,"keyTouchDistances");function dt(a){var i;let t=new Map,e=0;Array.isArray(a)||(a=[a]);for(let s of a)for(let l of s.keys()){let r=1/(Math.pow(s.get(l),2)+3e-5);e+=r,t.set(l,(i=t.get(l))!=null?i:0+r)}let n=[];for(let s of t.keys())n.push({keySpec:s,p:t.get(s)/e});return n.sort(function(s,l){return l.p-s.p})}o(dt,"distributionFromDistanceMaps");var Jr=["n","ne","e","se","s","sw","w","nw"],Ao=Math.PI,kr=(()=>{let a=new Map,t=Ao/4;for(let e=0;e{let s=Cs(t),l=n.flick.triggerDist-n.flick.dirLockDist,r=Math.max(0,ii(a.path.stats,t)-n.flick.dirLockDist),g=Math.min(1,.7*r/l),d=Math.sin(s)*g,u=-Math.cos(s)*g;e==null||e.scrollFlickPreview(d,u)}}o(Aa,"buildFlickScroller");var Gs=Math.PI/3,Nr=class Nr{constructor(t,e,n,i,s,l){this.directlyEmitsKeys=!0;this.sequence=t,this.gestureParams=s,this.baseSpec=i.key.spec,this.baseKeyDistances=n.getSimpleTapCorrectionDistances(t.stageReports[0].sources[0].path.stats.initialSample,this.baseSpec);let r=t.stageReports[0].sources[0].baseSource,c=r;t.on("complete",()=>{l==null||l.cancel()}),this.sequence.on("stage",d=>{var h;let u=c.path.stats;this.computedFlickDistribution=this.flickDistribution(u,!0);let I=this.computedFlickDistribution[0].keySpec;if(d.matchedId=="flick-restart"){c.path.replaceInitialSample(d.sources[0].path.stats.initialSample);return}if(d.matchedId=="flick-reset-centering"){c=r.constructSubview(!0,!0);return}else if(d.matchedId=="flick-reset-end"){this.emitKey(n,this.baseSpec,c.path.stats);return}else if(d.matchedId=="flick-reset"){this.flickScroller&&(this.flickScroller(c.currentSample),c.path.off("step",this.flickScroller)),this.lockedDir=null,this.lockedSelectable=null,c instanceof re&&c.disconnect();return}else if(d.matchedId=="flick-mid"){if(I==this.baseSpec)return;let b=Object.keys(this.baseSpec.flick).find(F=>this.baseSpec.flick[F]==I);this.lockedDir=b,this.lockedSelectable=I,this.flickScroller&&c.path.off("step",this.flickScroller),this.flickScroller=Aa(c,b,l,this.gestureParams),this.flickScroller(c.currentSample),c.path.on("step",this.flickScroller);return}let B=(h=this.lockedSelectable)!=null?h:I;this.emitKey(n,B,u)});let g=this.buildPopupRecognitionConfig(n);e({type:"push",config:g})}emitKey(t,e,n){let i;ii(n,this.lockedDir)>this.gestureParams.flick.triggerDist?i=t.keyEventFromSpec(e):i=t.keyEventFromSpec(this.baseSpec),i.keyDistribution=this.currentStageKeyDistribution(this.baseKeyDistances),t.raiseKeyEvent(i,null)}buildPopupRecognitionConfig(t){let e={getBoundingClientRect(){let n=Number.MAX_SAFE_INTEGER;return new DOMRect(-n,-n,2*n,2*n)}};return A(y({},t.gestureEngine.config),{maxRoamingBounds:e,safeBounds:e})}cancel(){}flickDistribution(t,e){let n=this.baseSpec.flick,i=[{spec:this.baseSpec,coord:[NaN,0]}];i=i.concat(Object.keys(n).map(I=>({spec:n[I],coord:kr.get(I)})));let s=t.angle,l=this.gestureParams.flick.triggerDist,c=Math.min(l,e?l:t.netDistance)/l,g=0,d=i.map(I=>{let B=0,h=I.coord;if(!isNaN(h[0])){let U=s-h[0],Q=2*Ao+h[0]-s;B=Math.min(U*U,Q*Q)}let b=Gs*(h[1]-c),F=b*b,G=1/(B+F+1e-6);return g+=G,{keySpec:I.spec,p:G}}),u=1/g;return d.forEach(I=>I.p*=u),d.sort((I,B)=>B.p-I.p)}currentStageKeyDistribution(t){let e=this.baseSpec,n=this.baseKeyDistances,i=this.computedFlickDistribution;if(!n.get(e))return[{keySpec:i[0].keySpec,p:1}];let l=i.findIndex(g=>g.keySpec==e),r=i.splice(l,1)[0].p,c=dt(n);return i.concat(c.map(g=>({keySpec:g.keySpec,p:g.p*r})))}};o(Nr,"Flick");var ni=Nr;var Qs=ma.TouchLayoutKeySp,si={longpress:{flickDistStart:8,flickDistFinal:40,waitLength:500,noiseTolerance:10},multitap:{waitLength:300,holdLength:150},flick:{startDist:10,dirLockDist:25,triggerDist:40}};function Wo(a){let t=a.getBoundingClientRect();return{clientX:t.left+t.width/2,clientY:t.top+t.height/2}}o(Wo,"getKeyCentroid");function fo(a){let t=Wo(a);return e=>{let n=e.lastSample.clientX-t.clientX,i=e.lastSample.clientY-t.clientY;return Math.sqrt(n*n+i*i)}}o(fo,"buildDistFromKeyCentroidFunctor");function li(a){let t=a.key.spec;if(t.sk)return!1;let e=["K_SHIFT","K_ALT","K_CTRL","K_NUMERALS","K_SYMBOLS","K_CURRENCIES"];for(let n of e)if(t.id==n)return!0;if(t.nextlayer)switch(t.sp){case Qs.special:case Qs.specialActive:case Qs.customSpecial:case Qs.customSpecialActive:return!0;default:return!1}else return!1}o(li,"keySupportsModipress");function Er(a,t){let e=o((b,F)=>{if(!b)return!1;let G=b.key.spec;switch(F){case"modipress-start":return li(b);case"special-key-start":return["K_LOPT","K_ROPT","K_BKSP"].indexOf(G.baseKeyID)!=-1;case"longpress":return!0;case"multitap-start":case"modipress-multitap-start":return a.hasMultitaps?!!G.multitap:!1;case"flick-start":return!!G.flick;default:return!0}},"gestureKeyFilter"),n=t;n.longpress.permitsFlick=b=>{let F=b==null?void 0:b.key.spec.flick;return!F||!(F.n||F.nw||F.ne)};let i=n.roamingEnabled=!a.hasFlicks,s=ce(i?Pa(n):ko(n)),l=ce(i?Tr(n):No(n)),r=d(ce(Ho(n,!0,i)),0),c=d(Da(n),0),g=d(tg(n),0);Object.defineProperty(r.contacts[0].model.timer,"duration",{get:()=>n.longpress.waitLength}),Object.defineProperty(c.sustainTimer,"duration",{get:()=>n.multitap.waitLength}),Object.defineProperty(g.sustainTimer,"duration",{get:()=>n.multitap.waitLength});function d(b,F){b=ce(b);let G=b.id;return typeof F=="number"&&(F=[F]),b.contacts.forEach((U,Q)=>{var p;if(F.indexOf(Q)!=-1){let X=(p=U.model.allowsInitialState)!=null?p:()=>!0;U.model=A(y({},U.model),{allowsInitialState:(R,L,T)=>e(T,G)&&X(R,L,T)})}}),b}o(d,"withKeySpecFiltering");let u=Ja(),I=_a(),B=[r,c,Oa(n),s,l,d(u,0),ka(n),ja(),d(I,0),qa(n),eg(),$a(),g,ng(n),ig()],h=[r.id,s.id,I.id,u.id];return i?(B.push(d(Na(n),0)),B.push(Ea())):(B.push(d(Jo(n),0)),B.push(Ta(n)),B.push(wa(n)),B.push(Ma(n)),B.push(Ya(n)),B.push(Ka()),B.push(za(n)),h.push("flick-start")),{gestures:B,sets:{default:h,modipress:h.filter(b=>b!=I.id),none:[]}}}o(Er,"gestureSetForLayout");function Yr(){return{itemPriority:0,pathResolutionAction:"reject",pathModel:{evaluate:a=>"resolve"}}}o(Yr,"instantContactRejectionModel");function Ke(){return{itemPriority:0,pathResolutionAction:"resolve",pathModel:{evaluate:a=>"resolve"}}}o(Ke,"instantContactResolutionModel");function Wa(a){let t=a.flick;return{itemPriority:1,pathModel:{evaluate:(e,n,i)=>{let s=e.stats,l=i==null?void 0:i.key.spec;if(l&&l.sk){let r=l.flick;if(!(r.nw||r.n||r.ne)){let g=s.netDistance,d=s.angle;if(g*Math.cos(d)>a.longpress.flickDistStart)return"reject"}}return s.netDistance>t.startDist?"resolve":null}},pathResolutionAction:"resolve",pathInheritance:"partial"}}o(Wa,"flickStartContactModel");function Lo(a,t){let e=t.key.spec.flick,n=Object.keys(e),i,s=0;for(let l of n){let r=ii(a,l);r>s&&(s=r,i=l)}return{dir:i,dist:s}}o(Lo,"determineLockFromStats");function fa(a){return{itemPriority:1,pathModel:{evaluate:(t,e,n)=>{let{dir:i,dist:s}=Lo(t.stats,n);if(s>a.flick.dirLockDist){let l=t.stats.angle,r=Cs(i),c=Math.abs(l-r),g=Math.abs(2*Math.PI+r-l);if(c<=Gs||g<=Gs)return"resolve"}else if(t.isComplete)return"reject"}},pathResolutionAction:"resolve",pathInheritance:"full"}}o(fa,"flickMidContactModel");function La(a){return{itemPriority:1,pathModel:{evaluate:(t,e,n,i)=>{if(t.isComplete)return"resolve";{let{dir:s}=Lo(i,n);if(ii(t.stats,s)!!(s!=null&&s.key.spec.sk),pathModel:{evaluate:i=>{var l;let s=i.stats;if(t&&n.permitsFlick(s.lastSample.item)&&((l=s.cardinalDirection)==null?void 0:l.indexOf("n"))!=-1){let r=s.netDistance,c=s.angle;if(r*Math.cos(c)>n.flickDistFinal)return"resolve"}else if(e){if(s.rawDistance>n.noiseTolerance||s.lastSample.item!=s.initialSample.item)return"reject"}else if(s.lastSample.item!=s.initialSample.item)return"reject";return i.isComplete?"reject":null}}}}o(Ra,"longpressContactModel");function Ro(){return{itemPriority:-1,pathResolutionAction:"resolve",pathModel:{evaluate:a=>"resolve"}}}o(Ro,"modipressContactStartModel");function va(){return{itemPriority:-1,itemChangeAction:"resolve",pathResolutionAction:"resolve",pathModel:{evaluate:a=>{if(a.isComplete)return"reject"}}}}o(va,"modipressContactHoldModel");function vo(){return{itemPriority:-1,itemChangeAction:"resolve",pathResolutionAction:"resolve",pathModel:{evaluate:a=>{if(a.isComplete)return"resolve"}}}}o(vo,"modipressContactEndModel");function Us(a,t){var n;let e=(n=a==null?void 0:a.roamingEnabled)!=null?n:!0;return{itemPriority:0,itemChangeAction:e?"reject":void 0,pathResolutionAction:"resolve",pathInheritance:!e&&t?"full":"chop",pathModel:{evaluate:i=>{if(i.isComplete&&!i.wasCancelled)return"resolve"}}}}o(Us,"simpleTapContactModel");function Ha(){return{itemPriority:0,pathResolutionAction:"resolve",pathModel:{evaluate:a=>{if(a.isComplete&&!a.wasCancelled)return"resolve"}}}}o(Ha,"subkeySelectContactModel");function Ja(){return{id:"special-key-start",resolutionPriority:0,contacts:[{model:y({},Ke()),endOnResolve:!1}],resolutionAction:{type:"chain",next:"special-key-end",item:"current"}}}o(Ja,"specialKeyStartModel");function ka(a){return{id:"special-key-end",resolutionPriority:0,contacts:[{model:A(y({},Us(a)),{itemChangeAction:"resolve"}),endOnResolve:!0}],resolutionAction:{type:"complete",item:"none"}}}o(ka,"specialKeyEndModel");function Ho(a,t,e){let n={id:"longpress",resolutionPriority:4,contacts:[{model:A(y({},Ra(a,t,e)),{itemPriority:1,pathInheritance:"chop"}),endOnResolve:!1},{model:Yr(),resetOnInstantFulfill:!0}],resolutionAction:{type:"chain",next:"subkey-select",selectionMode:"none",item:"none"}};return e?A(y({},n),{rejectionActions:{path:{type:"replace",replace:"longpress-roam"},timer:{type:"replace",replace:"longpress-roam-restore"}}}):n}o(Ho,"longpressModel");function Na(a){let t=Ho(a,!1,!0);return A(y({},t),{id:"longpress-roam"})}o(Na,"longpressModelAfterRoaming");function Ea(){return{id:"longpress-roam-restore",contacts:[{model:{pathModel:{evaluate:a=>null},itemChangeAction:"reject",pathInheritance:"full",pathResolutionAction:"reject",itemPriority:0}}],resolutionPriority:-1,rejectionActions:{item:{type:"replace",replace:"longpress-roam"}},resolutionAction:{type:"chain",next:"longpress-roam"}}}o(Ea,"longpressRoamRestoration");function Jo(a){return{id:"flick-start",resolutionPriority:3,contacts:[{model:Wa(a)}],resolutionAction:{type:"chain",item:"none",next:"flick-mid"}}}o(Jo,"flickStartModel");function Ya(a){let t=Jo(a);return A(y({},t),{contacts:[A(y({},t.contacts[0]),{model:A(y({},t.contacts[0].model),{baseCoordReplacer:(e,n)=>{let i=Wo(n),s=fo(n),l=e.initialSample,r=s(e);if(r>a.flick.triggerDist)return i;let c=a.flick.dirLockDist;if(rsg}};o(wr,"BannerScrollState");var Xs=wr;var wo="kmw-suggest-touched",lg="kmw-suggest-banner-scroller",Mo=.666,Ko="swallow-fade-transition",ci=class ci{constructor(t,e){this.index=t,this.rtl=e!=null?e:!1,this.constructRoot();let n=this.display=H("span");n.className="kmw-suggestion-text",this.container.appendChild(n)}get computedStyle(){return getComputedStyle(this.display)}constructRoot(){let t=this.div=H("div");t.className="kmw-suggest-option",t.id=ci.BASE_ID+this.index,this.div.suggestion=this;let e=this.container=document.createElement("div");e.className="kmw-suggestion-container";let i=(100-He.MARGIN*(He.LONG_SUGGESTION_DISPLAY_LIMIT-1))/He.LONG_SUGGESTION_DISPLAY_LIMIT;e.style.minWidth=i+"%",t.appendChild(e)}matchKeyboardProperties(t){let e=this.div;if(t){t.KLC&&(e.lang=t.KLC);let n=t.KFont;n&&n.family&&n.family!=""&&(e.style.fontFamily=n.family)}}get suggestion(){return this._suggestion}update(t,e){this._suggestion=t;let n=this.generateSuggestionText(this.rtl);if(this.container.replaceChild(n,this.display),this.display=n,e.minWidth!==void 0&&(this._minWidth=e.minWidth),this._paddingWidth=e.paddingWidth,this._collapsedWidth=e.collapsedWidth,t&&t.displayAs){let i=Zs(t.displayAs,e.emSize,e.styleForFont);this._textWidth=i.width}else this._textWidth=0;this.currentWidth=this.collapsedWidth,this.highlight(t==null?void 0:t.autoAccept),this.updateLayout()}updateLayout(){if(!this.suggestion&&this.index!=0){this.div.style.width="0px";return}else this.div.style.width="";let t=this.container.style;t.minWidth=this.collapsedWidth+"px",this.rtl?t.marginRight=this.collapsedWidth-this.expandedWidth+"px":t.marginLeft=this.collapsedWidth-this.expandedWidth+"px",this.updateFade()}updateFade(){this.div.classList.add(Ko),window.requestAnimationFrame(()=>{this.div.classList.remove(Ko)}),this.div.classList.add(`kmw-hide-fade-${this.rtl?"left":"right"}`);let t=`kmw-hide-fade-${this.rtl?"right":"left"}`;this.expandedWidth-this.collapsedWidth?this.div.classList.remove(t):this.div.classList.add(t)}get targetCollapsedWidth(){return this._collapsedWidth}get textWidth(){return this._textWidth}get paddingWidth(){return this._paddingWidth}get minWidth(){return this._minWidth}set minWidth(t){this._minWidth=t}get expandedWidth(){return this.minWidth>this.spanWidth?this.minWidth:this.spanWidth}get spanWidth(){var e,n;let t=(e=this.textWidth)!=null?e:0;return t&&(t+=(n=this.paddingWidth)!=null?n:0),t}get collapsedWidth(){let t=this.spanWidthe?this.minWidth:e}get currentWidth(){return this.div.offsetWidth}set currentWidth(t){tthis.expandedWidth&&(t=this.expandedWidth),this.rtl?this.container.style.marginRight=`${t-this.expandedWidth}px`:this.container.style.marginLeft=`${t-this.expandedWidth}px`}highlight(t){let e=this.div;t?e.classList.add(wo):e.classList.remove(wo)}isEmpty(){return!this._suggestion}generateSuggestionText(t){let e=this._suggestion;var n,i=H("span");if(i.className="kmw-suggestion-text",e==null)return i;if(e.displayAs==null||e.displayAs=="")n=" ";else{let s=t?8238:8237;n=String.fromCharCode(s)+e.displayAs}return i.innerHTML=n,i}};o(ci,"BannerSuggestion"),ci.BASE_ID="kmw-suggestion-";var Mr=ci,D=class D extends de{constructor(e,n){super(n||de.DEFAULT_HEIGHT);this.type="suggestion";this.currentSuggestions=[];this.options=[];this.separators=[];this.isRTL=!1;this.onSuggestionUpdate=o(e=>{var I;this.currentSuggestions=e,(I=this.highlightAnimation)==null||I.cancel();let n=this.options[0].computedStyle,i={fontSize:n.fontSize,fontFamily:n.fontFamily},s=getComputedStyle(document.body).fontSize,l=xs(s).val,r=getComputedStyle(this.options[0].container.firstChild),c=this.width/D.LONG_SUGGESTION_DISPLAY_LIMIT,g=new x(r.paddingLeft||"4px"),d=new x(r.paddingRight||"4px"),u={paddingWidth:g.val+d.val,emSize:l,styleForFont:i,collapsedWidth:c,minWidth:0};for(let B=0;BB){let b=e[B];h.update(b,u)}else h.update(null,u)}this.refreshLayout()},"onSuggestionUpdate");this.refreshLayout=o(()=>{let e=[],n=0,i=Math.min(this.currentSuggestions.length,8);for(let s=0;s0;){let r=(this.width-n-s)/e.length;e.sort((u,I)=>u.expandedWidth-I.expandedWidth);let c=e[0],g=c.expandedWidth-c.collapsedWidth,d=Math.min(g,r);d>0&&(e.forEach(u=>u.minWidth=u.collapsedWidth+d),n+=d*e.length),e.splice(0,1)}let l=(this.width-n-s)/i;for(let r=0;r0&&(this.options=[],this.separators=[]);for(var n=0;n{let g=this.selectionBounds.getBoundingClientRect();if(c.clientXg.right||c.clientYg.bottom)return null;let d=null,u=Number.MAX_VALUE;for(let I of this.options){let B=I.div.getBoundingClientRect();if(B.left<=c.clientX&&c.clientX{c.highlight(!0),this.highlightAnimation&&(this.highlightAnimation.cancel(),this.highlightAnimation.decouple()),this.highlightAnimation=new Ss(this.container,c,!1),this.highlightAnimation.expand()},"markSelection"),r=o(c=>{c.highlight(!1),this.highlightAnimation||(this.highlightAnimation=new Ss(this.container,c,!1)),this.highlightAnimation.collapse()},"clearSelection");return i.on("inputstart",c=>{if(s.source){c.terminate(!0);return}let g=this._predictionContext.selected;this._predictionContext.selected=null,g&&this.options.forEach(I=>{I.suggestion==g&&I.highlight(!1)}),this.scrollState=new Xs(c.currentSample,this.container.scrollLeft);let d=c.baseItem;s.source=c,s.scrollingHandler=I=>{var b;let B=this.scrollState.updateTo(I);(b=this.highlightAnimation)==null||b.setBaseScroll(B);let h=I.item?d:null;h!=s.suggestion&&(s.suggestion&&r(s.suggestion),s.suggestion=h,h&&l(h))},s.suggestion=c.currentSample.item,s.suggestion&&l(s.suggestion);let u=o(()=>{let I=this.currentSuggestions;ae(0).then(()=>W(this,null,function*(){if(I==this.currentSuggestions&&(this._predictionContext.selected=g,g)){for(let B of this.options)if(B.suggestion==g){B.highlight(!0);break}}})),s.suggestion&&(r(s.suggestion),s.suggestion=null),s.source=null,s.scrollingHandler=null},"terminationHandler");c.path.on("complete",u),c.path.on("invalidated",u),c.path.on("step",s.scrollingHandler)}),i.on("recognizedgesture",c=>{c.once("stage",g=>{let d=g.item;d&&!this.scrollState.hasScrolled&&this.currentSuggestions.length>0&&(this.currentSuggestions=[],this.predictionContext.accept(d.suggestion).then(()=>{this.container.scrollLeft=this.isRTL?this.container.scrollWidth:0})),this.scrollState=null})}),i}update(){var n;let e=super.update();return(n=this.selectionBounds)==null||n.updatePadding([-Mo*this.height,-Number.MAX_SAFE_INTEGER]),e}configureForKeyboard(e,n){let i=e.isRTL;this.container.textContent="",this.buildInternals(i),this.options.forEach(s=>s.matchKeyboardProperties(n)),this.onSuggestionUpdate(this.currentSuggestions)}get predictionContext(){return this._predictionContext}set predictionContext(e){this._predictionContext&&this._predictionContext.off("update",this.onSuggestionUpdate),this._predictionContext=e,e&&(e.on("update",this.onSuggestionUpdate),this.onSuggestionUpdate(e.currentSuggestions))}};o(D,"SuggestionBanner"),D.SUGGESTION_LIMIT=8,D.LONG_SUGGESTION_DISPLAY_LIMIT=3,D.MARGIN=1;var He=D,Fe=class Fe{constructor(t,e,n){this.setScrollOffset=o(()=>{if(!this.scrollContainer)return;let t=this.option.currentWidth-this.option.collapsedWidth,e=this.option.rtl,n=Math.max(this.rootScrollOffset-this.option.div.offsetLeft,0),i=Math.max(this.option.div.offsetLeft+this.option.collapsedWidth-(this.rootScrollOffset+this.scrollContainer.offsetWidth)),s=Math.max(e?i:n,0),l=Math.max(this.collapsedScrollOffset+(e?0:1)*t,0)+(e?0:-1)*s,r=Math.max(this.rootScrollOffset+(e?0:1)*t,0)+(e?0:-1)*s,c=e?Math.max(l,r):Math.min(l,r),g=Math.max(e?this.option.div.offsetLeft+this.option.currentWidth-(c+this.scrollContainer.offsetWidth):c-this.option.div.offsetLeft,0),d=Math.min(t,g),u=l+(e?1:-1)*d+(e?0:1)*s;if(this.scrollContainer.scrollLeft=u,this.pendingAnimation){let I=this.scrollContainer.scrollLeft-u;this.option.currentWidth+=I}},"setScrollOffset");this._expand=o(t=>{if(this.startTimestamp===void 0)return;let e=t-this.startTimestamp,n=e>Fe.TRANSITION_TIME;n&&(e=Fe.TRANSITION_TIME);let i=this.option.expandedWidth-this.option.collapsedWidth,s=e/Fe.TRANSITION_TIME,l=i*s;this.option.currentWidth=l+this.option.collapsedWidth,n?this.clear():this.pendingAnimation=window.requestAnimationFrame(this._expand),this.setScrollOffset()},"_expand");this._collapse=o(t=>{if(this.startTimestamp===void 0)return;let e=t-this.startTimestamp,n=e>Fe.TRANSITION_TIME;n&&(e=Fe.TRANSITION_TIME);let i=this.option.expandedWidth-this.option.collapsedWidth,s=1-e/Fe.TRANSITION_TIME,l=i*s;this.option.currentWidth=l+this.option.collapsedWidth,n?this.clear():this.pendingAnimation=window.requestAnimationFrame(this._collapse),this.setScrollOffset()},"_collapse");this.scrollContainer=t,this.option=e,this.collapsedScrollOffset=t.scrollLeft,this.rootScrollOffset=t.scrollLeft}setBaseScroll(t){this.collapsedScrollOffset=t,this.option.rtl?t>this.rootScrollOffset&&(this.rootScrollOffset=t):te.key.map(n=>new Or(a,e,n))).reduce((e,n)=>e.concat(n),[]).filter(e=>rg(e.keySpec)),kbdScaleRatio:t}}o(zo,"buildCorrectiveLayout");var cg={"*Shift*":8,"*Enter*":5,"*Tab*":6,"*BkSp*":4,"*Menu*":11,"*Hide*":10,"*Alt*":25,"*Ctrl*":1,"*Caps*":3,"*ABC*":16,"*abc*":17,"*123*":19,"*Symbol*":21,"*Currency*":20,"*Shifted*":9,"*AltGr*":2,"*TabLeft*":7,"*LAlt*":86,"*RAlt*":87,"*LCtrl*":88,"*RCtrl*":89,"*LAltCtrl*":96,"*RAltCtrl*":97,"*LAltCtrlShift*":98,"*RAltCtrlShift*":99,"*AltShift*":100,"*CtrlShift*":101,"*AltCtrlShift*":102,"*LAltShift*":103,"*RAltShift*":104,"*LCtrlShift*":105,"*RCtrlShift*":112,"*LTREnter*":5,"*LTRBkSp*":4,"*RTLEnter*":113,"*RTLBkSp*":114,"*ShiftLock*":115,"*ShiftedLock*":116,"*ZWNJ*":117,"*ZWNJiOS*":117,"*ZWNJAndroid*":118,"*ZWNJGeneric*":121,"*Sp*":128,"*NBSp*":130,"*NarNBSp*":131,"*EnQ*":132,"*EmQ*":133,"*EnSp*":134,"*EmSp*":135,"*PunctSp*":140,"*ThSp*":141,"*HSp*":142,"*ZWSp*":129,"*ZWJ*":119,"*WJ*":120,"*CGJ*":122,"*LTRM*":144,"*RTLM*":145,"*SH*":161,"*HTab*":162},jr=cg;var og=["default","shift","shift-on","special","special-on","","","","deadkey","blank","hidden"],ft=og;function Lt(a,t){switch(a){case"*ZWNJ*":a=t.device.OS==V.OperatingSystem.Android?"*ZWNJAndroid*":"*ZWNJiOS*";break;case"*Enter*":a=t.isRTL?"*RTLEnter*":"*LTREnter*";break;case"*BkSp*":a=t.isRTL?"*RTLBkSp*":"*LTRBkSp*";break;default:}let e=jr[a],n=57344+e;return e?String.fromCharCode(n):a}o(Lt,"renameSpecialKey");var ze=class ze{constructor(t,e){this.spec=t,this.layer=e}setButtonClass(){var i;let t=this.spec,e=this.btn;var n=0;typeof t.dk=="string"&&t.dk=="1"&&(n=8),n=(i=t.sp)!=null?i:n,(n<0||n>10)&&(n=0),e.className="kmw-key kmw-key-"+ft[n]}setToggleState(t){let e;switch(e=this.spec.sp,ft[e]){case"shift":case"shift-on":t===void 0&&(t=ft[e]=="shift"),this.spec.sp=1+(t?1:0);break;case"special":case"special-on":t===void 0&&(t=ft[e]=="special"),this.spec.sp=3+(t?1:0);break;default:return}this.setButtonClass()}isFrameKey(){let t=this.spec.sp||0;switch(ft[t]){case"default":case"deadkey":return!1;default:return!0}}allowsKeyTip(){return this.isFrameKey()?!1:!this.btn.classList.contains("kmw-spacebar")}highlight(t){var e=this.btn.classList;t?e.contains(ze.HIGHLIGHT_CLASS)||e.add(ze.HIGHLIGHT_CLASS):e.remove(ze.HIGHLIGHT_CLASS)}getIdealFontSize(t,e,n){if(!this._fontFamily)return new x("1em");n!=null||(n=1);let i=e.keyWidth,s=e.keyHeight,l=e.baseEmFontSize.scaledBy(e.layoutFontSize.val),r=this._fontSize;r.absolute||(r=l.scaledBy(r.val));let c={fontFamily:this._fontFamily,fontSize:r.styleString,height:e.keyHeight},g=Zs(t,l.scaledBy(n).val,c),d=.9,u=.9,I=2;var B;g.fontBoundingBoxAscent&&(B=g.fontBoundingBoxAscent+g.fontBoundingBoxDescent);let h=B!=null?B:0,b=i*d/(g.width+I),F=h&&s?s*u/h:void 0;var G=b;return F&&F90)&&(e=0)}let n=document.createElement("div");return n.className="kmw-key-label",e>0&&(n.innerText=String.fromCharCode(e)),n}processSubkeys(e,n){var i,s=e.subKeys=this.spec.sk;for(i=0;i{this.setPreview(null)}),this.btn.replaceChild(this.preview,n)}refreshLayout(e){super.refreshLayout(e),e.baseEmFontSize.val<12?this.capLabel.style.fontSize="6px":this.capLabel.style.fontSize=x.forScalar(.5).styleString}get displaysKeyCap(){return this.capLabel&&this.capLabel.style.display=="block"}set displaysKeyCap(e){if(!this.capLabel)throw new Error("Key element not yet constructed; cannot display key cap");this.capLabel.style.display=e?"block":"none"}};o(qr,"OSKBaseKey");var gi=qr;var fs=.15,$r=class $r{constructor(t,e,n){let i=this.element=document.createElement("div");i.className="kmw-key-row",this.heightFraction=1/e.row.length;let s=n.key;this.spec=n,this.keys=[];for(let c=0;c0)return this.keys[0].displaysKeyCap}set displaysKeyCaps(t){for(let e of this.keys)e.displaysKeyCap=t}refreshLayout(t){let e=this.element.style,n=t.heightStyle.scaledBy(this.heightFraction);e.maxHeight=e.lineHeight=e.height=n.styleString;let i=t.heightStyle.absolute?n:x.forScalar(1),s=i.scaledBy(fs/2),l=i.scaledBy(1-fs);this.keys.forEach(r=>{let c=r.square,g=r.btn,d=c.style;d.height=d.minHeight=i.styleString;let u=g.style;u.top=s.styleString,u.height=u.lineHeight=u.minHeight=l.styleString})}buildKeyLayout(t,e){let n=t.widthStyle.scaledBy(e.spec.proportionalWidth),i=t.heightStyle.scaledBy(this.heightFraction).scaledBy(1-fs);return{keyWidth:n.val*(n.absolute?1:t.keyboardWidth),keyHeight:i.val*(i.absolute?1:t.keyboardHeight),baseEmFontSize:t.baseEmFontSize,layoutFontSize:t.layoutFontSize}}detectStyles(t){this.keys.forEach(e=>{e.detectStyles(this.buildKeyLayout(t,e))})}refreshKeyLayouts(t){this.keys.forEach(e=>{var u;let n=e.btn,i=t.widthStyle,s=t.heightStyle,l=i.scaledBy(e.spec.proportionalWidth),r=i.scaledBy(e.spec.proportionalPad),c=s.scaledBy(this.heightFraction).scaledBy(1-fs),g=s.absolute?c.styleString:"100%",d=this.buildKeyLayout(t,e);(u=n.key)==null||u.refreshLayout(d),e.square.style.width=l.styleString,e.square.style.marginLeft=r.styleString,e.btn.style.width=i.absolute?l.styleString:"100%",e.square.style.height=g})}};o($r,"OSKRow");var di=$r;var ec=class ec{get rowHeight(){return this._rowHeight}get id(){return this.spec.id}constructor(t,e,n){this.spec=n;let i=this.element=document.createElement("div"),s=i.style;i.className="kmw-key-layer";var l=n.row.length;l>4&&t.device.formFactor=="phone"&&(i.className=i.className+" kmw-5rows"),s.fontFamily="font"in e?e.font:"",this.nextlayer=n.id,i.layer=n.id,typeof n.nextlayer=="string"&&(i.nextLayer=this.nextlayer=n.nextlayer);let r=n.row;this.rows=[];for(let c=0;cl.detectStyles(t));let e=t.keyboardHeight,n=this.rows.length,i=this._rowHeight=Math.floor(e/(n==0?1:n)),s=t.widthStyle.absolute;s&&(this.element.style.height=e+"px"),this.showLanguage(t.spacebarText);for(let l=0;ln.id);for(let n of e)this.buildLayer(n);return this._layers}get activeLayerId(){return this._activeLayerId}set activeLayerId(t){this._activeLayerId=t,this.getLayer(t);for(let e of Object.keys(this._layers)){let n=this._layers[e],i=n.element;n.id==t?i.style.display="block":i.style.display="none"}}findNearestKey(t){if(!t)return null;let e=t.stateToken;if(!e)throw new Error("Layer id not set for input coordinate");let n=this._layers[e];if(!n)throw new Error(`Layer id ${e} could not be found`);return this.nearestKey(t,n)}blinkLayer(t){if(typeof t=="string"){let n=t;if(t=this.getLayer(n),!t)throw new Error(`Layer id ${n} could not be found`)}let e=t;if(e.element.style.display!="block")for(let n in this._layers){if(this._layers[n].element.style.display=="block"){let i=this._layers[n];i.element.style.display="none"}this._layers[n].element.style.display="none"}e.element.style.display="block",Promise.resolve().then(()=>{let n=this._layers[this._activeLayerId];(e.element.style.display=="block"||n.element.style.display!="block")&&(e.element.style.display="none",n.element.style.display="block")})}nearestKey(t,e){if(e.rows.length==0)return null;let n={x:t.targetX/this.computedWidth,y:t.targetY/this.computedHeight};if(!isFinite(n.x)||!isFinite(n.y))return null;let i=Math.max(0,Math.min(e.rows.length-1,Math.floor(n.y*e.rows.length))),s=e.rows[i],l=null,r=Number.MAX_VALUE;for(let c of s.keys){let g=c.spec;if(g.sp==J.blank||g.sp==J.spacer)continue;let d=g.proportionalWidth/2,u=Math.abs(n.x-g.proportionalX);if(u-d<=0)return c.btn;{let I=u-d;I{this.orientation=i,this.show(this.key,this.state,this.previewHost)}}show(t,e,n){var r;let i=this.vkbd;if(e&&i.layerGroup.blinkLayer(t.key.spec.displayLayer),e&&t.offsetParent){let c=t.key.row.element,g=t.getClientRects()[0],d=c.getClientRects()[0],u=g.left-d.left,I=g.width,B=g.height,h=1.8,b=this.element.style,G=i.topContainer.getBoundingClientRect(),U=t.getBoundingClientRect(),Q,p=this.orientation,X=U.bottom-G.top;Q=X+(p=="top"?1:-1);let R=Q-Math.floor(Q),L=I+Math.ceil(I*.3)*2,T=Math.ceil(2.3*B)+R;p=="bottom"&&(Q+=T-B),b.top="auto";let _e=p=="top"?"bottom":"top";this.tip.classList.remove(`${Do}${_e}`),this.tip.classList.add(`${Do}${p}`),b.bottom=Math.floor(G.height-Q)+"px",b.textAlign="center",b.overflow="visible",b.width=L+"px",b.height=T+"px";let Hi=this.vkbd.currentLayer.element.style.fontFamily,Ce=getComputedStyle(i.element);b.fontFamily=t.key.spec.font||Hi||Ce.fontFamily;var s=parseInt(Ce.fontSize,10);if(s==Number.NaN&&(s=0),s!=0){let $e={keyWidth:1.6*I,keyHeight:1.6*B,baseEmFontSize:i.getKeyEmFontSize(),layoutFontSize:new x(i.kbdDiv.style.fontSize)};b.fontSize=t.key.getIdealFontSize(t.key.keyText,$e,h).styleString}var l=(L-I)/2;uwindow.innerWidth-I-l?(this.cap.style.left=L-I-1+"px",u-=l-1):this.cap.style.left=l+"px",b.left=u-l+"px";let Je=getComputedStyle(this.element),Un=G.height,ht=parseFloat(Je.bottom),xn=parseFloat(Je.height),qe=Math.ceil(T/2);this.cap.style.width=I+"px",this.tip.style.height=qe+"px";let kt=3,Zn=qe-kt+"px";p=="top"?(this.cap.style.top=Zn,this.cap.style.bottom=""):(this.cap.style.top="",this.cap.style.bottom=Zn);let Xn=X-Math.floor(Q)+T-(p=="top"?qe:-kt*2);if(this.cap.style.height=Xn+"px",this.constrain&&xn+ht>Un){let $e=xn+ht-Un;b.height=T-$e+"px";let la=Math.max(0,T-$e-T/2+2);this.cap.style.height=la+"px"}else ht<0&&(b.bottom="0px",this.cap.style.height=Math.max(0,Xn+ht)+"px");if(b.display="block",this.previewHost==n)return;let ke=this.preview;this.previewHost&&this.previewHost.off("preferredOrientation",this.reorient),this.previewHost=n,n&&(this.previewHost.on("preferredOrientation",this.reorient),this.preview=this.previewHost.element,this.tip.replaceChild(this.preview,ke),n.setCancellationHandler(()=>this.show(null,!1,null)),n.on("startFade",()=>{this.element.classList.remove("kmw-preview-fade"),this.element.offsetWidth,this.element.classList.add("kmw-preview-fade")}))}else{this.element.style.display="none",(r=this.previewHost)==null||r.off("preferredOrientation",this.reorient),this.previewHost=null;let c=this.preview;this.preview=document.createElement("div"),this.tip.replaceChild(this.preview,c),this.element.classList.remove("kmw-preview-fade"),this.orientation=Oo}this.key=t,this.state=e}};o(nc,"KeyTip");var Ii=nc;var ic="kmw-keypreview",dg="kmw-preview-overlay",ug="kmw-keytip",sc=class sc{constructor(t){this.state=!1;this.vkbd=t;let e=this.element=document.createElement("div");e.className=ic,e.id="kmw-keytip",e.style.pointerEvents="none",e.style.display="none",this.preview=document.createElement("div"),e.appendChild(this.preview)}show(t,e,n){let i=this.vkbd,s=t==null?void 0:t.key.spec.displayLayer;if(e&&i.layerGroup.blinkLayer(s),e&&(t!=null&&t.offsetParent)){let l=this.vkbd.topContainer.getBoundingClientRect(),r=t.getBoundingClientRect(),c=s!=i.layerId?dg:"";this.element.className=`${ic} ${t.className} ${c}`,this.element.id=`${ug}-${t.id}`;let g=this.element.style,d=this.vkbd.currentLayer.element.style.fontFamily;if(g.fontFamily=t.key.spec.font||d,g.left=r.left-l.left+"px",g.top=r.top-l.top+"px",g.width=r.width+"px",g.height=r.height+"px",this.element.style.display="block",this.previewHost==n)return;let u=this.preview;this.previewHost=n,n&&(this.preview=this.previewHost.element,this.element.replaceChild(this.preview,u),n.setCancellationHandler(()=>this.show(null,!1,null)),n.on("startFade",()=>{this.element.classList.remove("kmw-preview-fade"),this.element.offsetWidth,this.element.classList.add("kmw-preview-fade")}))}else{this.element.style.display="none",this.element.className=ic,this.previewHost=null;let l=this.preview;this.preview=document.createElement("div"),this.element.replaceChild(this.preview,l),this.element.classList.remove("kmw-preview-fade")}this.key=t,this.state=e}};o(sc,"TabletKeyTip");var Ls=sc;function Se(a){try{if(a=="desktop")return 1;var t=document.documentElement.clientWidth;if(screen.width>t)return 1;var e=screen.width;return be()?screen.widthscreen.height&&(e=screen.height),Math.round(100*e/t)/100}catch(n){return 1}}o(Se,"getViewportScale");var Rt=class Rt{constructor(t,e){this.directlyEmitsKeys=!0;this.hasModalVisualization=!1;this.deleteRepeater=o(()=>{this.repeatClosure(),this.timerHandle=window.setTimeout(this.deleteRepeater,Rt.REPEAT_DELAY)},"deleteRepeater");this.source=t;let n=t.stageReports[0].item;n.key.highlight(!0),this.repeatClosure=()=>{e(),n.key.highlight(!0)},this.timerHandle=window.setTimeout(this.deleteRepeater,Rt.INITIAL_DELAY),this.source.on("complete",()=>{window.clearTimeout(this.timerHandle),this.timerHandle=void 0,n.key.highlight(!1)})}cancel(){this.deleteRepeater(),this.source.cancel()}currentStageKeyDistribution(){return null}};o(Rt,"HeldRepeater"),Rt.INITIAL_DELAY=500,Rt.REPEAT_DELAY=100;var Rs=Rt;var lc=class lc extends Xe{constructor(t,e){if(typeof e!="string"||e=="")throw"The 'layer' parameter for subkey construction must be properly defined.";super(t,e)}getId(){return"popup-"+this.spec.elementID}construct(t,e,n,i){let s=this.spec,l=document.createElement("div"),r=l.style;l.className="kmw-key-square-ex",i&&(r.marginTop="5px"),r.width=n+"px",r.height=e.offsetHeight+"px";let c=document.createElement("div"),g=this.btn=As(c,new un(this,s.id));this.setButtonClass(),g.id=this.getId();let d=g.style;return d.height=r.height,d.lineHeight=e.style.lineHeight,d.width=r.width,d.position="absolute",g.appendChild(this.label=this.generateKeyText(t)),l.appendChild(g),this.square=l}allowsKeyTip(){return!1}};o(lc,"OSKSubKey");var bi=lc;var Po=.2,Bg=1.2,_o=3,Bn=5,jo=6+_o,rc=class rc{constructor(t,e,n,i,s){this.directlyEmitsKeys=!0;this.shouldLockLayer=!1;var U;this.baseKey=i,this.source=t,this.gestureParams=s,n.layerLocked&&(this.shouldLockLayer=!0),t.on("complete",()=>{var Q;(Q=this.currentSelection)==null||Q.key.highlight(!1),this.clear()}),t.on("stage",()=>{let Q=this.currentSelection;if(Q){let p=n.keyEventFromSpec(Q.key.spec);p.keyDistribution=this.currentStageKeyDistribution(),n.raiseKeyEvent(p,Q)}});let l=t.stageReports[0].sources[0].constructSubview(!0,!1);l.path.on("step",Q=>{var p,X;l.path.stats.netDistance>=4&&((p=this.currentSelection)==null||p.key.highlight(!1),(X=Q.item)==null||X.key.highlight(!0),this.currentSelection=Q.item)}),this.currentSelection=i,i.key.highlight(!0);let r=i.subKeys,c=this.element=document.createElement("div");c.id="kmw-popup-keys";var g=c.style;g.fontFamily=n.fontFamily;let d=getComputedStyle(i);g.fontSize=d.fontSize,g.visibility="hidden";let u=i.key.layer;(typeof u!="string"||u=="")&&(u=n.layerId);let I=r.length,B=Math.ceil(I/9),h=Math.ceil(I/B);this.subkeys=[];let b=Bn,F=0;for(let Q=0,p=0;Qn.width-2*Bn&&(X=n.width-2*Bn),(b+X+Bn>n.width||p>=h)&&(F++,p=0,b=Bn);let L=new bi(r[Q],u).construct(n,i,X,F>0);b+=X+Bn,this.menuWidth=Math.max((U=this.menuWidth)!=null?U:0,b),this.subkeys.push(L.firstChild),c.appendChild(L)}g.width=this.menuWidth+"px",this.shim=document.createElement("div"),this.shim.id="kmw-popup-shim",n.device.formFactor==V.FormFactor.Phone&&this.selectDefaultSubkey(i,c),n.element.appendChild(this.element),n.topContainer.appendChild(this.shim),this.reposition(n);let G=this.buildPopupRecognitionConfig(n);e({type:"push",config:G})}buildPopupRecognitionConfig(t){let e=this.element.getBoundingClientRect(),n=this.baseKey.getBoundingClientRect(),i=this.subkeys[0].style,l=-.666*Number.parseInt(i.height,10),r=3,c=n.bottom-e.bottom,g=new ue(this.element,[l*r,l,-c{let B=g.getBoundingClientRect(),h=null,b=Number.MAX_VALUE,F=Number.MAX_VALUE;if(u.clientXB.right||u.clientYB.bottom)return null;for(let G of this.subkeys){let U=G.getBoundingClientRect(),Q=Number.MAX_VALUE,p=Number.MAX_VALUE;if(U.left<=u.clientX&&u.clientX=u.clientX?U.left-u.clientX:u.clientX-U.right,U.top<=u.clientY&&u.clientY=u.clientY?U.top-u.clientY:u.clientY-U.bottom,Q==0&&p==0)return G;(Qg&&(c=g),c<0&&(c=0),l.left=c+"px";let d=i.getBoundingClientRect(),u=s.getBoundingClientRect();l.top=u.top-d.top-e.offsetHeight-_o+"px",l.visibility="visible";let I=t.isEmbedded,B=getComputedStyle(e),h=parseFloat(B.top),b=0,F=0;h0){let G=document.createElement("div"),U=G.style;G.id="kmw-popup-callout",n.appendChild(G),U.top=d+"px",U.borderTopWidth=F+"px";let Q=Bg*h,p=c.width*Q,X=this.menuWidth-2*r,R=X{let s=i.getBoundingClientRect();return{keySpec:i.key.spec,centerX:(s.right-s.width/2-t.left)/t.width,centerY:(s.bottom-s.height/2-t.top)/t.height,width:s.width/t.width,height:s.height/t.height}}),kbdScaleRatio:e}}currentStageKeyDistribution(){let t=this.source.stageReports[this.source.stageReports.length-1],e=this.source.stageReports[0],n=t.sources[0],i=n.currentSample,s=this.element.getBoundingClientRect(),l={x:i.targetX/s.width,y:i.targetY/s.height};l.x=l.x<0?0:l.x>1?1:l.x,l.y=l.y<0?0:l.y>1?1:l.y;let r=ps(l,this.buildCorrectiveLayout()),c=r.get(i.item.key.spec),g=Math.min(n.path.stats.duration-e.sources[0].path.stats.duration,this.gestureParams.longpress.waitLength)/(2*this.gestureParams.longpress.waitLength),d=Math.min(n.path.stats.rawDistance,this.gestureParams.longpress.noiseTolerance*4)/(this.gestureParams.longpress.noiseTolerance*8),u=Math.min(g*g,d*d),I=c+u,B=new Map,h=this.subkeys.find(b=>b.keyId==this.baseKey.keyId);return h?B.set(h.key.spec,I):B.set(this.baseKey.key.spec,I),dt([r,B])}cancel(){this.clear(),this.source.cancel()}clear(){this.element.parentNode&&this.element.parentNode.removeChild(this.element),this.shim.parentNode&&this.shim.parentNode.removeChild(this.shim),this.callout&&this.callout.parentNode&&this.callout.parentNode.removeChild(this.callout)}};o(rc,"SubkeyPopup");var In=rc;var cc=class cc{constructor(t,e,n){this.directlyEmitsKeys=!0;this.shouldRestore=!1;this.hasModalVisualization=!1;let i=t.stageReports[0];this.originalLayer=i.sources[0].stateToken,this.source=t,this.completionCallback=()=>{e.lockLayer(!1),this.shouldRestore&&(e.layerId=this.originalLayer,e.updateState()),n==null||n()},e.lockLayer(!0),t.on("stage",s=>{let l=s.matchedId;l.includes("modipress")&&l.includes("-end")?this.clear():l.includes("modipress")&&l.includes("-hold")&&(this.shouldRestore=!0)}),t.on("complete",()=>this.cancel())}get isLocked(){return this.shouldRestore}setLocked(){this.shouldRestore=!0}get completed(){return this.completionCallback===null}clear(){let t=this.completionCallback;this.completionCallback=null,t==null||t()}cancel(){this.clear(),this.source.cancel()}currentStageKeyDistribution(t){return null}};o(cc,"Modipress");var ut=cc;var oc=class oc{constructor(t,e,n,i,s){this.directlyEmitsKeys=!0;this.hasModalVisualization=!1;this.tapIndex=0;this.baseKey=n,this.baseContextToken=i,this.multitaps=[n.key.spec].concat(n.key.spec.multitap),this.sequence=t;let l=o(u=>{var B;(B=this.modipress)==null||B.clear();let I=new ut(t,e,()=>{this.modipress=e.activeModipress=null});this.modipress=e.activeModipress=I},"startModipress");this.originalLayer=e.layerId;let r=o(u=>(this.tapIndex+u)%this.multitaps.length,"tapLookahead"),c=o(()=>{s==null||s.setMultitapHint(this.multitaps[r(0)],this.multitaps[r(1)],e)},"updatePreview");t.on("complete",()=>{var u;(u=this.modipress)==null||u.cancel(),this.clear()});let g=o(u=>{var F;switch(u.matchedId){case"modipress-hold":this.clear(),t.off("stage",g);return;case"modipress-end-multitap-transition":case"modipress-multitap-end":case"modipress-end":case"multitap-end":case"simple-tap":return;case"modipress-multitap-lock-transition":(F=this.modipress)==null||F.setLocked();return;case"modipress-multitap-start":case"multitap-start":break;default:throw new Error(`Unsupported gesture state encountered during multitap: ${u.matchedId}`)}this.tapIndex=r(1);let I=this.multitaps[this.tapIndex];c();let B=e.keyEventFromSpec(I);B.baseTranscriptionToken=this.baseContextToken;let h=u.sources[0].currentSample,b=e.getSimpleTapCorrectionDistances(h,this.baseKey.key.spec);if(h.stateToken!=e.layerId&&!u.matchedId.includes("modipress")){let G=e.layerGroup.findNearestKey(A(y({},h),{stateToken:e.layerId})),U=b.get(G.key.spec);U==null&&console.warn("Could not find current layer's key"),b.delete(G.key.spec),b.set(h.item.key.spec,U)}B.keyDistribution=this.currentStageKeyDistribution(b),B.kNextLayer||(B.kNextLayer=this.originalLayer),e.raiseKeyEvent(B,null),u.matchedId=="modipress-multitap-start"&&l(u)},"stageHandler");t.on("stage",g),t.stageReports[0].matchedId=="modipress-start"&&l(t.stageReports[0]),c()}currentStageKeyDistribution(t){let e=dt(t),n=e.findIndex(c=>c.keySpec==this.baseKey.key.spec);if(n==-1)return li(this.baseKey)||console.warn("Could not find base key's probability for multitap correction"),e;let i=e.splice(n,1)[0].p,s=0,l=[];for(let c=0;c{c.p=r*c.p}),e.concat(l).sort((c,g)=>g.p-c.p)}cancel(){this.clear(),this.sequence.cancel()}clear(){}};o(oc,"Multitap");var hi=oc;var Fi=1.4142,ac=o(a=>Math.abs(a)<1e-10?0:a,"coerceZeroes"),gc=class gc extends S.default{constructor(e,n,i,s){var I;super();this.flickPreviews=new Map;this.orientation="top";let l=e.key.spec,r=this.flickEdgeLength=Math.max(i,s),c=this.div=document.createElement("div");c.className=c.id="kmw-gesture-preview",c.style.pointerEvents="none";let g=this.previewImgContainer=document.createElement("div");this.previewImgContainer.id="kmw-preview-img-container";let d=this.label=document.createElement("span");if(d.className="kmw-gesture-base-label kmw-key-text",d.id="kmw-gesture-base-label",g.appendChild(d),d.textContent=e.key.label.textContent,this.div.appendChild(this.previewImgContainer),l.flick){let B=l.flick||{};Object.keys(B).forEach(h=>{let b=document.createElement("div");b.className="kmw-flick-preview kmw-key-text",b.textContent=B[h].text;let F=b.style,G=kr.get(h),U=ac(-Math.sin(G[0])),Q=ac(Math.cos(G[0]));F.width="100%",F.textAlign="center",U<0?F.right=-U*Fi*r+"px":U>0?F.left=U*Fi*r+"px":F.left="0px",F.height="100%",F.lineHeight="100%",Q<0?F.bottom=-Q*Fi*r+"px":Q>0?F.top=Q*Fi*r+"px":F.top="0px",this.flickPreviews.set(h,b),g.appendChild(b)})}let u=this.hintLabel=document.createElement("div");u.className="kmw-key-popup-icon",n||(u.textContent=l==l.hintSrc?l.hint:(I=l.hintSrc)==null?void 0:I.text,u.style.fontWeight=u.textContent=="•"?"bold":""),c.appendChild(u)}get element(){return this.div}refreshLayout(){let e=getComputedStyle(this.div),n=Number.parseInt(e.height,10);this.flickPreviews.forEach(i=>{i.style.lineHeight=i.style.height=`${n}px`})}cancel(){var e;(e=this.onCancel)==null||e.call(this),this.onCancel=null}setCancellationHandler(e){this.onCancel=e}setMultitapHint(e,n,i){var r,c;let s=Lt(e.text,i),l=Lt(n.text,i);this.label.textContent=s,this.hintLabel.textContent=l,this.label.style.fontFamily=s!=e.text?"SpecialOSK":(r=e.font)!=null?r:this.label.style.fontFamily,this.hintLabel.style.fontFamily=l!=n.text?"SpecialOSK":(c=n.font)!=null?c:this.hintLabel.style.fontFamily,this.emit("startFade"),this.clearFlick()}scrollFlickPreview(e,n){this.clearHint();let i=this.previewImgContainer.style,s=this.flickEdgeLength*Fi;i.marginLeft=`${s*e}px`,i.marginTop=`${s*n}px`;let l=ac(n)<0?"bottom":"top";this.orientation!=l&&(this.orientation=l,this.emit("preferredOrientation",l))}clearFlick(){this.previewImgContainer.style.marginTop="0px",this.previewImgContainer.style.marginLeft="0px",this.previewImgContainer.classList.add("kmw-flick-clear")}clearHint(){this.hintLabel.classList.add("kmw-hint-clear")}clearAll(){this.clearFlick()}};o(gc,"GesturePreviewHost");var vs=gc;var qo=et.TIER!="stable"||et.VERSION_ENVIRONMENT!="",Ig=qo?10:0,mi=class mi extends S.default{constructor(e){var g,d,u;super();this.layerLocked=!1;this.layerIndex=0;this.isStatic=!1;this._fixedWidthScaling=!1;this._fixedHeightScaling=!0;this._borderWidth=0;this.stateKeys={K_CAPS:!1,K_NUMLOCK:!1,K_SCROLL:!1};this.activeGestures=[];this.activeModipress=null;this.repeatDelete=function(){this.deleting&&(this.modelKeyClick(this.deleteKey),this.deleting=window.setTimeout(this.repeatDelete,100))}.bind(this);this.config=e,this.config.device=e.device||e.hostDevice,this.config.isEmbedded=e.isEmbedded||!1,e.isStatic&&(this.isStatic=e.isStatic),(g=this.config).gestureParams||(g.gestureParams=y({},si)),this._fixedWidthScaling=this.device.touchable&&!this.isStatic,this._fixedHeightScaling=this.device.touchable&&!this.isStatic;var n=document.createElement("div");this.config.styleSheetManager=e.styleSheetManager||new ge(n);let i;if(e.keyboard)i=this.kbdLayout=e.keyboard.layout(e.device.formFactor),this.layoutKeyboardProperties=e.keyboardMetadata,this.isRTL=e.keyboard.isRTL;else{let I=E.buildDefaultLayout(null,null,e.device.formFactor);i=this.kbdLayout=wt.polyfill(I,null,e.device.formFactor),this.layoutKeyboardProperties=null,this.isRTL=!1}"font"in i?this.fontFamily=i.font:this.fontFamily="";let s=e.device.formFactor;this.layoutKeyboard=e.keyboard,this.layoutKeyboard||(this.layoutKeyboard=new Y(null)),this.layerGroup=new Bi(this,this.layoutKeyboard,s),this.layoutKeyboard.markLayoutCalibrated(s),n.appendChild(this.layerGroup.element),this.kbdDiv=n,this.isStatic||(this.gestureEngine=this.constructGestureEngine()),n.classList.add(e.device.formFactor,"kmw-osk-inner-frame");let l=(u=(d=this.layoutKeyboard)==null?void 0:d.id.replace("Keyboard_",""))!=null?u:"",r=l.indexOf("::");r!=-1&&(l=l.substring(r+2));let c="kmw-keyboard-"+l;this.element.classList.add(c)}get gestureParams(){return this.config.gestureParams}get layerId(){var e,n;return(n=(e=this.layerGroup)==null?void 0:e.activeLayerId)!=null?n:"default"}set layerId(e){let n=e!=this.layerId;if(this.layerGroup.getLayer(e))this.layerGroup.activeLayerId=e,this.gestureEngine&&(this.gestureEngine.stateToken=e);else throw new Error(`Keyboard ${this.layoutKeyboard.id} does not have a layer with id ${e}`);n&&!this.deferLayout&&(this.updateState(),this.layerGroup.refreshLayout(this.constructLayoutParams()))}get currentLayer(){var e;return(e=this.layerGroup)==null?void 0:e.activeLayer}get lgKey(){var e,n;return(n=(e=this.currentLayer)==null?void 0:e.globeKey)==null?void 0:n.btn}get hkKey(){var e,n;return(n=(e=this.currentLayer)==null?void 0:e.hideKey)==null?void 0:n.btn}get spaceBar(){var e,n;return(n=(e=this.currentLayer)==null?void 0:e.spaceBarKey)==null?void 0:n.btn}constructGestureEngine(){let e={targetRoot:this.element,mouseEventRoot:document.body,maxRoamingBounds:new ue(this.topContainer,[NaN]),itemIdentifier:(r,c)=>this.layerGroup.findNearestKey(r),recordingMode:qo,historyLength:Ig},n=new At(Er(this.kbdLayout,this.gestureParams),e);n.stateToken=this.layerId;let i={},s=o(r=>{for(let c of Object.keys(i)){if(c==r)continue;i[c].source.terminate(!0)}},"clearActiveGestures"),l=new Map;return n.on("inputstart",r=>{var u;let c=this.highlightKey(r.currentSample.item,!0);c&&((u=this.gesturePreviewHost)==null||u.cancel(),this.gesturePreviewHost=c),i[r.identifier]={source:r,roamingHighlightHandler:null,key:r.currentSample.item,previewHost:c};let g=i[r.identifier],d=o(()=>{ae(0).then(()=>{let I=g.previewHost;I&&(I.cancel(),this.gesturePreviewHost=null,g.previewHost=null),g.key&&(this.highlightKey(g.key,!1),g.key=null)})},"endHighlighting");g.roamingHighlightHandler=I=>{var b;let B=I.item,h=i[r.identifier].key;if(!this.kbdLayout.hasFlicks&&B!=h){this.highlightKey(h,!1),(b=this.gesturePreviewHost)==null||b.cancel(),this.gesturePreviewHost=null,g.previewHost=null;let F=this.highlightKey(B,!0);F&&(this.gesturePreviewHost=F,g.previewHost=F),i[r.identifier].key=B}},r.path.on("invalidated",d),r.path.on("complete",d),r.path.on("step",g.roamingHighlightHandler)}),n.on("recognizedgesture",r=>{var c;(c=this.activeModipress)==null||c.setLocked(),r.on("complete",()=>{var g;for(let d of r.allSourceIds)(g=i[d])!=null&&g.previewHost&&(this.gesturePreviewHost=null,i[d].previewHost.cancel()),delete i[d]}),r.on("stage",(g,d)=>{let u=r.allSourceIds.map(p=>{var X;return(X=i[p])==null?void 0:X.previewHost}).find(p=>!!p),I=o(()=>{u&&(u.cancel(),this.gesturePreviewHost=null)},"clearPreviewHost"),B=l.get(r);!B&&u&&!g.matchedId.includes("flick")&&u.clearFlick();let h;for(let p of g.allSourceIds){let X=o(R=>{R.key&&(this.highlightKey(R.key,!1),R.key=null),R.source.path.off("step",R.roamingHighlightHandler)},"clearRoaming");if(h=i[p],h)X(h);else{let R=p;ae(0).then(()=>{let L=i[R];L&&X(L)})}}let b=g.item,F=g.sources[0],G=F?F.currentSample:null,U=null;if(b&&!(B&&B[0].directlyEmitsKeys)){let p,X=this.getSimpleTapCorrectionDistances(F.currentSample,b.key.spec);B&&(p=B[0].currentStageKeyDistribution(X)),p||(p=dt(X));let R=!this.layerLocked&&B&&B[0]instanceof In&&B[0].shouldLockLayer;try{R&&this.lockLayer(!0),U=this.modelKeyClick(g.item,G,p)}finally{R&&this.lockLayer(!1)}}if(r.stageReports.length>1&&g.matchedId!="modipress-end")return;let Q=r.stageReports[0].item;if(g.matchedId=="special-key-start")b.key.spec.baseKeyID=="K_BKSP"?(I(),B=[new Rs(r,()=>this.modelKeyClick(b,G))]):b.key.spec.baseKeyID=="K_LOPT"&&(r.on("complete",()=>{b.key.highlight(!1),this.emit("globekey",b,!1)}),s(F.identifier),b.key.highlight(!0));else if(g.matchedId.indexOf("longpress")>-1)I(),B=[new In(r,d,this,r.stageReports[0].sources[0].baseItem,this.gestureParams)];else if(Q!=null&&Q.key.spec.multitap&&(g.matchedId=="initial-tap"||g.matchedId=="multitap"||g.matchedId=="modipress-start"))h.previewHost=null,r.on("complete",()=>{I()}),B=[new hi(r,this,Q,U.contextToken,u)];else if(g.matchedId.indexOf("flick")>-1)B=[new ni(r,d,this,r.stageReports[0].sources[0].baseItem,this.gestureParams,u)];else if(g.matchedId.includes("modipress")&&g.matchedId.includes("-start"))if(I(),this.layerLocked)console.warn("Unexpected state: modipress start attempt during an active modipress");else{B||(B=[]);let p=new ut(r,this,()=>{let X=B.indexOf(p);X>-1&&B.splice(X,1),this.activeModipress=null});B.push(p),this.activeModipress=p}else I();B&&(this.activeGestures=this.activeGestures.concat(B),l.set(r,B),r.on("complete",()=>{let p=this.activeGestures.filter(X=>B.includes(X));this.activeGestures=this.activeGestures.filter(X=>!B.includes(X)),p.forEach(X=>{X instanceof ut&&X.cancel()})}))})}),n}get element(){return this.kbdDiv}get device(){return this.config.device}get hostDevice(){return this.config.hostDevice}get fontRootPath(){return this.config.pathConfig.fonts}get styleSheetManager(){return this.config.styleSheetManager}get topContainer(){return this.config.topContainer}get isEmbedded(){return this.config.isEmbedded}postInsert(){}get width(){return this._width}get height(){return this._height}get layoutWidth(){if(this.usesFixedWidthScaling){let e=this.width;return e-=this._borderWidth*2,x.inPixels(e)}else return x.forScalar(1)}get layoutHeight(){if(this.usesFixedHeightScaling){let e=this.height;return e-=this._borderWidth*2,x.inPixels(e)}else return x.forScalar(1)}get internalHeight(){return this.usesFixedHeightScaling?x.inPixels(this.layoutHeight.val-this._borderWidth*2-this.layerGroup.verticalPadding):x.forScalar(1)}get fontSize(){return this._fontSize||(this._fontSize=new x("1em")),this._fontSize}set fontSize(e){this._fontSize=e,this.kbdDiv.style.fontSize=e.styleString}get usesFixedWidthScaling(){return this._fixedWidthScaling}set usesFixedWidthScaling(e){this._fixedWidthScaling=e}get usesFixedHeightScaling(){return this._fixedHeightScaling}set usesFixedHeightScaling(e){this._fixedHeightScaling=e}get usesFixedPositioning(){let e=this.element;for(;e;){if(getComputedStyle(e).position=="fixed")return!0;e=e.offsetParent}return!1}setSize(e,n,i){this._width=e,this._height=n,this.kbdDiv&&(this.kbdDiv.style.width=e?this._width+"px":"",this.kbdDiv.style.height=n?this._height+"px":"",!this.device.touchable&&n&&(this.fontSize=new x(this._height/8+"px")),i||this.refreshLayout())}getTouchCoordinatesOnKeyboard(e){let n={x:e.targetX,y:e.targetY};return n.x/=this.layerGroup.element.offsetWidth,n.y/=this.kbdDiv.offsetHeight,n}getSimpleTapCorrectionDistances(e,n){let i=this.getTouchCoordinatesOnKeyboard(e),l=this.layerGroup.element.offsetWidth,r=this.kbdDiv.offsetHeight;if(!l||!r)return new Map;let c=l/r,g=zo(this.kbdLayout.getLayer(this.layerId),c);return ps(i,g)}keyTarget(e){let n=e;try{if(n){if(n.classList.contains("kmw-key"))return Ws(n);if(n.parentNode&&n.parentNode.classList.contains("kmw-key"))return Ws(n.parentNode);if(n.firstChild&&n.firstChild.classList.contains("kmw-key"))return Ws(n.firstChild)}}catch(i){}return null}cancelDelete(){this.deleting&&window.clearTimeout(this.deleting),this.deleting=0}modelKeyClick(e,n,i){let s=this.initKeyEvent(e);return n&&(s.source=n),i&&(s.keyDistribution=i),this.raiseKeyEvent(s,e)}initKeyEvent(e){this.highlightKey(e,!1);let n=e.key?e.key.spec:null;return n?this.keyEventFromSpec(n):null}keyEventFromSpec(e){let n=this.layoutKeyboard.constructKeyEvent(e,this.device,this.stateKeys);return n.srcKeyboard=this.layoutKeyboard,n}_UpdateVKShiftStyle(e){var r;var n;e||(e=this.layerId);let i=this.layerGroup.getLayer(e);if(!i||(this.gestureEngine&&(this.gestureEngine.stateToken=e),!((r=this.layoutKeyboard)!=null&&r.usesDesktopLayoutOnDevice(this.device))))return;let s=["K_CAPS","K_NUMLOCK","K_SCROLL"],l=[i.capsKey,i.numKey,i.scrollKey];for(n=0;n=0)return null;let i=e.key.allowsKeyTip();return n=this.activeGestures.find(l=>l.hasModalVisualization)?!1:n,e.key.highlight(n),n&&i?this.gesturePreviewHost?null:this.showGesturePreview(e):null}getKeyEmFontSize(){if(!this.fontSize)return new x("0px");if(this.device.formFactor=="desktop"){let e=.8;return this.fontSize.scaledBy(e)}else{let e=getComputedStyle(document.body).fontSize,n=new x(e),i=1;if(!this.isStatic){if(this.fontSize.absolute)return this.fontSize;i=this.fontSize.val}return n.scaledBy(i)}}updateState(){this.currentLayer&&(this.nextLayer=this.layerId,this.currentLayer.nextlayer&&(this.nextLayer=this.currentLayer.nextlayer),this.layerGroup.activeLayerId=this.layerId,this._UpdateVKShiftStyle())}refreshLayout(){if(this.deferLayout)return;let e=this.device;var n=1;e.OS==V.OperatingSystem.iOS&&!this.isEmbedded&&(n=n/Se(this.device.formFactor));let i=this.kbdDiv.style;this.usesFixedHeightScaling&&this.height&&(i.height=i.maxHeight=this.height+"px"),i.fontSize=this.fontSize.scaledBy(n).styleString;let s=this.width&&this.height,l=getComputedStyle(this.kbdDiv),r=getComputedStyle(this.layerGroup.element),c=l.height!=""&&l.height!="auto",g=r.height!=""&&r.height!="auto";if(l.border&&(this._borderWidth=new x(l.borderWidth).val),s)this._computedWidth=this.width,this._computedHeight=this.height;else if(c)this._computedWidth=parseInt(l.width,10),this._computedHeight=parseInt(l.height,10);else if(g)this._computedWidth=parseInt(r.width,10),this._computedHeight=parseInt(r.height,10);else return;this.layerGroup.refreshLayout(this.constructLayoutParams()),this.isStatic||(this.gestureEngine.config.maxRoamingBounds.updatePadding([-.333*this.currentLayer.rowHeight]),this.gestureParams.longpress.flickDistStart=.24*this.currentLayer.rowHeight,this.gestureParams.flick.startDist=.3*this.currentLayer.rowHeight,this.gestureParams.flick.dirLockDist=.35*this.currentLayer.rowHeight,this.gestureParams.flick.triggerDist=.75*this.currentLayer.rowHeight,this.gestureParams.longpress.flickDistFinal=.75*this.currentLayer.rowHeight)}constructLayoutParams(){var e,n;return{keyboardWidth:this._computedWidth-2*this._borderWidth,keyboardHeight:this._computedHeight-2*this._borderWidth-this.layerGroup.verticalPadding,widthStyle:this.layoutWidth,heightStyle:this.internalHeight,baseEmFontSize:this.getKeyEmFontSize(),layoutFontSize:new x(this.layerGroup.element.style.fontSize),spacebarText:(n=(e=this.layoutKeyboardProperties)==null?void 0:e.displayName)!=null?n:"(System keyboard)"}}computedAdjustedOskHeight(e){if(!this.layerGroup)return e;let n=this.layerGroup.spec.layer,i=0;for(let r in n){let g=n[r].row.length,d=Math.floor(e/(g==0?1:g)),u=g*d;u>i&&(i=u)}return i+0}appendStyleSheet(){var e=this.layoutKeyboard,n=this.layoutKeyboardProperties;this.styleSheet&&this.styleSheet.parentNode&&this.styleSheet.parentNode.removeChild(this.styleSheet);var i=n==null?void 0:n.textFont,s=n==null?void 0:n.oskFont;this.styleSheetManager.addStyleSheetForFont(i,this.fontRootPath,this.device.OS),this.styleSheetManager.addStyleSheetForFont(s,this.fontRootPath,this.device.OS),this.config.specialFont&&this.styleSheetManager.addStyleSheetForFont(this.config.specialFont,"",this.device.OS);var l=this.addFontStyle(i,s);e!=null&&typeof e.oskStyling=="string"&&(l=l+e.oskStyling),l&&(this.styleSheet=st(l),this.styleSheetManager.linkStylesheet(this.styleSheet)),this.styleSheetManager.allLoadedPromise().then(()=>{this.layerGroup.resetPrecalcFontSizes(),this.refreshLayout()})}addFontStyle(e,n){let i="",s=o(l=>l.family.replace(/\u0022/g,"").replace(/,/g,'","'),"family");return(e||n)&&(i=` +.kmw-key-text { + font-family: "${s(n||e)}"; +} + +.kmw-suggestion-text { + font-family: "${s(e||n)}"; +} +`),i}static buildDocumentationKeyboard(e,n,i,s,l,r){if(!e)return null;var c=typeof s=="undefined"?"desktop":s,g=typeof l=="undefined"?"default":l,d={};d.formFactor=c,c!="desktop"?(d.OS=V.OperatingSystem.iOS,d.touchable=!0):(d.OS=V.OperatingSystem.Windows,d.touchable=!1);let u=e.layout(c),I=new V("other",d.formFactor,d.OS,d.touchable),B=new mi({keyboard:e,keyboardMetadata:n,hostDevice:I,isStatic:!0,topContainer:null,pathConfig:i,styleSheetManager:null,specialFont:{family:"SpecialOSK",files:[`${i.resources}/osk/keymanweb-osk.ttf`],path:""}});B.layerGroup.element.className=B.kbdDiv.className,B.layerGroup.element.classList.add(d.formFactor+"-static");let h=B.kbdDiv.childNodes[0],b=document.createElement("div");b.classList.add(d.OS.toLowerCase(),d.formFactor),u!=null?(B.layerId=g,B.layerGroup.activeLayerId=g,B.setSize(800,r),B.fontSize=To(I,r,!1),b.style.fontSize=B.element.style.fontSize,B.refreshLayout(),h.style.height=B.kbdDiv.style.height,h.style.maxHeight=B.kbdDiv.style.maxHeight):h.innerHTML="

No "+c+" layout is defined for "+e.name+".

",h.style.border="1px solid #ccc",B.updateState();let F=o(()=>W(this,null,function*(){if(document.contains(h))try{yield B.styleSheetManager.allLoadedPromise();let U=B.styleSheet;U&&h.appendChild(U);let Q=[].concat(B.styleSheetManager.sheets);for(let p of Q)p!=U&&(p.href||(B.styleSheetManager.unlink(p),document.head.appendChild(p)));B.refreshLayout(),B.styleSheet=null,B.shutdown()}finally{G.disconnect()}}),"detectAndHandleInsertion"),G=new MutationObserver(F);G.observe(document.body,{childList:!0,subtree:!0}),b.append(h);for(let U of ye.STYLESHEET_FILES){let Q=`${i.resources}/osk/${U}`,p=B.styleSheetManager.linkExternalSheet(Q,!0);p.parentNode.removeChild(p),b.appendChild(p)}return B.appendStyleSheet(),delete B._width,delete B._height,b}onHide(){this.hkKey&&this.highlightKey(this.hkKey,!1)}optionKey(e,n,i){n.indexOf("K_LOPT")>=0?this.emit("globekey",e,i):n.indexOf("K_ROPT")>=0&&i&&this.emit("hiderequested",e)}showGesturePreview(e){let n=this.keytip,i=this.constructLayoutParams(),s=i.keyboardWidth*e.key.spec.proportionalWidth,l=i.keyboardHeight/this.currentLayer.rows.length,r=new vs(e,this.device.formFactor=="phone",s,l);return n==null?e.key.setPreview(r):n.show(e,!0,r),r.refreshLayout(),r}createKeyTip(){if(this.keytip==null)if(this.device.formFactor=="phone"){let e=this.isEmbedded;this.keytip=new Ii(this,e)}else this.keytip=new Ls(this);this.keytip&&this.keytip.element&&this.element.appendChild(this.keytip.element)}createGlobeHint(){return this.config.embeddedGestureConfig.createGlobeHint?this.config.embeddedGestureConfig.createGlobeHint(this):null}shutdown(){var e;this.styleSheet&&this.styleSheet.parentNode&&this.styleSheet.parentNode.removeChild(this.styleSheet),this.activeGestures.forEach(n=>n.cancel()),this.gestureEngine&&this.gestureEngine.destroy(),this.deleting&&window.clearTimeout(this.deleting),(e=this.keytip)==null||e.show(null,!1,null)}lockLayer(e){this.layerLocked=e}raiseKeyEvent(e,n){if(e.kName=="K_LOPT"||e.kName=="K_ROPT")return this.optionKey(n,e.kName,!0),{};let i={},s=o((l,r)=>{var g,d;i.contextToken=(g=l==null?void 0:l.transcription)==null?void 0:g.token;let c=(d=l==null?void 0:l.transcription)==null?void 0:d.transform;i.alteredText=l&&(!c||xt(c))},"keyEventCallback");return this.layerLocked&&(e.kNextLayer=this.layerId),this.emit("keyevent",e,s),i}};o(mi,"VisualKeyboard"),mi.specialCharacters=Xe.specialCharacters;var me=mi;var dc=class dc extends S.default{};o(dc,"Activator");var De=dc,uc=class uc extends De{get enabled(){return!0}set enabled(t){}get activate(){return!0}get conditionsMet(){return!0}};o(uc,"StaticActivator");var vt=uc;var Bc=class Bc{constructor(){this.map=new Map}promiseForTouchpoint(t){return this.map.get(t)||this.map.set(t,new f),this.map.get(t)}maintainTouches(t){let e=Array.from(this.map.keys());for(let n=0;n{if(!this.mayDisable&&!this.activationModel.enabled){this.activationModel.off("activate",this.activationListener);try{this.activationModel.enabled=!0}finally{this.activationModel.on("activate",this.activationListener)}}this.commonCheckAndDisplay()},"activationListener");this.layerChangeHandler=o((e,n)=>{var i,s;return this.vkbd&&this.vkbd._UpdateVKShiftStyle(n),(this.vkbd&&this.vkbd.layerId!=n||e.value!=n)&&(i=this.vkbd)!=null&&i.layerGroup.getLayer(n)&&!((s=this.vkbd)!=null&&s.layerLocked)&&(this.vkbd.layerId=n),!1},"layerChangeHandler");this._Visible=!1;this.config=e=y({},e),(i=this.config).gestureParams||(i.gestureParams=si),this.config.allowHideAnimations===void 0&&(this.config.allowHideAnimations=!0),this.config.device=e.device||e.hostDevice,this.config.isEmbedded=e.isEmbedded||!1,this.config.embeddedGestureConfig=e.embeddedGestureConfig||{},this.config.activator.on("activate",this.activationListener),this._Box=H("div"),this.kbdStyleSheetManager=new ge(this._Box,this.config.doCacheBusting||!1),this.uiStyleSheetManager=new ge(this._Box),this.bannerView=new rs,this.bannerView.events.on("bannerchange",()=>this.refreshLayout()),this._Box.appendChild(this.bannerView.element),this._bannerController=new oi(this.bannerView,this.hostDevice,this.config.predictionContextManager),this.keyboardView=this._GenerateKeyboardView(null,null),this._Box.appendChild(this.keyboardView.element);let n=$o(this.config);for(let s of bn.STYLESHEET_FILES){let l=`${n}${s}`;this.uiStyleSheetManager.linkExternalSheet(l)}this.setBaseMouseEventListeners(),this.hostDevice.touchable&&this.setBaseTouchEventListeners(),this._Box.style.display="none"}get keyCodes(){return C.keyCodes}get modifierCodes(){return C.modifierCodes}get modifierBitmasks(){return C.modifierBitmasks}get stateBitmasks(){return C.stateBitmasks}get gestureParams(){return this.config.gestureParams}get configuration(){return this.config}get bannerController(){return this._bannerController}get hostDevice(){return this.config.hostDevice}get fontRootPath(){return this.config.pathConfig.fonts}get isEmbedded(){return this.config.isEmbedded}setBaseMouseEventListeners(){this._Box.onmouseenter=e=>{this.mouseEnterPromise&&this.mouseEnterPromise.resolve(),this.mouseEnterPromise=new f,this.emit("pointerinteraction",this.mouseEnterPromise.corePromise)},this._Box.onmouseleave=e=>{this.mouseEnterPromise.resolve(),this.mouseEnterPromise=null}}removeBaseMouseEventListeners(){this._Box.onmouseenter=null,this._Box.onmouseleave=null}setBaseTouchEventListeners(){let e=o(function(n){return n.cancelable&&n.preventDefault(),n.stopPropagation(),!1},"commonPrevention");this._boxBaseTouchEventCancel=n=>(this.touchEventPromiseManager.maintainTouches(n.touches),e(n)),this._boxBaseTouchStart=n=>{for(let i=0;i0&&(g-=this.bannerView.height+5),this.vkbd.setSize(this.computedWidth,g,e);let d=this._Box.style;d.width=d.maxWidth=this.computedWidth+"px",d.height=d.maxHeight=this.computedHeight+"px"}else{let g=this._Box.style;g.width="auto",g.height="auto",g.maxWidth=g.maxHeight=""}}refreshLayoutIfNeeded(e){this.needsLayout&&this.refreshLayout(e)}postKeyboardLoad(){this._Visible=!1,this.postKeyboardAdjustments(),this.displayIfActive&&this.present()}loadActiveKeyboard(){var s,l,r,c,g;this.setBoxStyling(),this.needsLayout=!0;let e=this.keyboardView,n=this.kbdStyleSheetManager;this.kbdStyleSheetManager=new ge(this._Box,this.config.doCacheBusting||!1);let i=this.keyboardView=this._GenerateKeyboardView((s=this.keyboardData)==null?void 0:s.keyboard,(l=this.keyboardData)==null?void 0:l.metadata);if(this._Box.replaceChild(i.element,e.element),i.postInsert(),(g=this.bannerController)==null||g.configureForKeyboard((r=this.keyboardData)==null?void 0:r.keyboard,(c=this.keyboardData)==null?void 0:c.metadata),e instanceof me&&e.shutdown(),n.unlinkAll(),this.banner.appendStyles(),this.vkbd){this.vkbd.createKeyTip();let d=this.vkbd.createGlobeHint();d&&this._Box.appendChild(d.element),this.vkbd.appendStyleSheet()}this.postKeyboardLoad()}_GenerateKeyboardView(e,n){let i=this.targetDevice;return this._Box.className="",e==null&&!i.touchable?new Wt:e&&e.layout(i.formFactor)?this._GenerateVisualKeyboard(e,n):!e||!n?this._GenerateVisualKeyboard(null,null):new ai(e)}_GenerateVisualKeyboard(e,n){let i=this.targetDevice,s=$o(this.config),l=new me({keyboard:e,keyboardMetadata:n,device:i,hostDevice:this.hostDevice,topContainer:this._Box,styleSheetManager:this.kbdStyleSheetManager,pathConfig:this.config.pathConfig,embeddedGestureConfig:this.config.embeddedGestureConfig,isEmbedded:this.config.isEmbedded,specialFont:{family:"SpecialOSK",files:[`${s}/keymanweb-osk.ttf`],path:""},gestureParams:this.config.gestureParams});return l.on("keyevent",(r,c)=>this.emit("keyevent",r,c)),l.on("globekey",(r,c)=>this.emit("globekey",r,c)),l.on("hiderequested",r=>{this.doHide(!0),this.emit("hiderequested",r)}),this._Box.className=i.formFactor+" "+i.OS.toLowerCase()+" kmw-osk-frame",l}present(){if(this.mayShow()){if(this.keyboardView.updateState(),this._Box.style.display="block",this.refreshLayoutIfNeeded(),this._Visible=!0,this._Box.style.opacity="1",this._Box.style.visibility=="hidden"){let e=this;window.setTimeout(function(){e._Box.style.visibility="visible"},0)}this.setDisplayPositioning()}}startHide(e){if(!this.mayHide(e))return;e&&(this.activationModel.enabled=!!(this.keyboardData.keyboard.isCJK||this.hostDevice.touchable));let n=null;this._Box&&this.hostDevice.touchable&&!(this.keyboardView instanceof Wt)&&this.config.allowHideAnimations?n=this.useHideAnimation():n=Promise.resolve(!0);let i=this;n.then(function(s){s&&i.finalizeHide()}),this.doHide(e)}finalizeHide(){if(!(document.body.className.indexOf("osk-always-visible")>=0&&this.hostDevice.formFactor=="desktop")){if(this._Box){let e=this._Box.style;e.display="none",e.transition="",e.opacity="1",this._Visible=!1}this.vkbd&&this.vkbd.onHide()}}mayShow(){return!(!this.activationModel.conditionsMet||!this.keyboardView||this.keyboardView instanceof Wt||!this.activationModel.enabled||!this._Box)}mayHide(e){return!(this.activationModel.conditionsMet&&!this.mayDisable||this.activationModel instanceof vt||!e&&this.hostDevice.formFactor=="desktop"&&document.body.className.indexOf("osk-always-visible")>=0)}useHideAnimation(){let e=this._Box.style,n=this;return new Promise(function(i){let s=o(function(){return n._Box.removeEventListener("transitionend",s,!1),n._Box.removeEventListener("webkitTransitionEnd",s,!1),n._Box.removeEventListener("transitioncancel",s,!1),n._Box.removeEventListener("webkitTransitionCancel",s,!1),n._animatedHideTimeout!=0&&window.clearTimeout(n._animatedHideTimeout),n._animatedHideTimeout=0,n._Visible&&n.activationModel.conditionsMet?(e.transition="",e.opacity="1",i(!1),!1):(i(!0),!0)},"cleanup"),l=o(function(){n._Box.removeEventListener("transitionrun",l,!1),n._Box.removeEventListener("webkitTransitionRun",l,!1),n._Box.addEventListener("transitionend",s,!1),n._Box.addEventListener("webkitTransitionEnd",s,!1),n._Box.addEventListener("transitioncancel",s,!1),n._Box.addEventListener("webkitTransitionCancel",s,!1)},"startup");n._Box.addEventListener("transitionrun",l,!1),n._Box.addEventListener("webkitTransitionRun",l,!1),e.transition="opacity 0.5s linear 0",e.opacity="0",n._animatedHideTimeout=window.setTimeout(s,200)})}hideNow(){if(!this.mayHide(!1)||!this._Box)return;this._animatedHideTimeout&&(window.clearTimeout(this._animatedHideTimeout),this._animatedHideTimeout=0);let e=this._Box.style;e.transition="",e.opacity="0",this.finalizeHide()}shutdown(){this.removeBaseMouseEventListeners(),this.removeBaseTouchEventListeners();var e=this._Box;e.parentElement&&e.parentElement.removeChild(e),this.kbdStyleSheetManager.unlinkAll(),this.uiStyleSheetManager.unlinkAll(),this.bannerController.shutdown()}getRect(){var e={};return e.left=e.left=K(this._Box),e.top=e.top=z(this._Box),e.width=this.computedWidth,e.height=this.computedHeight,e}isEnabled(){return this.displayIfActive}isVisible(){return this._Visible}hide(){this.activationModel.enabled=!1,this.startHide(!0)}show(e){arguments.length>0?this.activationModel.enabled=e:this.activationModel.conditionsMet&&(this.activationModel.enabled=!this.activationModel.enabled)}doShow(e){this.legacyEvents.callEvent("show",e)}doHide(e){let n={HiddenByUser:e};this.legacyEvents.callEvent("hide",n)}addEventListener(e,n){this.legacyEvents.addEventListener(e,n)}removeEventListener(e,n){this.legacyEvents.removeEventListener(e,n)}};o(bn,"OSKView"),bn.STYLESHEET_FILES=["kmwosk.css","globe-hint.css"];var ye=bn;var pi=class pi extends S.default{constructor(e){super();this.mouseCancellingHandler=o(function(e){return e.preventDefault(),e.cancelBubble=!0,!1},"mouseCancellingHandler");this._element=this.buildTitleBar(),this.helpEnabled=!1,this.configEnabled=!1,e&&(this.element.onmousedown=e.mouseDownHandler)}get helpEnabled(){return this._helpEnabled}set helpEnabled(e){this._helpEnabled=e,this._helpButton.style.display=e?"inline":"none"}get configEnabled(){return this._configEnabled}set configEnabled(e){this._configEnabled=e,this._configButton.style.display=e?"inline":"none"}get layoutHeight(){return pi.DISPLAY_HEIGHT}get element(){return this._element}setPinCJKOffset(){this._unpinButton.style.left="15px"}showPin(e){this._unpinButton.style.display=e?"block":"none"}setTitle(e){this._caption.innerHTML=e}setTitleFromKeyboard(e){let n=""+(e==null?void 0:e.name)+"";this._caption.innerHTML=n}buildTitleBar(){let e=H("div");e.id="keymanweb_title_bar",e.className="kmw-title-bar";var n=this._caption=H("span");n.className="kmw-title-bar-caption",n.style.color="#fff",e.appendChild(n);var i=this._closeButton=this.buildCloseButton();return this._closeButton.onclick=()=>(this.emit("close"),!1),e.appendChild(i),i=this._helpButton=this.buildHelpButton(),this._helpButton.onclick=()=>(this.emit("help"),!1),e.appendChild(i),i=this._configButton=this.buildConfigButton(),this._configButton.onclick=()=>(this.emit("config"),!1),e.appendChild(i),i=this._unpinButton=this.buildUnpinButton(),this._unpinButton.onclick=()=>(this.emit("unpin"),!1),e.appendChild(i),e}buildCloseButton(){var e=H("div");return e.id="kmw-close-button",e.className="kmw-title-bar-image",e.onmousedown=this.mouseCancellingHandler,e}buildHelpButton(){let e=H("div");return e.id="kmw-help-image",e.className="kmw-title-bar-image",e.title="KeymanWeb Help",e.onmousedown=this.mouseCancellingHandler,e}buildConfigButton(){let e=H("div");return e.id="kmw-config-image",e.className="kmw-title-bar-image",e.title="KeymanWeb Configuration Options",e.onmousedown=this.mouseCancellingHandler,e}buildUnpinButton(){let e=H("div");return e.id="kmw-pin-image",e.className="kmw-title-bar-image",e.title="Pin the On Screen Keyboard to its default location on the active text box",e.onmousedown=this.mouseCancellingHandler,e}refreshLayout(){}};o(pi,"TitleBar"),pi.DISPLAY_HEIGHT=x.inPixels(20);var hn=pi;var Ci=class Ci extends S.default{constructor(e){super();this.mouseCancellingHandler=o(function(e){return e.preventDefault(),e.cancelBubble=!0,!1},"mouseCancellingHandler");this._element=this.buildResizeBar(),e&&(this._resizeHandle.onmousedown=e.mouseDownHandler)}get layoutHeight(){return Ci.DISPLAY_HEIGHT}get element(){return this._element}get handle(){return this._resizeHandle}allowResizing(e){this._resizeHandle.style.display=e?"block":"none"}buildResizeBar(){var e=H("div");e.className="kmw-footer",e.onmousedown=this.mouseCancellingHandler;var n=H("div");n.className="kmw-footer-caption",n.innerHTML='KeymanWeb',n.id="keymanweb-osk-footer-caption",n.addEventListener("dblclick",s=>(this.emit("showbuild"),!1),!1),e.appendChild(n);var i=H("div");return i.className="kmw-footer-resize",e.appendChild(i),this._resizeHandle=i,e}refreshLayout(){}};o(Ci,"ResizeBar"),Ci.DISPLAY_HEIGHT=x.inPixels(16);var Gi=Ci;var Fn=class Fn{constructor(t,e,n){this.x=t,this.y=e}static fromEvent(t){let e;if(window.TouchEvent&&t instanceof TouchEvent||t.changedTouches?e=t.changedTouches[0]:e=t,e.pageX)return new Fn(e.pageX,e.pageY,t);if(e.clientX){let n=e.clientX+document.body.scrollLeft,i=e.clientY+document.body.scrollTop;return new Fn(n,i,t)}else return new Fn(null,null,t)}};o(Fn,"InputEventCoordinate");var Hs=Fn,bc=class bc{constructor(t){this._VPreviousMouseMove=document.onmousemove,this._VPreviousMouseUp=document.onmouseup,this._VPreviousCursor=document.body.style.cursor,this._VPreviousMouseButton=typeof t.which=="undefined"?t.button:t.which}restore(){document.onmousemove=this._VPreviousMouseMove,document.onmouseup=this._VPreviousMouseUp,document.body.style.cursor&&(document.body.style.cursor=this._VPreviousCursor)}matchesCausingClick(t){return this._VPreviousMouseButton==(typeof t.which=="undefined"?t.button:t.which)}};o(bc,"MouseStartSnapshot");var Ic=bc,hc=class hc{constructor(t){this.startHandler=this._VMoveMouseDown.bind(this),this.cursorType=t}get enabled(){return this._enabled}set enabled(t){this._enabled=t}get isActive(){return!!this._mouseStartSnapshot}get mouseDownHandler(){return this.startHandler}_VMoveMouseDown(t){return!t||!this._enabled?!0:(this._mouseStartSnapshot||(this._mouseStartSnapshot=new Ic(t)),this._startCoord=Hs.fromEvent(t),document.onmousemove=this._VMoveMouseMove.bind(this),document.onmouseup=this._VMoveMouseUp.bind(this),document.body.style.cursor&&(document.body.style.cursor=this.cursorType),t.preventDefault(),t.cancelBubble=!0,this.onDragStart(),!1)}_VMoveMouseMove(t){if(!t||!this.enabled)return!0;if(t.preventDefault(),t.cancelBubble=!0,this._mouseStartSnapshot.matchesCausingClick(t)){let e=Hs.fromEvent(t),n=e.x-this._startCoord.x,i=e.y-this._startCoord.y;return this.onDragMove(n,i),!1}else return this._VMoveMouseUp(t)}_VMoveMouseUp(t){return t?(this._mouseStartSnapshot.restore(),this._mouseStartSnapshot=null,t.preventDefault(),t.cancelBubble=!0,this.onDragRelease(),!1):!0}};o(hc,"MouseDragOperation");var mn=hc;var Fc=class Fc extends De{constructor(){super(...arguments);this._enabled=!0;this.actValue=null}get activate(){return this._enabled&&!!this.actValue}checkState(e){this.activate!=e&&this.emit("activate",this.activate)}get enabled(){return this._enabled}set enabled(e){let n=this.activate;this._enabled=e,this.checkState(n)}get activationTrigger(){return this.actValue}set activationTrigger(e){let n=this.activate,i=this.actValue;this.actValue=e,this.checkState(n),i!=e&&this.emit("triggerchange",e)}get conditionsMet(){return!!this.activationTrigger}};o(Fc,"TwoStateActivator");var Bt=Fc;var mc=class mc extends le{constructor(){super("KeymanWeb_OnScreenKeyboard")}loadWithDefaults(t){return y(y({},t),this.load())}load(){let t=super.load((e,n)=>{switch(n){case"version":return e;default:return Number.parseInt(e,10)}});return t.width||delete t.width,t.height||delete t.height,t}save(t){super.save(t)}};o(mc,"FloatingOSKCookieSerializer");var Js=mc;var yc=class yc extends ye{constructor(e){e.activator=e.activator||new Bt;super(e);this.userPositioned=!1;this.specifiedPosition=!1;this.noDrag=!1;this.layoutSerializer=new Js;this.restorePosition=function(e){let n=this._Visible,i=new f;this.emit("dragmove",i.corePromise),this.loadPersistedLayout(),this.userPositioned=!1,e||(delete this.dfltX,delete this.dfltY),this.savePersistedLayout(),n&&this.present(),this.titleBar.showPin(!1),i.resolve(),this.doResizeMove()}.bind(this);this.typedActivationModel.on("triggerchange",()=>this.setDisplayPositioning()),document.body.appendChild(this._Box),this.titleBar=new hn(this.titleDragHandler),this.titleBar.on("help",()=>{this.legacyEvents.callEvent("helpclick",{})}),this.titleBar.on("config",()=>{this.legacyEvents.callEvent("configclick",{})}),this.titleBar.on("close",()=>this.startHide(!0)),this.titleBar.on("unpin",()=>this.restorePosition(!0)),this.resizeBar=new Gi(this.resizeDragHandler),this.resizeBar.on("showbuild",()=>this.emit("showbuild")),this.headerView=this.titleBar,this._Box.insertBefore(this.headerView.element,this._Box.firstChild);let n=o(l=>{let r=this.headerView;if(r&&r instanceof hn)switch(l){case"configclick":r.configEnabled=this.legacyEvents.listenerCount("configclick")>0;break;case"helpclick":r.helpEnabled=this.legacyEvents.listenerCount("helpclick")>0;break;default:return}},"onListenedEvent"),i=new sn(this),s=new sn(this.legacyEvents);for(let l of[i,s])l.on("listeneradded",n),l.on("listenerremoved",n);this.activeKeyboard&&this.postKeyboardAdjustments(),this.loadPersistedLayout()}get typedActivationModel(){return this.activationModel}_Unload(){this.keyboardView=null,this.bannerView=null,this._Box=null}setBoxStyling(){let e=this._Box.style;e.zIndex="9999",e.display="none",e.width="auto",e.position="absolute"}postKeyboardAdjustments(){this.titleBar&&(this.enableMoveResizeHandlers(),this.activeKeyboard&&this.titleBar.setTitleFromKeyboard(this.activeKeyboard.keyboard),this.vkbd?(this.footerView=this.resizeBar,this._Box.appendChild(this.footerView.element)):(this.footerView&&this._Box.removeChild(this.footerView.element),this.footerView=null),this.loadPersistedLayout(),this.setNeedsLayout())}isEnabled(){return this.displayIfActive}isVisible(){return this._Visible}savePersistedLayout(){var e=this.getPos();let n={visible:this.displayIfActive?1:0,userSet:this.userPositioned?1:0,left:e.left,top:e.top,_version:M.CURRENT.toString()};this.vkbd&&(n.width=this.width.val,n.height=this.height.val),this.layoutSerializer.save(n)}loadPersistedLayout(){let e=this.layoutSerializer.loadWithDefaults({visible:1,userSet:0,left:-1,top:-1,_version:void 0,width:.3*screen.width,height:.15*screen.height});this.activationModel.enabled=e.visible==1,this.userPositioned=e.userSet==1,this.x=e.left,this.y=e.top;let n=e._version,i=n===void 0,s=e.width,l=e.height;s<.2*screen.width&&(s=.2*screen.width),l<.1*screen.height&&(l=.1*screen.height),s>.9*screen.width&&(s=.9*screen.width),l>.5*screen.height&&(l=.5*screen.height),(i||!n)&&(this.headerView&&this.headerView.layoutHeight.absolute&&(l+=this.headerView.layoutHeight.val),this.footerView&&this.footerView.layoutHeight.absolute&&(l+=this.footerView.layoutHeight.val)),this.setSize(s,l),(this.x==-1||this.y==-1||!this._Box)&&(this.userPositioned=!1),this.x.9*screen.width&&(r=.9*screen.width),l.width=r+"px",this.setSize(r,this.computedHeight,!0)}if("height"in e){var c=e.height-(n.offsetHeight-s.offsetHeight);c<.1*screen.height&&(c=.1*screen.height),c>.5*screen.height&&(c=.5*screen.height),l.height=c+"px",l.fontSize=c/8+"px",this.setSize(this.computedWidth,c,!0)}"nosize"in e&&(this.resizingEnabled=!e.nosize)}"nomove"in e&&(this.noDrag=e.nomove,this.movementEnabled=!this.noDrag),this.savePersistedLayout()}}getPos(){var e=this._Box,n={left:this._Visible?e.offsetLeft:this.x,top:this._Visible?e.offsetTop:this.y};return n}setPos(e){if(typeof this._Box!="undefined"){if(this.userPositioned){var n=e.left,i=e.top;typeof n!="undefined"&&(n<-.8*this._Box.offsetWidth&&(n=-.8*this._Box.offsetWidth),this.userPositioned&&(this._Box.style.left=n+"px",this.x=n)),typeof i!="undefined"&&(i<0&&(i=0),this.userPositioned&&(this._Box.style.top=i+"px",this.y=i))}this.titleBar.showPin(this.userPositioned)}}setDisplayPositioning(){var e=this._Box.style;if(e.position="absolute",this.activationModel.activate&&(e.display="block"),e.left="0px",this.specifiedPosition||this.userPositioned)e.left=this.x+"px",e.top=this.y+"px";else{let n=this.typedActivationModel.activationTrigger||null;this.dfltX?e.left=this.dfltX:typeof n!="undefined"&&n!=null&&(e.left=K(n)+"px"),this.dfltY?e.top=this.dfltY:typeof n!="undefined"&&n!=null&&(e.top=z(n)+n.offsetHeight+"px")}this.specifiedPosition=!1}presentAtPosition(e,n){this.mayShow()&&(this.specifiedPosition=e>=0||n>=0,this.specifiedPosition&&(this.x=e,this.y=n),this.specifiedPosition=this.specifiedPosition||this.userPositioned,this.present())}present(){this.mayShow()&&(this.titleBar.showPin(this.userPositioned),super.present(),this.doShow({x:this._Box.offsetLeft,y:this._Box.offsetTop,userLocated:this.userPositioned}))}startHide(e){super.startHide(e),e&&this.savePersistedLayout()}show(e){e!==void 0?super.show(e):super.show(),this.savePersistedLayout()}userLocated(){return this.userPositioned}get movementEnabled(){return this.titleDragHandler.enabled}set movementEnabled(e){this.titleDragHandler.enabled=e,this.titleBar.showPin(e&&this.userPositioned)}get resizingEnabled(){return this.resizeDragHandler.enabled}set resizingEnabled(e){this.resizeDragHandler.enabled=e,this.resizeBar.allowResizing(e)}get isBeingMoved(){return this.titleDragHandler.isActive}get isBeingResized(){return this.resizeDragHandler.isActive}enableMoveResizeHandlers(){this.titleDragHandler.enabled=!this.noDrag,this.resizeDragHandler.enabled=!0}get titleDragHandler(){let e=this;return this._moveHandler?this._moveHandler:(this._moveHandler=new class extends mn{constructor(){super("move")}onDragStart(){this.startX=e._Box.offsetLeft,this.startY=e._Box.offsetTop,e.activeKeyboard.keyboard.isCJK&&e.titleBar.setPinCJKOffset(),this.dragPromise&&this.dragPromise.resolve(),this.dragPromise=new f,e.emit("dragmove",this.dragPromise.corePromise)}onDragMove(i,s){e.titleBar.showPin(!0),e.userPositioned=!0,e._Box.style.left=this.startX+i+"px",e._Box.style.top=this.startY+s+"px";var l=e.getRect();e.setSize(l.width,l.height,!0),e.x=l.left,e.y=l.top}onDragRelease(){e.vkbd&&(e.vkbd.currentKey=null),this.dragPromise.resolve(),this.dragPromise.then(()=>{e.userPositioned=!0,e.doResizeMove(),e.savePersistedLayout()}),this.dragPromise=null}},this._moveHandler)}get resizeDragHandler(){let e=this;return this._resizeHandler?this._resizeHandler:(this._resizeHandler=new class extends mn{constructor(){super("se-resize")}onDragStart(){this.startWidth=e.computedWidth,this.startHeight=e.computedHeight,this.dragPromise&&this.dragPromise.resolve(),this.dragPromise=new f,e.emit("resizemove",this.dragPromise.corePromise)}onDragMove(i,s){let l=this.startWidth+i,r=this.startHeight+s;l<.2*screen.width&&(l=.2*screen.width),r<.1*screen.height&&(r=.1*screen.height),l>.9*screen.width&&(l=.9*screen.width),r>.5*screen.height&&(r=.5*screen.height),e.setSize(l,r,!0)}onDragRelease(){e.vkbd&&(e.vkbd.currentKey=null),e.vkbd&&(this.startWidth=e.computedWidth,this.startHeight=e.computedHeight),e.refreshLayout(),this.dragPromise.resolve(),this.dragPromise.then(()=>{e.doResizeMove(),e.savePersistedLayout()}),this.dragPromise=null}},this._resizeHandler)}};o(yc,"FloatingOSKView");var Oe=yc;var pc=class pc extends ye{constructor(e){e.isEmbedded?e.activator=e.activator||new vt:e.activator=e.activator||new Bt;super(e);this.isResizing=!1;this.restorePosition=function(e){}.bind(this);document.body.appendChild(this._Box)}_Unload(){this.keyboardView=null,this.bannerView=null,this._Box=null}setBoxStyling(){let e=this._Box.style;e.zIndex="9999",e.display="none",e.width="100%",e.position="fixed"}refreshLayout(e){if(!this.isResizing){try{this.isResizing=!0,this.doResize()}finally{this.isResizing=!1}super.refreshLayout(e)}}doResize(){if(this.vkbd){let e=this.getDefaultKeyboardHeight();this.setSize(this.getDefaultWidth(),e+this.banner.height)}}postKeyboardAdjustments(){this.doResize()}getDefaultKeyboardHeight(){var r,c;let e=this.targetDevice;if(this.configuration.heightOverride)return this.configuration.heightOverride();let n=(r=document==null?void 0:document.documentElement)==null?void 0:r.clientWidth,i=(c=document==null?void 0:document.documentElement)==null?void 0:c.clientHeight;if(typeof n=="undefined"&&(n=Math.min(screen.height,screen.width),i=Math.max(screen.height,screen.width),be())){let g=n;n=i,i=g}var s=Math.floor(Math.min(i,n)/2),l=s;return e.formFactor=="phone"&&(be()?l=Math.floor(i/1.6):l=Math.floor(i/2.4)),this.targetDevice.OS==V.OperatingSystem.iOS&&(l=l/Se(this.targetDevice.formFactor)),l}getDefaultWidth(){var i;let e=this.targetDevice;if(this.configuration.widthOverride)return this.configuration.widthOverride();var n;return n=(i=document==null?void 0:document.documentElement)==null?void 0:i.clientWidth,typeof n=="undefined"&&(this.targetDevice.OS==V.OperatingSystem.iOS?n=window.innerWidth:e.OS==V.OperatingSystem.Android?n=screen.availWidth:n=screen.width),n}setRect(e){}getPos(){var e=this._Box,n={left:this._Visible?e.offsetLeft:this.x,top:this._Visible?e.offsetTop:this.y};return n}setPos(e){}setDisplayPositioning(){let e=this._Box.style;this.vkbd&&(e.position="fixed",e.left=e.bottom="0px",e.border="none",e.borderTop="1px solid gray")}present(){super.present(),this.legacyEvents.callEvent("show",{})}};o(pc,"AnchoredOSKView");var yn=pc;var Cc=class Cc extends De{constructor(){super(...arguments);this.flag=!0}get enabled(){return this.flag}set enabled(e){this.activate=e}get activate(){return this.flag}set activate(e){this.flag!=e&&(this.flag=e,this.emit("activate",e))}get conditionsMet(){return!0}};o(Cc,"SimpleActivator");var pn=Cc;var Gc=class Gc extends ye{constructor(e){e.activator=e.activator||new pn;super(e);this.restorePosition=function(e){}.bind(this)}get element(){return this._Box}_Unload(){this.keyboardView=null,this.bannerView=null,this._Box=null}setBoxStyling(){let e=this._Box.style;e.display="none",e.position="relative"}postKeyboardAdjustments(){}getDefaultKeyboardHeight(){return this.keyboardView instanceof me?this.keyboardView.height:this.computedHeight}getDefaultWidth(){return this.computedWidth}setRect(e){}getPos(){var e=this._Box,n={left:this._Visible?e.offsetLeft:void 0,top:this._Visible?e.offsetTop:void 0};return n}setPos(e){}present(){super.present(),this.legacyEvents.callEvent("show",{})}setDisplayPositioning(){}allowsDeviceChange(e){return!0}};o(Gc,"InlinedOSKView");var Cn=Gc;var xc={};Sn(xc,{AnchoredOSKView:()=>Qi,FloatingOSKView:()=>Ui,InlinedOSKView:()=>Qc});function Uc(a){return{hostDevice:a.config.hostDevice,pathConfig:a.config.paths,predictionContextManager:a.contextManager.predictionContext,isEmbedded:!1}}o(Uc,"buildBaseOskConfiguration");var Zc=class Zc extends yn{constructor(t,e){let n=y(y({},Uc(t)),e||{});super(n)}};o(Zc,"PublishedAnchoredOSKView");var Qi=Zc,Xc=class Xc extends Oe{constructor(t,e){let n=y(y({},Uc(t)),e||{});super(n)}};o(Xc,"PublishedFloatingOSKView");var Ui=Xc,Sc=class Sc extends Cn{constructor(t,e){let n=y(y({},Uc(t)),e||{});super(n)}};o(Sc,"PublishedInlineOSKView");var Qc=Sc;var Vc=class Vc extends it{constructor(){super(...arguments);this.events=new S.default;this.changed=!1}focus(){let e=this.getElement();e.focus&&e.focus()}isForcingScroll(){return!1}dispatchInputEventOn(e){let n;window.InputEvent&&(n=new InputEvent("input",{bubbles:!0,cancelable:!1})),e&&n&&e.dispatchEvent(n)}};o(Vc,"OutputTarget");var O=Vc;var Ac=class Ac extends O{constructor(e){super();this.root=e,this._cachedSelectionStart=-1}get isSynthetic(){return!1}static isSupportedType(e){return e=="email"||e=="search"||e=="text"||e=="url"}getElement(){return this.root}clearSelection(){this.getCaret(),this.root.value=this.root.value._kmwSubstring(0,this.processedSelectionStart)+this.root.value._kmwSubstring(this.processedSelectionEnd),this.setCaret(this.processedSelectionStart)}isSelectionEmpty(){return this.root.selectionStart==this.root.selectionEnd}hasSelection(){return!0}invalidateSelection(){this._cachedSelectionStart=-1}getCaret(){return this.root.selectionStart!=this._cachedSelectionStart&&(this._cachedSelectionStart=this.root.selectionStart,this.processedSelectionStart=this.root.value._kmwCodeUnitToCodePoint(this.root.selectionStart),this.processedSelectionEnd=this.root.value._kmwCodeUnitToCodePoint(this.root.selectionEnd)),this.root.selectionDirection=="forward"?this.processedSelectionEnd:this.processedSelectionStart}getDeadkeyCaret(){return this.getCaret()}setCaret(e){this.setSelection(e,e,"none")}setSelection(e,n,i){let s=this.root.value._kmwCodePointToCodeUnit(e),l=this.root.value._kmwCodePointToCodeUnit(n);this.root.setSelectionRange(s,l,i),this.processedSelectionStart=e,this.processedSelectionEnd=n,this.forceScroll(),this.root.setSelectionRange(s,l,i)}forceScroll(){let e=this.getElement(),n=e.selectionStart,i=e.selectionEnd;this._activeForcedScroll=!0;try{e.blur(),e.focus()}finally{e.selectionStart=n,e.selectionEnd=i,this._activeForcedScroll=!1}}isForcingScroll(){return this._activeForcedScroll}getSelectionDirection(){return this.root.selectionDirection}getTextBeforeCaret(){return this.getCaret(),this.getText()._kmwSubstring(0,this.processedSelectionStart)}getSelectedText(){return this.getCaret(),this.getText()._kmwSubstring(this.processedSelectionStart,this.processedSelectionEnd)}setTextBeforeCaret(e){this.getCaret();let n=this.processedSelectionEnd-this.processedSelectionStart,i=this.getSelectionDirection(),s=e._kmwLength();this.root.value=e+this.getText()._kmwSubstring(this.processedSelectionStart),this.setSelection(s,s+n,i)}setTextAfterCaret(e){let n=this.getSelectionDirection();this.root.value=this.getTextBeforeCaret()+e,this.setSelection(this.processedSelectionStart,this.processedSelectionEnd,n)}getTextAfterCaret(){return this.getCaret(),this.getText()._kmwSubstring(this.processedSelectionEnd)}getText(){return this.root.value}deleteCharsBeforeCaret(e){if(e>0){let n=this.getTextBeforeCaret(),i=this.processedSelectionStart;e>i&&(e=i),this.adjustDeadkeys(-e),this.setTextBeforeCaret(n.kmwSubstring(0,i-e)),this.setCaret(i-e)}}insertTextBeforeCaret(e){if(!e)return;let n=this.getCaret(),i=this.getTextBeforeCaret(),s=this.getText()._kmwSubstring(this.processedSelectionStart);this.adjustDeadkeys(e._kmwLength()),this.root.value=i+e+s,this.setCaret(n+e._kmwLength())}handleNewlineAtCaret(){let e=this.root;e&&(e.type=="search"||e.type=="submit")?(e.disabled=!1,e.form.submit()):this.events.emit("unhandlednewline",e)}doInputEvent(){this.dispatchInputEventOn(this.root)}};o(Ac,"Input");var Pe=Ac;var Wc=class Wc extends O{constructor(e){super();this.root=e,this._cachedSelectionStart=-1}get isSynthetic(){return!1}getElement(){return this.root}clearSelection(){this.getCaret(),this.root.value=this.root.value._kmwSubstring(0,this.processedSelectionStart)+this.root.value._kmwSubstring(this.processedSelectionEnd),this.setCaret(this.processedSelectionStart)}isSelectionEmpty(){return this.root.selectionStart==this.root.selectionEnd}hasSelection(){return!0}invalidateSelection(){this._cachedSelectionStart=-1}getCaret(){return this.root.selectionStart!=this._cachedSelectionStart&&(this._cachedSelectionStart=this.root.selectionStart,this.processedSelectionStart=this.root.value._kmwCodeUnitToCodePoint(this.root.selectionStart),this.processedSelectionEnd=this.root.value._kmwCodeUnitToCodePoint(this.root.selectionEnd)),this.root.selectionDirection=="forward"?this.processedSelectionEnd:this.processedSelectionStart}getDeadkeyCaret(){return this.getCaret()}setCaret(e){this.setSelection(e,e,"none")}setSelection(e,n,i){let s=this.root.value._kmwCodePointToCodeUnit(e),l=this.root.value._kmwCodePointToCodeUnit(n);this.root.setSelectionRange(s,l,i),this.processedSelectionStart=e,this.processedSelectionEnd=n,this.forceScroll(),this.root.setSelectionRange(s,l,i)}forceScroll(){let e=this.getElement(),n=e.selectionStart,i=e.selectionEnd;this._activeForcedScroll=!0;try{e.blur(),e.focus()}finally{e.selectionStart=n,e.selectionEnd=i,this._activeForcedScroll=!1}}isForcingScroll(){return this._activeForcedScroll}getSelectionDirection(){return this.root.selectionDirection}getTextBeforeCaret(){return this.getCaret(),this.getText()._kmwSubstring(0,this.processedSelectionStart)}setTextBeforeCaret(e){this.getCaret();let n=this.processedSelectionEnd-this.processedSelectionStart,i=this.getSelectionDirection(),s=e._kmwLength();this.root.value=e+this.getText()._kmwSubstring(this.processedSelectionStart),this.setSelection(s,s+n,i)}setTextAfterCaret(e){let n=this.getSelectionDirection();this.root.value=this.getTextBeforeCaret()+e,this.setSelection(this.processedSelectionStart,this.processedSelectionEnd,n)}getTextAfterCaret(){return this.getCaret(),this.getText()._kmwSubstring(this.processedSelectionEnd)}getSelectedText(){return this.getCaret(),this.getText()._kmwSubstring(this.processedSelectionStart,this.processedSelectionEnd)}getText(){return this.root.value}deleteCharsBeforeCaret(e){if(e>0){let n=this.getTextBeforeCaret(),i=this.processedSelectionStart;e>i&&(e=i),this.adjustDeadkeys(-e),this.setTextBeforeCaret(n.kmwSubstring(0,i-e)),this.setCaret(i-e)}}insertTextBeforeCaret(e){if(!e)return;let n=this.getCaret(),i=this.getTextBeforeCaret(),s=this.getText()._kmwSubstring(this.processedSelectionStart);this.adjustDeadkeys(e._kmwLength()),this.root.value=i+e+s,this.setCaret(n+e._kmwLength())}handleNewlineAtCaret(){this.insertTextBeforeCaret(` +`)}doInputEvent(){this.dispatchInputEventOn(this.root)}};o(Wc,"TextArea");var Gn=Wc;var fc=class fc{constructor(t,e){this.node=t,this.offset=e}};o(fc,"SelectionCaret");var xi=fc,Lc=class Lc{constructor(t,e){this.start=t,this.end=e}};o(Lc,"SelectionRange");var Zi=Lc,Rc=class Rc{constructor(t,e){this.cmd=t,this.stateType=e}};o(Rc,"StyleCommand");var Be=Rc,vc=class vc extends O{constructor(e){super();if(this.root=e,e.contentWindow&&e.contentWindow.document&&e.contentWindow.document.designMode=="on")this.doc=e.contentWindow.document,this.docRoot=e.contentWindow.document.documentElement;else throw"Specified IFrame is not in design-mode!"}get isSynthetic(){return!1}getElement(){return this.root}focus(){this.doc.defaultView.focus()}isSelectionEmpty(){return this.hasSelection()?this.doc.getSelection().isCollapsed:!0}hasSelection(){let e=this.doc.getSelection(),n=document.getSelection();return n.anchorNode==e.anchorNode&&n.focusNode==e.focusNode,!0}clearSelection(){if(this.hasSelection()){let e=this.doc.getSelection();e.isCollapsed||e.deleteFromDocument()}else console.warn("Attempted to clear an unowned Selection!")}invalidateSelection(){}getCarets(){let e=this.doc.getSelection(),n=e.anchorNode.compareDocumentPosition(e.focusNode);if(e.isCollapsed){let i=new xi(e.anchorNode,e.anchorOffset);return new Zi(i,i)}else{let i=new xi(e.anchorNode,e.anchorOffset),s=new xi(e.focusNode,e.focusOffset);return i.node==s.node&&(n=s.offset-i.offset>0?2:4),n&2?new Zi(i,s):new Zi(s,i)}}getDeadkeyCaret(){return this.getTextBeforeCaret().kmwLength()}getTextBeforeCaret(){if(!this.hasSelection())return this.getText();let e=this.getCarets().start;return e.node.nodeType!=3?"":e.node.textContent.substr(0,e.offset)}getSelectedText(){return""}getTextAfterCaret(){if(!this.hasSelection())return"";let e=this.getCarets().end;return e.node.nodeType!=3?"":e.node.textContent.substr(e.offset)}getText(){return this.docRoot.innerText}deleteCharsBeforeCaret(e){if(!this.hasSelection()||e<=0)return;let n=this.getCarets().start;if(e>n.offset&&(e=n.offset),n.node.nodeType!=3){console.warn("Deletion of characters requested without available context!");return}let i=this.doc.createRange(),s=n.offset-n.node.nodeValue.substr(0,n.offset)._kmwSubstr(-e).length;i.setStart(n.node,s),i.setEnd(n.node,n.offset),this.adjustDeadkeys(-e),i.deleteContents()}insertTextBeforeCaret(e){if(!this.hasSelection())return;let n=this.getCarets().start,i=e._kmwLength(),s=this.doc.getSelection();if(i==0)return;this.adjustDeadkeys(i);let l=this.root.ownerDocument.createRange();if(n.node.nodeType==3){let c=n.node;c.insertData(n.offset,e),l.setStart(c,n.offset+e.length)}else{var r=this.doc.createTextNode(e);let c=this.doc.createRange();c.setStart(n.node,n.offset),c.collapse(!0),c.insertNode(r),l.setStart(r,e.length)}l.collapse(!0),s.removeAllRanges();try{s.addRange(l)}catch(c){n.node.parentElement.scrollIntoView(),s.addRange(l)}s.collapseToEnd()}handleNewlineAtCaret(){}setTextAfterCaret(e){if(!this.hasSelection())return;let n=this.getCarets().end;if(e._kmwLength()!=0)if(n.node.nodeType==3){let l=n.node;l.replaceData(n.offset,l.length,e)}else{var s=n.node.ownerDocument.createTextNode(e);let l=this.root.ownerDocument.createRange();l.setStart(n.node,n.offset),l.collapse(!0),l.insertNode(s)}}saveProperties(){var e=[new Be("backcolor",1),new Be("fontname",1),new Be("fontsize",1),new Be("forecolor",1),new Be("bold",0),new Be("italic",0),new Be("strikethrough",0),new Be("subscript",0),new Be("superscript",0),new Be("underline",0)];this.doc.defaultView&&e.push(new Be("hilitecolor",1));for(var n=0;n{super(...args)};if(e.isContentEditable)t(),this.root=e;else throw"Specified element is not already content-editable!"}get isSynthetic(){return!1}getElement(){return this.root}isSelectionEmpty(){return this.hasSelection()?this.root.ownerDocument.getSelection().isCollapsed:!0}hasSelection(){let e=this.root.ownerDocument.getSelection();return!(this.root!=e.anchorNode&&!this.root.contains(e.anchorNode)||this.root!=e.focusNode&&!this.root.contains(e.focusNode))}clearSelection(){if(this.hasSelection()){let e=this.root.ownerDocument.getSelection();e.isCollapsed||e.deleteFromDocument()}else console.warn("Attempted to clear an unowned Selection!")}invalidateSelection(){}getCarets(){let e=this.root.ownerDocument.getSelection(),n=e.anchorNode.compareDocumentPosition(e.focusNode);if(e.isCollapsed){let i=new Xi(e.anchorNode,e.anchorOffset);return new Si(i,i)}else{let i=new Xi(e.anchorNode,e.anchorOffset),s=new Xi(e.focusNode,e.focusOffset);return i.node==s.node&&(n=s.offset-i.offset>0?2:4),n&2?new Si(i,s):new Si(s,i)}}getDeadkeyCaret(){return this.getTextBeforeCaret().kmwLength()}getTextBeforeCaret(){if(!this.hasSelection())return this.getText();let e=this.getCarets().start;return e.node.nodeType!=3?"":e.node.textContent.substr(0,e.offset)}getSelectedText(){return""}getTextAfterCaret(){if(!this.hasSelection())return"";let e=this.getCarets().end;return e.node.nodeType!=3?"":e.node.textContent.substr(e.offset)}getText(){return this.root.innerText}deleteCharsBeforeCaret(e){if(!this.hasSelection()||e<=0)return;let n=this.getCarets().start;if(e>n.offset&&(e=n.offset),n.node.nodeType!=3){console.warn("Deletion of characters requested without available context!");return}let i=this.root.ownerDocument.createRange(),s=n.offset-n.node.nodeValue.substr(0,n.offset)._kmwSubstr(-e).length;i.setStart(n.node,s),i.setEnd(n.node,n.offset),this.adjustDeadkeys(-e),i.deleteContents()}insertTextBeforeCaret(e){if(!this.hasSelection())return;let n=this.getCarets().start,i=e._kmwLength(),s=this.root.ownerDocument.getSelection();if(i==0)return;this.adjustDeadkeys(i);let l=this.root.ownerDocument.createRange();if(n.node.nodeType==3){let c=n.node;c.insertData(n.offset,e),l.setStart(c,n.offset+e.length)}else{var r=n.node.ownerDocument.createTextNode(e);let c=this.root.ownerDocument.createRange();c.setStart(n.node,n.offset),c.collapse(!0),c.insertNode(r),l.setStart(r,e.length)}l.collapse(!0),s.removeAllRanges();try{s.addRange(l)}catch(c){n.node.parentElement.scrollIntoView(),s.addRange(l)}s.collapseToEnd()}handleNewlineAtCaret(){}setTextAfterCaret(e){if(!this.hasSelection())return;let n=this.getCarets().end;if(e._kmwLength()!=0)if(n.node.nodeType==3){let l=n.node;l.replaceData(n.offset,l.length,e)}else{var s=n.node.ownerDocument.createTextNode(e);let l=this.root.ownerDocument.createRange();l.setStart(n.node,n.offset),l.collapse(!0),l.insertNode(s)}}doInputEvent(){this.dispatchInputEventOn(this.root)}};o(kc,"ContentEditable");var Ht=kc;function j(a,t){var e;return a?a.Window?t=="Window":(a.defaultView?e=a.defaultView[t]:a.ownerDocument&&(e=a.ownerDocument.defaultView[t]),e?a instanceof e:!1):!1}o(j,"nestedInstanceOf");function ks(a){if(j(a,"HTMLInputElement"))return new Pe(a);if(j(a,"HTMLTextAreaElement"))return new Gn(a);if(j(a,"HTMLIFrameElement")){let t=a;return t.contentWindow&&t.contentWindow.document&&t.contentWindow.document.designMode=="on"?new P(t):a.isContentEditable?new Ht(a):null}else if(a.isContentEditable)return new Ht(a);return null}o(ks,"wrapElement");var Nc=class Nc{constructor(){this.pending=!1;let t=this.bg=document.createElement("div"),e=document.createElement("div"),n=this.lt=document.createElement("div"),i=this.gr=document.createElement("div"),s=this.bx=document.createElement("div");t.className="kmw-wait-background",e.className="kmw-wait-box",this.dismiss=null,n.className="kmw-wait-text",i.className="kmw-wait-graphic",s.className="kmw-alert-close";let l=e.onmousedown=e.onclick=c=>{s.style.display=="block"&&(t.style.display="none",this.dismiss&&this.dismiss())};e.addEventListener("touchstart",l,!1);let r=t.onmousedown=t.onclick=c=>{c.preventDefault(),c.stopPropagation()};t.addEventListener("touchstart",r,!1),e.appendChild(s),e.appendChild(n),e.appendChild(i),t.appendChild(e),document.body.appendChild(t)}get rootElement(){return this.bg}wait(t){let e=this.bg;typeof e=="undefined"||e==null||(t?(this.pending=!0,window.setTimeout(()=>{this.pending&&(window.scrollTo(0,0),this.bx.style.display="none",this.lt.className="kmw-wait-text",this.lt.innerHTML=t,this.gr.style.display="block",e.style.display="block")},1e3)):this.pending&&(this.lt.innerHTML="",this.pending=!1,e.style.display="none"))}alert(t,e){let n=this.bg;this.bx.style.display="block",this.lt.className="kmw-alert-text",this.lt.innerHTML=t,this.gr.style.display="none",n.style.display="block",this.dismiss=arguments.length>1?e:null}shutdown(){this.bg.parentNode.removeChild(this.bg)}};o(Nc,"AlertHost");var It=Nc;function Vi(){return document.readyState==="complete"?Promise.resolve():new Promise((a,t)=>{let e=o(()=>{window.removeEventListener("load",e),a()},"loadHandler");window.addEventListener("load",e)})}o(Vi,"whenDocumentReady");var Ec=class Ec extends Jn{initialize(e){this._options?this._options=y(y({},this._options),e):this._options=y({},e),super.initialize(e),this._options=e,this._ui=e.ui,this._attachType=e.attachType,Vi().then(()=>{var n;e.useAlerts&&!this.alertHost?this._alertHost=new It:!e.useAlerts&&this.alertHost&&((n=this._alertHost)==null||n.shutdown(),this._alertHost=null)})}get options(){return this._options}get attachType(){return this._attachType}get alertHost(){return this._alertHost}set signalUser(e){(!e||e!=this.alertHost)&&this.alertHost.shutdown(),this._alertHost=e}debugReport(){let e=super.debugReport();return e.attachType=this.attachType,e.ui=this._ui,e.keymanEngine="app/browser",e}onRuleFinalization(e,n){let i=e.transcription.transform;xt(i)||n instanceof O&&(n.changed=!0)}};o(Ec,"BrowserConfiguration");var Ns=Ec,ea=y({ui:"",attachType:"",useAlerts:!0},Zl);var Yc=class Yc{constructor(t,e,n){this.interface=t,this.keyboard=e}};o(Yc,"AttachmentInfo");var Ai=Yc;function je(a){let t=a==null?void 0:a.target;return Ve(t)}o(je,"eventOutputTarget");function Ve(a){var t;if(a==null)return null;if(a.body&&(a=a.body),a.nodeType==3&&(a=a.parentNode),j(a,"HTMLInputElement")){let e=a.type.toLowerCase();if(!(e=="text"||e=="search"))return null}return(t=a._kmwAttachment)==null?void 0:t.interface}o(Ve,"outputTargetForElement");var Es=class Es extends S.default{constructor(e,n){if(!e)throw new Error("Cannot attach to a null/undefined document");super();this.baseFont="";this.appliedFont="";this.embeddedPageContexts=[];this._inputList=[];this._sortedInputs=[];this._InputModeObserverCore=o(e=>{this.disableInputModeObserver();try{for(let n of e){let i=n.target;this.isAttached(i)&&(i._kmwAttachment.inputMode=i.inputMode,this.device.touchable&&(i.inputMode="none"))}}finally{this.enableInputModeObserver()}},"_InputModeObserverCore");this._EnablementMutationObserverCore=o(e=>{for(var n=0;n=0:!1,l=i.target.className.indexOf("kmw-disabled")>=0;if(s&&!l?this._EnableControl(i.target):!s&&l&&this._DisableControl(i.target),!l&&i.attributeName=="readonly"){var r=i.oldValue?i.oldValue!=null:!1,c=i.target;if(c instanceof c.ownerDocument.defaultView.HTMLInputElement||c instanceof c.ownerDocument.defaultView.HTMLTextAreaElement){var g=c.readOnly;r&&!g?this._EnableControl(i.target):!r&&g&&this._DisableControl(i.target)}}}},"_EnablementMutationObserverCore");this._AutoAttachObserverCore=o(e=>{for(var n=[],i=[],s=0;s{this.listInputs()},1):this.listInputs())},"_AutoAttachObserverCore");this._MutationAdditionObserved=o(e=>{if(e instanceof e.ownerDocument.defaultView.HTMLIFrameElement){let n=o(()=>{window.setTimeout(()=>{this.attachToControl(e)},1)},"attachFunctor");e.addEventListener("load",n)}else this.attachToControl(e)},"_MutationAdditionObserved");this._MutationRemovalObserved=o(e=>{this.detachFromControl(e)},"_MutationRemovalObserved");this.options=n,this.document=e,this.stylesheetManager=new ge(this.document.body)}get device(){return this.options.hostDevice}get window(){return this.document.defaultView}get inputList(){let e=this.embeddedPageContexts.map(n=>n.inputList).reduce((n,i)=>n.concat(i),[]);return[].concat(this._inputList).concat(e)}get sortedInputs(){return this._sortedInputs}install(e){this.manualAttach=e,this.baseFont=this.getBaseFont(),this.manualAttach||(this._SetupDocument(this.document.documentElement),this.listInputs()),this.options.owner||this.initMutationObservers(this.document,e)}setupElementAttachment(e){if(!e._kmwAttachment){let n=ks(e);n||j(e,"HTMLIFrameElement")||console.warn("Could not create processing interface for newly-attached element!"),e._kmwAttachment=new Ai(n,null,this.device.touchable)}}clearElementAttachment(e){e._kmwAttachment=null}isKMWInput(e){if(e instanceof e.ownerDocument.defaultView.HTMLTextAreaElement)return!0;if(e instanceof e.ownerDocument.defaultView.HTMLInputElement){if(Pe.isSupportedType(e.type))return!0}else if(e instanceof e.ownerDocument.defaultView.HTMLIFrameElement)try{if(e.contentWindow){let n=e.contentWindow.document;if(n)return!(this.device.touchable&&n.designMode.toLowerCase()=="on")}else return!!e._kmwAttachment}catch(n){console.warn("Error during attachment to / detachment from iframe: "),console.warn(n)}else if(e.isContentEditable)return!0;return!1}isAttached(e){if(e._kmwAttachment)return!0;if(j(e,"HTMLIFrameElement")){if(e.contentDocument==this.document)return!0;for(let i of this.embeddedPageContexts)if(i.isAttached(e))return!0}return!1}isKMWDisabled(e){let n=e.className;return e.readOnly?!0:!!(n&&n.indexOf("kmw-disabled")>=0)}enableInputElement(e){var n;this.isKMWDisabled(e)||(e instanceof e.ownerDocument.defaultView.HTMLIFrameElement?this._AttachToIframe(e):(this.setupElementAttachment(e),e._kmwAttachment.inputMode=(n=e.inputMode)!=null?n:"text",this.disableInputModeObserver(),e.inputMode="none",this.enableInputModeObserver(),e.classList.add("keymanweb-font"),this._inputList.push(e),this.emit("enabled",e)))}disableInputElement(e){var i;if(e)if(e.ownerDocument.defaultView&&e instanceof e.ownerDocument.defaultView.HTMLIFrameElement||e instanceof HTMLIFrameElement)this._DetachFromIframe(e);else{if(this.isAttached(e)){let l=(i=e._kmwAttachment)==null?void 0:i.inputMode;this.disableInputModeObserver(),e.inputMode=l,this.enableInputModeObserver()}e.className.indexOf("keymanweb-font")>=0&&(e.className=e.className.replace("keymanweb-font","").trim());var n=this.inputList.indexOf(e);n>-1&&this._inputList.splice(n,1),this.emit("disabled",e)}}enableTouchElement(e){return this.isKMWDisabled(e)?(this.emit("disabled",e),!1):(this.isAttached(e)||this.setupElementAttachment(e),this.enableInputElement(e),!0)}disableTouchElement(e){if(this.isAttached(e)){let n=e._kmwAttachment.inputMode;this.disableInputModeObserver(),e.inputMode=n,this.enableInputModeObserver()}}_AttachToIframe(e){try{let n=e.contentWindow.document;if(n){if(n.designMode.toLowerCase()=="on")this.setupElementAttachment(e),n.body._kmwAttachment=e._kmwAttachment,this._inputList.push(e),this.emit("enabled",e);else if(this.embeddedPageContexts.filter(i=>i.document==n).length==0){let i=new Es(n,A(y({},this.options),{owner:e}));this.embeddedPageContexts.push(i),i.on("enabled",s=>this.emit("enabled",s)),i.on("disabled",s=>this.emit("disabled",s)),i.install(this.manualAttach)}}}catch(n){}}_DetachFromIframe(e){let n=o(()=>{this.clearElementAttachment(e);let i=this._inputList.indexOf(e);i!=-1&&this._inputList.splice(i,1),this.emit("disabled",e)},"detachFromDesignIframe");try{let i=e.contentWindow.document;if(i){if(i.designMode.toLowerCase()=="on")i.body._kmwAttachment=null,n();else for(let s=0;s=0&&(e.className=n.replace("kmw-disabled","").trim())}listInputs(){let e=[],n=document.getElementsByTagName("input"),i=document.getElementsByTagName("textarea");for(let l=0;ll.y!=r.y?l.y-r.y:l.x-r.x);let s=[];for(let l=0;l=s.length?i-s.length:i,i=i<0?i+s.length:i,s[i]}_GetDocumentEditables(e){let n=[];if(e.ownerDocument&&e instanceof e.ownerDocument.defaultView.HTMLElement){let s=e.ownerDocument.defaultView;(e instanceof s.HTMLInputElement||e instanceof s.HTMLTextAreaElement||e instanceof s.HTMLIFrameElement)&&n.push(e)}if(e.getElementsByTagName){var i=o(function(s){return es(e.getElementsByTagName(s))},"LiTmp");n=n.concat(i("INPUT"),i("TEXTAREA"),i("IFRAME"))}return e.querySelectorAll&&(n=n.concat(es(e.querySelectorAll("[contenteditable]")))),e.ownerDocument&&e instanceof e.ownerDocument.defaultView.HTMLElement&&e.isContentEditable&&n.push(e),n}_SetupDocument(e){let n=this._GetDocumentEditables(e);for(var i=0;i0&&n.length==0)i=1;else if(e.length==0&&n.length>0)i=2;else{var r=e[0],c=n[0];r.offsetTopc.offsetTop?i=2:r.offsetLeftc.offsetLeft&&(i=2)}switch(i){case 0:s=l;break;case 1:s=getComputedStyle(e[0]).fontFamily||"";break;case 2:s=getComputedStyle(n[0]).fontFamily||"";break}return(typeof s=="undefined"||s=="monospace")&&(s=l),s}buildAttachmentFontStyle(e){let n=e,i=this.baseFont;n&&typeof n.family!="undefined"&&(i=n.family),i=i.replace(/\u0022/g,"");var s=new RegExp("\\s?"+i+",?"),l=this.appliedFont.replace(/\u0022/g,"");l=l.replace(s,""),l=l.replace(/,$/,""),l==""?l=i:l=i+","+l,l='"'+l.replace(/\,\s?/g,'","')+'"';let r=`.keymanweb-font{ +font-family:`+l+` !important; +} +`;return this.appliedFont=l,r}setAttachmentFont(e,n,i){this.stylesheetManager.unlinkAll(),this.stylesheetManager.addStyleSheetForFont(e,n,i),this.stylesheetManager.linkStylesheet(st(this.buildAttachmentFontStyle(e)))}shutdown(){var e,n,i,s;try{(e=this.enablementObserver)==null||e.disconnect(),(n=this.attachmentObserver)==null||n.disconnect(),(i=this.inputModeObserver)==null||i.disconnect(),(s=this.stylesheetManager)==null||s.unlinkAll(),this.inputModeObserver=null,this.embeddedPageContexts.forEach(l=>{try{l.shutdown()}catch(r){}});for(let l of this.inputList)try{this.detachFromControl(l)}catch(r){this.emit("disabled",l)}this._inputList=[]}catch(l){console.error("Error occurred during shutdown"),console.error(l)}}};o(Es,"PageContextAttachment");var Wi=Es;var wc=class wc{constructor(t,e){this.activationPending=t,this.activated=e}};o(wc,"FocusStateAPIObject");var Tc=wc,Mc=class Mc extends S.default{constructor(e){super();this._maintainingFocus=!1;this.restoringFocus=!1;this._IgnoreNextSelChange=0;this.isTargetForcingScroll=e}get maintainingFocus(){return this._maintainingFocus}set maintainingFocus(e){let n=this._maintainingFocus;this._maintainingFocus=e,n&&!e&&this.emit("maintainingfocusend")}getUIState(){return new Tc(this.maintainingFocus,this.restoringFocus)}setMaintainingFocus(e){this.maintainingFocus=!!e}setFocusTimer(){this.focusing=!0,this.focusTimer=window.setTimeout(()=>{this.focusing=!1},50)}};o(Mc,"FocusAssistant");var Ys=Mc;function ta(a,t){let e=t!=null&&t.isRTL?"rtl":"ltr";a&&(a instanceof a.ownerDocument.defaultView.HTMLInputElement||a instanceof a.ownerDocument.defaultView.HTMLTextAreaElement?a.value.length==0&&(a.dir=e):typeof a.textContent=="string"&&a.textContent.length==0&&(a.dir=e))}o(ta,"_SetTargDir");var Ts=class Ts extends Nn{constructor(e,n){super(e);this.cookieManager=new le("KeymanWeb_Keyboard");this.focusAssistant=new Ys(()=>{var e;return(e=this.activeTarget)==null?void 0:e.isForcingScroll()});this.domEventTracker=new xe;this._ControlFocus=o(e=>{let n=je(e);return n&&this.setActiveTarget(n,!0),!0},"_ControlFocus");this._ControlBlur=o(e=>{if(this.focusAssistant._IgnoreNextSelChange)return this.focusAssistant._IgnoreNextSelChange--,e.cancelBubble=!0,e.stopPropagation(),!0;if(this.focusAssistant.isTargetForcingScroll())return e.cancelBubble=!0,e.stopPropagation(),!0;let n=je(e);if(n==null)return!0;this.lastActiveTarget&&this._BlurKeyboardSettings(this.lastActiveTarget.getElement());let i=this.activeTarget;this.currentTarget=null,(i||this.lastActiveTarget)&&(this.mostRecentTarget=n),this.focusAssistant.restoringFocus=!1;let s=this.activeKeyboard,l=this.focusAssistant.maintainingFocus;return!l&&s&&s.keyboard.notify(0,n,0),i&&!this.activeTarget&&this.emit("targetchange",null),this.apiEvents.callEvent("controlblurred",{target:n.getElement(),event:e,isActivating:l}),this.doChangeEvent(n),this.resetContext(),!0},"_ControlBlur");this._Click=o(e=>(this.resetContext(),!0),"_Click");this.nonKMWTouchHandler=o(e=>{this.focusAssistant.focusing=!1,clearTimeout(this.focusAssistant.focusTimer),this.forgetActiveTarget()},"nonKMWTouchHandler");this._eventsObj=n,this.page=new Wi(window.document,{hostDevice:this.engineConfig.hostDevice}),this.focusAssistant.on("maintainingfocusend",()=>{!this.activeTarget&&this.mostRecentTarget&&this.emit("targetchange",this.activeTarget)})}get apiEvents(){return this._eventsObj()}initialize(){this.on("keyboardasyncload",(e,n)=>{var i;(i=this.engineConfig.alertHost)==null||i.wait("Installing keyboard
"+e.name),n.then(()=>{var s;(s=this.engineConfig.alertHost)==null||s.wait()})}),this.engineConfig.deferForInitialization.then(()=>{let e=this.engineConfig.hostDevice,n=o(i=>i.stopPropagation(),"noPropagation");this.page.on("enabled",i=>{if(!(i._kmwAttachment.interface instanceof P))e.touchable&&(this.domEventTracker.detachDOMEvent(i,"touchstart",this.nonKMWTouchHandler),this.domEventTracker.attachDOMEvent(i,"touchmove",n,!1),this.domEventTracker.attachDOMEvent(i,"touchend",n,!1)),this.domEventTracker.attachDOMEvent(i,"focus",this._ControlFocus),this.domEventTracker.attachDOMEvent(i,"blur",this._ControlBlur),this.domEventTracker.attachDOMEvent(i,"click",this._Click);else{var s=i.contentWindow.document;e.browser=="firefox"?(this.domEventTracker.attachDOMEvent(s,"focus",this._ControlFocus),this.domEventTracker.attachDOMEvent(s,"blur",this._ControlBlur)):(this.domEventTracker.attachDOMEvent(s.body,"focus",this._ControlFocus),this.domEventTracker.attachDOMEvent(s.body,"blur",this._ControlBlur))}i.ownerDocument.activeElement==i&&this.setActiveTarget(Ve(i),!0)}),this.page.on("disabled",i=>{var l;if(!j(i,"HTMLIFrameElement"))e.touchable&&this.domEventTracker.attachDOMEvent(i,"touchstart",this.nonKMWTouchHandler,!1),this.domEventTracker.detachDOMEvent(i,"focus",this._ControlFocus),this.domEventTracker.detachDOMEvent(i,"blur",this._ControlBlur),this.domEventTracker.detachDOMEvent(i,"click",this._Click);else{let r=i.contentWindow.document;e.browser=="firefox"?(this.domEventTracker.detachDOMEvent(r,"focus",this._ControlFocus),this.domEventTracker.detachDOMEvent(r,"blur",this._ControlBlur)):(this.domEventTracker.detachDOMEvent(r.body,"focus",this._ControlFocus),this.domEventTracker.detachDOMEvent(r.body,"blur",this._ControlBlur))}var s=(l=this.mostRecentTarget)==null?void 0:l.getElement();s&&s==i&&this.forgetActiveTarget()}),this.page.install(this.engineConfig.attachType=="manual")})}get activeTarget(){let e=this.focusAssistant.maintainingFocus;return this.currentTarget||(e?this.mostRecentTarget:null)}get lastActiveTarget(){return this.mostRecentTarget}deactivateCurrentTarget(){let e=this.activeTarget||this.lastActiveTarget;e&&this.page.isAttached(e.getElement())&&this._BlurKeyboardSettings(e.getElement()),this.activeTarget||this.setActiveTarget(null,!0)}forgetActiveTarget(){this.focusAssistant.maintainingFocus=!1,this.focusAssistant.restoringFocus=!1;let e=this.activeTarget||this.mostRecentTarget;e&&this._BlurKeyboardSettings(e.getElement()),this.setActiveTarget(null,!0),e==this.lastActiveTarget&&(this.mostRecentTarget=null)}setActiveTarget(e,n){var c;let i=this.mostRecentTarget,s=this.activeTarget;if(e==s){s&&(this.currentTarget=s);return}let l=!!i;if(this.currentTarget=this.mostRecentTarget=e,this.predictionContext.setCurrentTarget(e),this.focusAssistant.restoringFocus?this._BlurKeyboardSettings(e.getElement()):e&&this._FocusKeyboardSettings(e.getElement(),!l),this._CommonFocusHelper(e))return;let r=e==null?void 0:e.getElement();if(e instanceof P&&(r=e.docRoot),r&&r.ownerDocument&&r instanceof r.ownerDocument.defaultView.HTMLElement&&ta(r,(c=this.activeKeyboard)==null?void 0:c.keyboard),e!=s&&this.emit("targetchange",e),n){let g=i==null?void 0:i.getElement();i instanceof P&&(g=i.docRoot),r?this.apiEvents.callEvent("controlfocused",{target:r,activeControl:g}):g&&this.apiEvents.callEvent("controlblurred",{target:g,event:null,isActivating:this.focusAssistant.maintainingFocus})}}get activeKeyboard(){return this._activeKeyboard}restoreLastActiveTarget(){this.mostRecentTarget&&(this.focusAssistant.restoringFocus=!0,this.mostRecentTarget.focus(),this.focusAssistant.restoringFocus=!1)}insertText(e,n,i){this.restoreLastActiveTarget();let s=this.activeTarget;return s==null&&this.mostRecentTarget&&(s=this.activeTarget),s!=null?super.insertText(e,n,i):!1}currentKeyboardSrcTarget(){let e=this.currentTarget||this.mostRecentTarget;return this.isTargetKeyboardIndependent(e)?e:null}isTargetKeyboardIndependent(e){let n=e==null?void 0:e.getElement()._kmwAttachment;return!!(n!=null&&n.keyboard||(n==null?void 0:n.keyboard)==="")}activateKeyboardForTarget(e,n){var s,l;let i=n==null?void 0:n.getElement()._kmwAttachment;if(i?(i.keyboard=(s=e==null?void 0:e.metadata.id)!=null?s:"",i.languageCode=(l=e==null?void 0:e.metadata.langId)!=null?l:""):this.globalKeyboard=e,this.currentKeyboardSrcTarget()==n){this._activeKeyboard=e;let r=e==null?void 0:e.metadata;this.page.setAttachmentFont(r==null?void 0:r.KFont,this.engineConfig.paths.fonts,this.engineConfig.hostDevice.OS)}}setKeyboardForTarget(e,n,i){if(e instanceof P){console.warn("'keymanweb.setKeyboardForControl' cannot set keyboard on iframes.");return}let s=e.getElement()._kmwAttachment,l=this.currentKeyboardSrcTarget()==e;if(s){if(s.keyboard=n||null,s.languageCode=i||null,l||this.currentKeyboardSrcTarget()==e){let r=this.globalKeyboard.metadata;this.activateKeyboard(s.keyboard||r.id,s.languageCode||r.langId,!0)}}else return}getKeyboardStubForTarget(e){if(this.isTargetKeyboardIndependent(e)){let n=e.getElement()._kmwAttachment;return this.keyboardCache.getStub(n.keyboard,n.languageCode)}else return this.globalKeyboard.metadata}getFallbackStubKey(){let e={id:"",langId:""};return this.engineConfig.hostDevice.touchable&&this.keyboardCache.defaultStub||e}activateKeyboard(e,n,i){return W(this,null,function*(){var l,r,c,g,d,u;i||(i=!1);let s=this.currentKeyboardSrcTarget();this.engineConfig.deferForInitialization.isFulfilled||(yield this.engineConfig.deferForInitialization.corePromise),e||(e=this.getFallbackStubKey().id,n=this.getFallbackStubKey().langId);try{let I=yield Ji(Ts.prototype,this,"activateKeyboard").call(this,e,n,i);return(l=this.engineConfig.alertHost)==null||l.wait(),i&&!s&&this.cookieManager.save({current:`${e}:${n}`}),s==this.currentKeyboardSrcTarget()&&(ta((r=this.currentTarget)==null?void 0:r.getElement(),this.keyboardCache.getKeyboard(e)),this.page.setAttachmentFont((g=(c=this.activeKeyboard)==null?void 0:c.metadata)==null?void 0:g.KFont,this.engineConfig.paths.fonts,this.engineConfig.hostDevice.OS),this.restoreLastActiveTarget()),I}catch(I){let B=o(()=>W(this,null,function*(){let b=this.getFallbackStubKey();b.id!=e&&(yield this.activateKeyboard(b.id,b.langId,!0).catch(()=>{}))}),"fallback");(d=this.engineConfig.alertHost)==null||d.wait();let h=(I==null?void 0:I.message)||"Sorry, the "+e+" keyboard for "+n+" is not currently available.";throw I instanceof pt?console.error(I||h):console.warn(I||h),this.engineConfig.alertHost?(u=this.engineConfig.alertHost)==null||u.alert(h,B):yield B(),I}})}_BlurKeyboardSettings(e,n,i){var r;var s=this.activeKeyboard?this.activeKeyboard.keyboard.id:"",l=(r=this.activeKeyboard)==null?void 0:r.metadata.langId;n!==void 0&&i!==void 0&&(s=n,l=i),e&&e._kmwAttachment.keyboard!=null?(e._kmwAttachment.keyboard=s,e._kmwAttachment.languageCode=l):this.globalKeyboard=this.activeKeyboard}_FocusKeyboardSettings(e,n){var l;let i=e._kmwAttachment,s=this.globalKeyboard;i.keyboard!=null?this.activateKeyboard(i.keyboard,i.languageCode,!0):!n&&(s==null?void 0:s.metadata)!=((l=this._activeKeyboard)==null?void 0:l.metadata)&&this.activateKeyboard(s==null?void 0:s.metadata.id,s==null?void 0:s.metadata.langId,!0)}_CommonFocusHelper(e){var s;let n=this.focusAssistant,i=(s=this.activeKeyboard)==null?void 0:s.keyboard;return n.restoringFocus||(e==null||e.deadkeys().clear(),i==null||i.notify(0,e,1)),!n.restoringFocus&&this.mostRecentTarget!=e&&(n.maintainingFocus=!1),n.restoringFocus=!1,this.resetContext(),!1}doChangeEvent(e){if(e.changed){let n=new Event("change",{bubbles:!0,cancelable:!1});e.getElement().dispatchEvent(n)}e.changed=!1}getSavedKeyboardRaw(){var n=new le("KeymanWeb_Keyboard").load(decodeURIComponent);return typeof n.current!="string"||n.current=="Keyboard_us:eng"?"Keyboard_us:en":n.current}getSavedKeyboard(){let e=this.getSavedKeyboardRaw(),n=this.keyboardCache.getStubList(),i;for(let s=0;s0?n[0].KI+":"+n[0].KLC:"Keyboard_us:en"}restoreSavedKeyboard(e){let i=e.split(":");i.length<2&&(i[1]=""),(this.keyboardCache.getStub(i[0],i[1])||this.keyboardCache.defaultStub)&&this.activateKeyboard(i[0],i[1])}shutdown(){this.page.shutdown(),this.domEventTracker.shutdown()}};o(Ts,"ContextManager");var fi=Ts;var Kc=class Kc extends Ae{constructor(e){super();this.contextManager=e}isCommand(e){switch(this.codeForEvent(e)){case C.keyCodes.K_TAB:case C.keyCodes.K_TABBACK:case C.keyCodes.K_TABFWD:return!0;default:return super.isCommand(e)}}applyCommand(e,n){let i=this.codeForEvent(e),s=o(l=>{var d;let r=this.contextManager,c=(d=r.activeTarget)==null?void 0:d.getElement(),g=r.page.findNeighboringInput(c,l);g==null||g.focus()},"moveToNext");switch(i){case C.keyCodes.K_TAB:s((e.Lmodifiers&m.K_SHIFTFLAG)!=0);break;case C.keyCodes.K_TABBACK:s(!0);break;case C.keyCodes.K_TABFWD:s(!1);break}super.applyCommand(e,n)}};o(Kc,"DefaultBrowserRules");var Li=Kc;function zc(a){return a.keyCode?a.keyCode:a.which?a.which:null}o(zc,"_GetEventKeyCode");function ws(a,t,e){if(a.cancelBubble===!0)return null;let n=zc(a);if(n==null)return null;var i=t.modStateFlags,s=0,l=!1,r=!1;let c=C.keyCodes;switch(n){case c.K_CTRL:case c.K_LCTRL:case c.K_RCTRL:case c.K_CONTROL:case c.K_LCONTROL:case c.K_RCONTROL:l=!0;break;case c.K_LMENU:case c.K_RMENU:case c.K_ALT:case c.K_LALT:case c.K_RALT:r=!0;break}s|=a.getModifierState("Shift")?16:0,a.getModifierState("Control")&&(s|=a.location!=0&&l?a.location==1?m.LCTRLFLAG:m.RCTRLFLAG:i&3),a.getModifierState("Alt")&&(s|=a.location!=0&&r?a.location==1?m.LALTFLAG:m.RALTFLAG:i&12);let g=0;g|=a.getModifierState("CapsLock")?m.CAPITALFLAG:m.NOTCAPITALFLAG,g|=a.getModifierState("NumLock")?m.NUMLOCKFLAG:m.NOTNUMLOCKFLAG,g|=a.getModifierState("ScrollLock")?m.SCROLLFLAG:m.NOTSCROLLFLAG,s|=g;let d=t.modStateFlags!=s;t.modStateFlags=s;let u=m.RALTFLAG|m.LCTRLFLAG;(i&u)==u&&(s&u)!=u&&(s&=~u),s&m.RALTFLAG&&(s&=~m.LCTRLFLAG);let I=C.modifierBitmasks,B=t.activeKeyboard,h;B&&B.isChiral?(h=s&I.CHIRAL,B.emulatesAltGr&&(h&I.ALT_GR_SIM)==I.ALT_GR_SIM&&(h^=I.ALT_GR_SIM,h|=m.RALTFLAG)):h=s&16|(s&(m.LCTRLFLAG|m.RCTRLFLAG)?32:0)|(s&(m.LALTFLAG|m.RALTFLAG)?64:0),h|=a.metaKey?m.K_METAFLAG:0,e.browser==V.Browser.Firefox&&te.browserMap.FF["k"+n]&&(n=te.browserMap.FF["k"+n]);let b=new ee({device:e,kName:"",Lcode:n,Lmodifiers:h,Lstates:g,LmodifierChange:d,isSynthetic:!1}),F=typeof a.charCode!="undefined"&&a.charCode!=null&&(a.charCode==0||(h&111)!=0);b.LisVirtualKey=F||a.type!="keypress",b=Sl(b,B,t.baseLayout);let G=new ee(b);return G.source=a,G}o(ws,"preprocessKeyboardEvent");var Dc=class Dc extends Dt{constructor(e,n,i){super();this.domEventTracker=new xe;this.swallowKeypress=!1;this._KeyDown=o(e=>{var l;let n=this.contextManager.activeKeyboard,i=je(e);if(!i||n==null)return!0;let s=i.getElement();return((l=s==null?void 0:s.getAttribute("class"))==null?void 0:l.indexOf("kmw-disabled"))>=0?!0:this.keyDown(e)},"_KeyDown");this._KeyPress=o(e=>{var i;return!je(e)||((i=this.contextManager.activeKeyboard)==null?void 0:i.keyboard)==null?!0:this.keyPress(e)},"_KeyPress");this._KeyUp=o(e=>{let n=je(e);var i=ws(e,this.processor,this.hardDevice);if(i==null||n==null)return!0;var s=n.getElement();if(i.Lcode==13){var l=!1;if(j(s,"HTMLTextAreaElement")&&(l=!0),!l)return s instanceof s.ownerDocument.defaultView.HTMLInputElement&&(s.form&&(s.type=="search"||s.type=="submit")?s.form.submit():this.contextManager.page.findNeighboringInput(s,!1).focus()),!0}return this.keyUp(e)},"_KeyUp");this.hardDevice=e,this.contextManager=i,this.processor=n;let s=i.page,l=this.domEventTracker;s.on("enabled",r=>{let c=Ve(r);if(!(c instanceof P))l.attachDOMEvent(r,"keypress",this._KeyPress),l.attachDOMEvent(r,"keydown",this._KeyDown),l.attachDOMEvent(r,"keyup",this._KeyUp);else{let g=c.getElement().contentDocument;l.attachDOMEvent(g.body,"keydown",this._KeyDown),l.attachDOMEvent(g.body,"keypress",this._KeyPress),l.attachDOMEvent(g.body,"keyup",this._KeyUp)}}),s.on("disabled",r=>{let c=Ve(r);if(!(c instanceof P))l.detachDOMEvent(r,"keypress",this._KeyPress),l.detachDOMEvent(r,"keydown",this._KeyDown),l.detachDOMEvent(r,"keyup",this._KeyUp);else{let g=c.getElement().contentDocument;l.detachDOMEvent(g.body,"keydown",this._KeyDown),l.detachDOMEvent(g.body,"keypress",this._KeyPress),l.detachDOMEvent(g.body,"keyup",this._KeyUp)}})}keyDown(e){this.swallowKeypress=!1;var n=ws(e,this.processor,this.hardDevice);if(n==null)return!0;let i={LeventMatched:!1};return this.emit("keyevent",n,(s,l)=>{i.LeventMatched=s&&!s.triggerKeyDefault,i.LeventMatched?(e&&e.preventDefault&&(e.preventDefault(),e.stopPropagation()),this.swallowKeypress=!!n.Lcode,n.Lcode==8&&(this.swallowKeypress=!1)):this.swallowKeypress=!1}),!i.LeventMatched}keyUp(e){var n=ws(e,this.processor,this.hardDevice);if(n==null)return!0;let i=je(e);return this.processor.doModifierPress(n,i,!1)}keyPress(e){var s;var n=ws(e,this.processor,this.hardDevice);if(n==null||n.LisVirtualKey)return!0;if(!((s=this.contextManager.activeKeyboard)!=null&&s.keyboard.isMnemonic))return!this.swallowKeypress||n.Lcode<32||this.hardDevice.browser==V.Browser.Safari&&n.Lcode>63232&&n.Lcode<63744;let i={};return this.swallowKeypress||this.emit("keyevent",n,(l,r)=>{i.preventDefaultKeystroke=!!l}),this.swallowKeypress||i.preventDefaultKeystroke?(this.swallowKeypress=!1,e&&e.preventDefault&&(e.preventDefault(),e.stopPropagation()),!1):(this.swallowKeypress=!1,!0)}shutdown(){this.domEventTracker.shutdown()}};o(Dc,"HardwareEventKeyboard");var Ri=Dc;var Oc=class Oc{constructor(){this.innerWidth=window.innerWidth,this.innerHeight=window.innerHeight}equals(t){return this.innerWidth==t.innerWidth&&this.innerHeight==t.innerHeight}};o(Oc,"RotationState");var Ms=Oc,bt=class bt{constructor(t){this.idlePermutationCounter=bt.IDLE_PERMUTATION_CAP;this.keyman=t}resolve(){var n;var t=this.keyman.osk;(n=this.keyman.touchLanguageMenu)==null||n.hide(),this.keyman.touchLanguageMenu=null,t.setNeedsLayout(),this.oskVisible&&t.present(),this.isActive=!1,this.updateTimer&&(window.clearInterval(this.updateTimer),this.rotState=null);let e=this.keyman.contextManager.activeTarget;e&&window.setTimeout(()=>{this.keyman.ensureElementVisibility(e.getElement())},0)}initNewRotation(){this.oskVisible=this.keyman.osk.isVisible(),this.keyman.osk.hideNow(),this.isActive=!0}init(){var t=this.keyman.config.hostDevice.OS,e=this.keyman.util;t=="ios"?(e.attachDOMEvent(window,"orientationchange",()=>(this.iOSEventHandler(),!1)),e.attachDOMEvent(window,"resize",()=>(this.iOSEventHandler(),!1))):t=="android"&&("onmozorientationchange"in screen?e.attachDOMEvent(screen,"mozorientationchange",()=>(this.initNewRotation(),!1)):e.attachDOMEvent(window,"orientationchange",()=>(this.initNewRotation(),!1)),e.attachDOMEvent(window,"resize",()=>(this.resolve(),!1)))}iOSEventHandler(){this.isActive||(this.initNewRotation(),this.rotState=new Ms,this.updateTimer=window.setInterval(this.iOSEventUpdate.bind(this),bt.UPDATE_INTERVAL)),this.idlePermutationCounter=0}iOSEventUpdate(){var t=new Ms;this.rotState.equals(t)?++this.idlePermutationCounter==bt.IDLE_PERMUTATION_CAP&&this.resolve():(this.rotState=t,this.idlePermutationCounter=0)}};o(bt,"RotationProcessor"),bt.IDLE_PERMUTATION_CAP=15,bt.UPDATE_INTERVAL=20;var Ks=bt;var Pc=class Pc{constructor(t,e){this.domEventTracker=new xe;this.suppressFocusCheck=o(t=>(this.focusAssistant.isTargetForcingScroll()&&(t.stopPropagation(),t.cancelBubble=!0),!0),"suppressFocusCheck");this.pageFocusHandler=o(()=>{var t;return!this.focusAssistant.maintainingFocus&&((t=this.engine.osk)!=null&&t.vkbd)&&(this.engine.contextManager.deactivateCurrentTarget(),this.engine.contextManager.resetContext()),!1},"pageFocusHandler");this.touchStartActivationHandler=o(t=>{var i,s;let e=this.engine.osk;if(!e)return!1;let n=this.engine.config.hostDevice;if(this.deactivateOnRelease=!0,this.touchY=t.touches[0].screenY,this.deactivateOnScroll=!1,n.OS=="android"&&n.browser=="chrome"){if(typeof e._Box=="undefined"||typeof e._Box.style=="undefined")return!1;let l=t.target.parentElement;if(typeof l!="undefined"&&l!=null&&(((i=l.getAttribute("class"))==null?void 0:i.indexOf("kmw-key-"))>=0||typeof l.parentElement!="undefined"&&l.parentElement!=null&&(l=l.parentElement,((s=l.getAttribute("class"))==null?void 0:s.indexOf("kmw-key-"))>=0)))return!1;this.deactivateOnScroll=!0}return!1},"touchStartActivationHandler");this.touchMoveActivationHandler=o(t=>{this.deactivateOnScroll&&(this.focusAssistant.focusing=!1,this.engine.contextManager.deactivateCurrentTarget());let e=t.touches[0].screenY,n=this.touchY;return(e-n>5||n-e<5)&&(this.deactivateOnRelease=!1),!1},"touchMoveActivationHandler");this.touchEndActivationHandler=o(t=>(this.deactivateOnRelease&&!this.engine.touchLanguageMenu&&!this.focusAssistant.focusing&&this.engine.contextManager.deactivateCurrentTarget(),this.deactivateOnRelease=!1,!1),"touchEndActivationHandler");this._WindowLoad=o(()=>{document.body.scrollTop=0,typeof document.documentElement!="undefined"&&(document.documentElement.scrollTop=0)},"_WindowLoad");this._WindowUnload=o(()=>{this.engine.shutdown()},"_WindowUnload");this.window=t,this.engine=e,this.attachHandlers(),e.config.hostDevice.touchable&&(this.buildPageTrailer(),this.rotationProcessor=new Ks(this.engine),this.rotationProcessor.init())}buildPageTrailer(){let t=this.mobilePageTrailer=document.createElement("div"),e=t.style;e.width="100%",e.height=screen.width/2+"px",document.body.appendChild(t)}get focusAssistant(){return this.engine.contextManager.focusAssistant}attachHandlers(){let t=this.domEventTracker,e=this.engine.config.hostDevice,n=this.window.document.body;t.attachDOMEvent(this.window,"focus",this.pageFocusHandler,!1),t.attachDOMEvent(this.window,"blur",this.pageFocusHandler,!1),t.attachDOMEvent(n,"focus",this.suppressFocusCheck,!0),t.attachDOMEvent(n,"blur",this.suppressFocusCheck,!0),e.touchable&&(t.attachDOMEvent(n,"touchstart",this.touchStartActivationHandler,!1),t.attachDOMEvent(n,"touchmove",this.touchMoveActivationHandler,!1),t.attachDOMEvent(n,"touchend",this.touchEndActivationHandler,!1)),t.attachDOMEvent(window,"load",this._WindowLoad,!1),t.attachDOMEvent(window,"unload",this._WindowUnload,!1),t.attachDOMEvent(document,"keyup",this.engine.hotkeyManager._Process,!1)}shutdown(){var i;let t=this.domEventTracker,e=this.engine.config.hostDevice,n=this.window.document.body;t.detachDOMEvent(this.window,"focus",this.pageFocusHandler,!1),t.detachDOMEvent(this.window,"blur",this.pageFocusHandler,!1),t.detachDOMEvent(n,"focus",this.suppressFocusCheck,!0),t.detachDOMEvent(n,"blur",this.suppressFocusCheck,!0),e.touchable&&(t.detachDOMEvent(n,"touchstart",this.touchStartActivationHandler,!1),t.detachDOMEvent(n,"touchmove",this.touchMoveActivationHandler,!1),t.detachDOMEvent(n,"touchend",this.touchEndActivationHandler,!1),(i=this.mobilePageTrailer)==null||i.parentElement.removeChild(this.mobilePageTrailer)),t.detachDOMEvent(window,"load",this._WindowLoad,!1),t.detachDOMEvent(window,"unload",this._WindowUnload,!1),t.detachDOMEvent(document,"keyup",this.engine.hotkeyManager._Process,!1)}};o(Pc,"PageIntegrationHandlers");var zs=Pc;function pe(a){let t=document.createElement(a);return t.style.userSelect="none",t.style.MozUserSelect="none",t.style.KhtmlUserSelect="none",t.style.UserSelect="none",t.style.WebkitUserSelect="none",t}o(pe,"_CreateElement");function Jt(a,t){try{if(a&&typeof window.getComputedStyle!="undefined")return window.getComputedStyle(a,"").getPropertyValue(t)}catch(e){}return""}o(Jt,"getStyleValue");var jc=class jc{constructor(t){this.keyman=t,this.scrolling=!1,this.shim=this.constructShim()}constructShim(){let t=this,e=pe("div"),n=this.keyman.osk;return e.id="kmw-language-menu-background",e.addEventListener("touchstart",i=>{if(i.preventDefault(),t.hide(),i.touches.length>2){var s=i.touches[1].pageX,l=i.touches[1].pageY;let r=n.vkbd.spaceBar;s>r.offsetLeft&&sr.offsetTop&&ll.scrollHeight-l.offsetHeight-1&&(l.scrollTop=l.scrollHeight-l.offsetHeight-1)},!1),this.activeLgNo=this.addLanguages(c,e),this.lgList.style.visibility="hidden",document.body.appendChild(this.lgList),t.OS=="android"&&"devicePixelRatio"in window&&(this.lgList.style.fontSize=2/window.devicePixelRatio+"em"),t.OS=="android"&&t.formFactor=="tablet"&&"devicePixelRatio"in window){var u=parseInt(Jt(i,"width"),10),I=i.style;isNaN(u)||(I.width=I.maxWidth=2*u/window.devicePixelRatio+"px"),u=parseInt(Jt(l,"width"),10),I=l.style,isNaN(u)||(I.width=I.maxWidth=2*u/window.devicePixelRatio+"px"),u=parseInt(Jt(c,"width"),10),I=c.style,isNaN(u)||(I.width=I.maxWidth=2*u/window.devicePixelRatio+"px")}this.adjust(0);var B=d.childNodes[1].offsetTop-d.childNodes[0].offsetTop,h=Math.floor(i.offsetHeight/26),b=Math.round(100*h/B)/100,F=b>.6?1:2;b>1.25&&(b=1.25);for(let Q=0;Q<26;Q++){var G=d.childNodes[Q].style;F==2&&Q%2==1?G.display="none":(G.fontSize=b*F+"em",G.lineHeight=h*F+"px")}var U=l.offsetWidth;l.scrollHeight>l.offsetHeight+3?U=U+d.offsetWidth:d.style.display="none",i.style.width=U+"px",this.lgList.style.visibility="",this.scrollToIndex(this.activeLgNo,l,c)}adjust(t){let e=this.keyman.osk,n=this.keyman.config.hostDevice;var i=this.lgList,s=i.firstChild,l=s.firstChild,r=0,c=i.style,g=i.childNodes[1],d=window.innerHeight-e.vkbd.lgKey.offsetHeight-16,u=l.childNodes.length+t-1,I=l.firstChild.firstChild.offsetHeight,B=u*I;n.OS=="ios"&&(n.formFactor=="phone"?(r=be()?36:0,d=(window.innerHeight-r-16)*Se(n.formFactor)):n.formFactor=="tablet"&&(r=be()?16:0,d=d-r)),c.left=K(e.vkbd.lgKey)+"px",B>d&&(B=d),c.height=B+"px",c.bottom="0px",g.style.height=s.style.height=c.height}scrollToLanguage(t,e,n){t.stopImmediatePropagation(),t.stopPropagation(),t.preventDefault();let i=t.touches[0].target;if(i.nodeName=="P"){var s,l,r=i.innerHTML.charCodeAt(0),c=n.childNodes;try{for(s=0;s=r));s++);}catch(g){}this.scrollToIndex(s,e,n)}}scrollToIndex(t,e,n){let i;try{i=n.firstChild.getBoundingClientRect().height*(t-.5)+1,e.scrollTop=i}catch(l){i=0}try{e.scrollTop<0&&(e.scrollTop=0),e.scrollTop>e.scrollHeight-e.offsetHeight-1&&(e.scrollTop=e.scrollHeight-e.offsetHeight-1)}catch(l){}}addLanguages(t,e){var d;var n=e.length;let i=this.keyman.config.hostDevice,s=[];for(let u=0;u1){B.className="kbd-list",B.innerHTML=s[u]+"...",B.scrolled=!1,B.ontouchend=b=>{b.stopPropagation(),B.scrolled?B.scrolled=!1:B.parentElement.className=B.parentElement.className=="kbd-list-closed"?"kbd-list-open":"kbd-list-closed",h.adjust(B.parentElement.className=="kbd-list-closed"?0:B.kList.length)},B.addEventListener("touchstart",function(b){b.stopPropagation()},!1),B.addEventListener("touchmove",function(b){B.scrolled=!0,b.stopPropagation()},!1);for(let b=0;b{if(this.originalBodyStyle)return console.error("Unexpected state: `originalBodyStyle` was not cleared by a previous `unlockBodyScroll()` call"),!1;this.originalBodyStyle={};let u=this.originalBodyStyle,I=document.body.style;return u.overflowY=I.overflowY,u.height=I.height,I.overflowY="hidden",I.height="100%",!0},"lockBodyScroll"),l=o(()=>{if(!this.originalBodyStyle){console.error("Unexpected state: `originalBodyStyle` is unset; cannot restore original body style");return}let u=this.originalBodyStyle,I=document.body.style;I.overflowY=u.overflowY,I.height=u.height,this.originalBodyStyle=null},"unlockBodyScroll"),r=o(function(u){u.stopPropagation(),this.className.indexOf("selected")<=0&&(this.className=this.className+" selected"),i.scrolling=!1,i.y0=u.touches[0].pageY,s()},"touchStart"),c=o(function(u){u.stopImmediatePropagation();var I=i.lgList.childNodes[0],B=I.scrollHeight-I.offsetHeight,h,b;if(typeof u.pageY!="undefined")h=u.pageY;else if(typeof u.touches!="undefined")h=u.touches[0].pageY;else return!1;if(b=h-i.y0,b<0)I.scrollTop>=B-1&&(i.y0=h);else if(b>0)I.scrollTop<2&&(i.y0=h);else return!1;return(b<-5||b>5)&&(i.scrolling=!0,this.className=this.className.replace(/\s*selected/,""),i.y0=h),!0},"touchMove"),g=o(function(u){let I=this;return typeof u.stopImmediatePropagation!="undefined"?u.stopImmediatePropagation():u.stopPropagation(),i.scrolling?this.className=this.className.replace(/\s*selected/,""):(i.keyman.contextManager.focusAssistant.setFocusTimer(),i.lgList.style.display="none",i.keyman.contextManager.activateKeyboard(I.kn,I.kc,!0),i.keyman.contextManager.restoreLastActiveTarget(),i.hide()),l(),!0},"touchEnd"),d=o(function(u){l()},"touchCancel");e.addEventListener("touchstart",r,!1),e.addEventListener("touchmove",c,!1),e.addEventListener("touchend",g,!1),e.addEventListener("touchcancel",d,!1)}hide(){let t=this.keyman.osk;this.lgList&&(t.vkbd.highlightKey(t.vkbd.lgKey,!1),this.lgList.style.visibility="hidden",window.setTimeout(()=>{this.shim.parentElement&&(document.body.removeChild(this.shim),document.body.removeChild(this.lgList))},500)),this.keyman.touchLanguageMenu=null}};o(jc,"LanguageMenu");var Ds=jc;function na(a,t,e){let n=e.focusAssistant;t.on("globekey",(i,s)=>{s&&t.hostDevice.touchable&&(a.touchLanguageMenu=new Ds(a),a.touchLanguageMenu.show()),t.vkbd&&t.vkbd.highlightKey(i,!1)}),t.on("hiderequested",i=>{t&&(t.startHide(!0),e.forgetActiveTarget())}),t.addEventListener("hide",i=>{var s;i!=null&&i.HiddenByUser&&((s=e.activeTarget)==null||s.focus())}),t.on("showbuild",()=>{var i;(i=a.config.alertHost)==null||i.alert("KeymanWeb Version "+Wn.VERSION+'

Copyright © 2007-2023 SIL International')}),t.on("dragmove",i=>W(this,null,function*(){n.restoringFocus=!0,yield i,e.restoreLastActiveTarget(),n.restoringFocus=!1,n.setMaintainingFocus(!1)})),t.on("resizemove",i=>W(this,null,function*(){n.restoringFocus=!0,yield i,e.restoreLastActiveTarget(),n.restoringFocus=!1,n.setMaintainingFocus(!1)})),t.on("pointerinteraction",i=>W(this,null,function*(){n.setMaintainingFocus(!0),yield i,n.setMaintainingFocus(!1)}))}o(na,"setupOskListeners");function hg(a){let t=document.createElement(a);return t.style.userSelect="none",t}o(hg,"createUnselectableElement");var _c=class _c{constructor(t){this.getAbsoluteX=K;this.getAbsoluteY=z;this._GetAbsoluteX=K;this._GetAbsoluteY=z;this._GetAbsolute=this.getAbsolute;this.toNzString=this.nzString;this.createElement=hg;this.getStyleValue=Jt;this.config=t,this.stylesheetManager=new ge(document.body,t.applyCacheBusting),this.domEventTracker=new xe}isTouchDevice(){return this.config.hostDevice.touchable}getAbsolute(t){return{x:K(t),y:z(t)}}getOption(t,e){return t in this.config.paths?this.config.paths[t]:t in this.config.options?this.config.options[t]:arguments.length>1?e:""}setOption(t,e){switch(t){case"attachType":break;case"ui":break;case"useAlerts":this.config.signalUser=e?new It:null;break;case"setActiveOnRegister":this.config.activateFirstKeyboard=!!e;break;case"spacebarText":this.config.spacebarText=e;break;default:throw new Error("Path-related options may not be changed after the engine has initialized.")}}loadCookie(t){return new le(t).load(decodeURIComponent)}saveCookie(t,e){new le(t).save(e,encodeURIComponent)}addStyleSheet(t){let e=st(t);return this.stylesheetManager.linkStylesheet(e),e}removeStyleSheet(t){return this.stylesheetManager.unlink(t)}linkStyleSheet(t){this.stylesheetManager.linkExternalSheet(t)}getLanguageCodes(t){return t.indexOf("-")==-1?[t]:t.split("-")}attachDOMEvent(t,e,n,i){this.domEventTracker.attachDOMEvent(t,e,n,i)}detachDOMEvent(t,e,n,i){this.domEventTracker.detachDOMEvent(t,e,n,i)}get alertHost(){return this.config.alertHost?this.config.alertHost:(this._alertHost||(this._alertHost=new It),this._alertHost)}alert(t,e){this.alertHost.alert(t,e)}nzString(t,e){let n="";return arguments.length>1&&(n=e),typeof t=="undefined"||t==null||t==0||t==""?n:""+t}toNumber(t,e){let n=parseInt(t,10);return isNaN(n)?e:n}toFloat(t,e){let n=parseFloat(t);return isNaN(n)?e:n}rgba(t,e,n,i,s){let l="transparent";try{l="rgba("+e+","+n+","+i+","+s+")"}catch(r){l="rgb("+e+","+n+","+i+")"}return l}shutdown(){var t,e,n;(t=this.stylesheetManager)==null||t.unlinkAll(),(e=this.domEventTracker)==null||e.shutdown(),(n=this._alertHost)==null||n.shutdown()}};o(_c,"UtilApiEndpoint");var Os=_c;var $c=class $c{constructor(t,e,n){this.code=t,this.shift=e,this.handler=n}matches(t,e){return this.code==t&&this.shift==e}};o($c,"Hotkey");var qc=$c,eo=class eo{constructor(){this.hotkeys=[];this._Process=o(t=>{t||(t=window.event);var e=zc(t);if(e==null)return!1;for(var n=(t.shiftKey?16:0)|(t.ctrlKey?32:0)|(t.altKey?64:0),i=0;i{this.keyboardInterface.resetContextCache();var t;for(this._BeepTimeout=0,t=0;tthis.legacyAPIEvents),s=>({keyboardInterface:new Qn(window,s),defaultOutputRules:new Li(s.contextManager)}));this._initialized=0;this.hotkeyManager=new Ps;this.getOskHeight=null;this.getOskWidth=null;this.helpURL="https://help.keyman.com/go";this.keyEventRefocus=o(()=>{this.contextManager.restoreLastActiveTarget()},"keyEventRefocus");this._GetKeyboardDetail=o(function(e,n){return{Name:e.KN,InternalName:e.KI,LanguageName:e.KL,LanguageCode:e.KLC,RegionName:e.KR,RegionCode:e.KRC,CountryName:e.KC,CountryCode:e.KCC,KeyboardID:e.KD,Font:e.KFont,OskFont:e.KOskFont,HasLoaded:!!n,IsRTL:n?n.isRTL:null}},"_GetKeyboardDetail");this._util=new Os(i),this.beepHandler=new js(this.core.keyboardInterface),this.core.keyboardProcessor.beepHandler=()=>this.beepHandler.beep(this.contextManager.activeTarget),this.hardKeyboard=new Ri(i.hardDevice,this.core.keyboardProcessor,this.contextManager),this.contextManager.on("targetchange",s=>{let l=s==null?void 0:s.getElement();this.osk&&(this.osk.activationModel.activationTrigger=l),this.config.hostDevice.touchable&&s&&this.ensureElementVisibility(l)})}ensureElementVisibility(e){if(!e||!this.osk)return;let n=z(e),i=window.pageYOffset,s=n-i;n>=i&&(s-=window.innerHeight-this.osk._Box.offsetHeight-e.offsetHeight-2,s<0&&(s=0)),s!=0&&window.scrollTo(0,s+i)}get util(){return this._util}get views(){return xc}get initialized(){return this._initialized}get ui(){return this._ui}set ui(e){this._ui&&this._ui.shutdown(),this._ui=e,this.config.deferForInitialization.isFulfilled&&e.initialize()}init(e){return W(this,null,function*(){let i=new Qt().detect(),s=y(y({},ea),e);if(this.config.hostDevice=i,this.config.initialize(s),this._initialized=1,yield Vi(),this.config.deferForInitialization.isResolved)return Promise.resolve();yield Ji(_s.prototype,this,"init").call(this,s),this.keyboardRequisitioner.cloudQueryEngine.once("unboundregister",()=>{var r;(r=this.contextManager.activeKeyboard)!=null&&r.keyboard||this.setActiveKeyboard("","")}),this.contextManager.initialize();let l=this.contextManager.getSavedKeyboardRaw();i.touchable?this.osk=new Qi(this):this.osk=new Ui(this),na(this,this.osk,this.contextManager),this.pageIntegration=new zs(window,this),this.config.finalizeInit(),this.ui&&(this.ui.initialize(),this.legacyAPIEvents.callEvent("loaduserinterface",{})),this._initialized=2,yield Promise.resolve(),this.contextManager.restoreSavedKeyboard(l),yield Promise.resolve()})}get register(){return this.keyboardRequisitioner.cloudQueryEngine.registerFromCloud}getUIState(){return this.contextManager.focusAssistant.getUIState()}activatingUI(e){this.contextManager.focusAssistant.setMaintainingFocus(!!e)}setKeyboardForControl(e,n,i){if(e instanceof e.ownerDocument.defaultView.HTMLIFrameElement){console.warn("'keymanweb.setKeyboardForControl' cannot set keyboard on iframes.");return}if(!this.isAttached(e)){console.error("KeymanWeb is not attached to element "+e);return}let s=null;if(n&&(s=this.keyboardRequisitioner.cache.getStub(n,i),!s))throw new Error(`No keyboard has been registered with id ${n} and language code ${i}.`);this.contextManager.setKeyboardForTarget(e._kmwAttachment.interface,n,i)}getKeyboardForControl(e){let n=Ve(e);return this.contextManager.getKeyboardStubForTarget(n).id}getLanguageForControl(e){let n=Ve(e);return this.contextManager.getKeyboardStubForTarget(n).langId}isAttached(e){return this.contextManager.page.isAttached(e)}addKeyboards(...e){return this.config.deferForInitialization.then(()=>{if(!e||!e[0]||e[0].length==0)return this.keyboardRequisitioner.fetchCloudCatalog().catch(n=>(console.error(n[0].error),n));{let n=[];return Array.isArray(e[0])?n=n.concat(e[0]):Array.isArray(e)&&(n=n.concat(e)),this.keyboardRequisitioner.addKeyboardArray(n)}})}addKeyboardsForLanguage(e){return this.config.deferForInitialization.then(()=>typeof e=="string"?this.keyboardRequisitioner.addLanguageKeyboards(e.split(",").map(n=>n.trim())):this.keyboardRequisitioner.addLanguageKeyboards(e))}isCJK(e){let n;if(e){let i=e;i.KeyboardID?n=this.keyboardRequisitioner.cache.getKeyboard(i.KeyboardID):n=new Y(e)}else n=this.core.activeKeyboard;return n&&n.isCJK}getKeyboard(e,n){let i=this.keyboardRequisitioner.cache.getStub(e,n),s=this.keyboardRequisitioner.cache.getKeyboardForStub(i);return i&&this._GetKeyboardDetail(i,s)}getKeyboards(){let e=[],n=this.keyboardRequisitioner.cache,i=n.getStubList();for(let s=0;s 16 bit so not available here.\r\n // See keys_mod_other in keyman_core_ldml.ts\r\n}\r\n", + "\r\n// Define standard keycode numbers (exposed for use by other modules)\r\n\r\n/**\r\n * May include non-US virtual key codes\r\n */\r\nexport const USVirtualKeyCodes = {\r\n K_BKSP:8,\r\n K_TAB:9,\r\n K_ENTER:13,\r\n K_SHIFT:16,\r\n K_CONTROL:17,\r\n K_ALT:18,\r\n K_PAUSE:19,\r\n K_CAPS:20,\r\n K_ESC:27,\r\n K_SPACE:32,\r\n K_PGUP:33,\r\n K_PGDN:34,\r\n K_END:35,\r\n K_HOME:36,\r\n K_LEFT:37,\r\n K_UP:38,\r\n K_RIGHT:39,\r\n K_DOWN:40,\r\n K_SEL:41,\r\n K_PRINT:42,\r\n K_EXEC:43,\r\n K_INS:45,\r\n K_DEL:46,\r\n K_HELP:47,\r\n K_0:48,\r\n K_1:49,\r\n K_2:50,\r\n K_3:51,\r\n K_4:52,\r\n K_5:53,\r\n K_6:54,\r\n K_7:55,\r\n K_8:56,\r\n K_9:57,\r\n K_A:65,\r\n K_B:66,\r\n K_C:67,\r\n K_D:68,\r\n K_E:69,\r\n K_F:70,\r\n K_G:71,\r\n K_H:72,\r\n K_I:73,\r\n K_J:74,\r\n K_K:75,\r\n K_L:76,\r\n K_M:77,\r\n K_N:78,\r\n K_O:79,\r\n K_P:80,\r\n K_Q:81,\r\n K_R:82,\r\n K_S:83,\r\n K_T:84,\r\n K_U:85,\r\n K_V:86,\r\n K_W:87,\r\n K_X:88,\r\n K_Y:89,\r\n K_Z:90,\r\n K_NP0:96,\r\n K_NP1:97,\r\n K_NP2:98,\r\n K_NP3:99,\r\n K_NP4:100,\r\n K_NP5:101,\r\n K_NP6:102,\r\n K_NP7:103,\r\n K_NP8:104,\r\n K_NP9:105,\r\n K_NPSTAR:106,\r\n K_NPPLUS:107,\r\n K_SEPARATOR:108,\r\n K_NPMINUS:109,\r\n K_NPDOT:110,\r\n K_NPSLASH:111,\r\n K_F1:112,\r\n K_F2:113,\r\n K_F3:114,\r\n K_F4:115,\r\n K_F5:116,\r\n K_F6:117,\r\n K_F7:118,\r\n K_F8:119,\r\n K_F9:120,\r\n K_F10:121,\r\n K_F11:122,\r\n K_F12:123,\r\n K_NUMLOCK:144,\r\n K_SCROLL:145,\r\n K_LSHIFT:160,\r\n K_RSHIFT:161,\r\n K_LCONTROL:162,\r\n K_RCONTROL:163,\r\n K_LALT:164,\r\n K_RALT:165,\r\n K_COLON:186,\r\n K_EQUAL:187,\r\n K_COMMA:188,\r\n K_HYPHEN:189,\r\n K_PERIOD:190,\r\n K_SLASH:191,\r\n K_BKQUOTE:192,\r\n K_LBRKT:219,\r\n /**\r\n * == K_OEM_5, 0xDC\r\n */\r\n K_BKSLASH:220,\r\n K_RBRKT:221,\r\n K_QUOTE:222,\r\n /**\r\n * ISO B00, key to right of left shift, not on US keyboard,\r\n * 0xE2, K_OEM_102\r\n */\r\n K_oE2:226,\r\n K_OE2:226,\r\n K_oC1:193, // ISO B11, ABNT-2 key to left of right shift, not on US keyboard\r\n K_OC1:193,\r\n 'K_?C1':193,\r\n 'k_?C1':193,\r\n K_oDF:0xDF,\r\n K_ODF:0xDF,\r\n K_LOPT:50001,\r\n K_ROPT:50002,\r\n K_NUMERALS:50003,\r\n K_SYMBOLS:50004,\r\n K_CURRENCIES:50005,\r\n K_UPPER:50006,\r\n K_LOWER:50007,\r\n K_ALPHA:50008,\r\n K_SHIFTED:50009,\r\n K_ALTGR:50010,\r\n K_TABBACK:50011,\r\n K_TABFWD:50012\r\n};\r\n\r\nconst k = USVirtualKeyCodes;\r\n\r\n/** Map a CLDR scancode to a US VKey ala USVirtualKeyCodes */\r\nexport const CLDRScanToUSVirtualKeyCodes = {\r\n 0x02: k.K_1,\r\n 0x03: k.K_2,\r\n 0x04: k.K_3,\r\n 0x05: k.K_4,\r\n 0x06: k.K_5,\r\n 0x07: k.K_6,\r\n 0x08: k.K_7,\r\n 0x09: k.K_8,\r\n 0x0A: k.K_9,\r\n 0x0B: k.K_0,\r\n 0x0C: k.K_HYPHEN,\r\n 0x0D: k.K_EQUAL,\r\n\r\n 0x10: k.K_Q,\r\n 0x11: k.K_W,\r\n 0x12: k.K_E,\r\n 0x13: k.K_R,\r\n 0x14: k.K_T,\r\n 0x15: k.K_Y,\r\n 0x16: k.K_U,\r\n 0x17: k.K_I,\r\n 0x18: k.K_O,\r\n 0x19: k.K_P,\r\n 0x1A: k.K_LBRKT,\r\n 0x1B: k.K_RBRKT,\r\n\r\n 0x1E: k.K_A,\r\n 0x1F: k.K_S,\r\n 0x20: k.K_D,\r\n 0x21: k.K_F,\r\n 0x22: k.K_G,\r\n 0x23: k.K_H,\r\n 0x24: k.K_J,\r\n 0x25: k.K_K,\r\n 0x26: k.K_L,\r\n 0x27: k.K_COLON,\r\n 0x28: k.K_QUOTE,\r\n 0x29: k.K_BKQUOTE,\r\n\r\n 0x2B: k.K_BKSLASH,\r\n 0x2C: k.K_Z,\r\n 0x2D: k.K_X,\r\n 0x2E: k.K_C,\r\n 0x2F: k.K_V,\r\n 0x30: k.K_B,\r\n 0x31: k.K_N,\r\n 0x32: k.K_M,\r\n 0x33: k.K_COMMA,\r\n 0x34: k.K_PERIOD,\r\n 0x35: k.K_SLASH,\r\n\r\n 0x39: k.K_SPACE,\r\n\r\n 0x56: k.K_oE2, // << Same as 0x7D; found on iso, abnt2\r\n 0x73: k.K_oC1,\r\n 0x7D: k.K_oE2, // << Same as 0x56; found on jis\r\n\r\n};\r\n\r\nexport type KeyMap = number[][];\r\n\r\n/**\r\n * Convert a scan code numerical KeyMap to VKeys\r\n * @param scans keymap to convert\r\n * @param badScans output: set of not-found scancodes\r\n * @returns\r\n */\r\nexport function CLDRScanToKeyMap(scans: KeyMap, badScans?: Set): KeyMap {\r\n return scans.map((row) => row.map((scan) => CLDRScanToVkey(scan, badScans)));\r\n}\r\n\r\n/** Convert one scan code to vkey, or undefined */\r\nexport function CLDRScanToVkey(scan: number, badScans?: Set): number {\r\n /** typescript fun to index the scan table */\r\n function hasScanCode(key: PropertyKey): key is keyof typeof CLDRScanToUSVirtualKeyCodes {\r\n return key in CLDRScanToUSVirtualKeyCodes;\r\n }\r\n if (hasScanCode(scan)) {\r\n return CLDRScanToUSVirtualKeyCodes[scan];\r\n } else {\r\n badScans?.add(scan);\r\n return undefined;\r\n }\r\n}\r\n\r\n", + "//\r\n// .keyman-touch-layout JSON format\r\n//\r\n// Follows /common/schemas/keyman-touch-layout/keyman-touch-layout.spec.json for\r\n// reading and\r\n// /common/schemas/keyman-touch-layout/keyman-touch-layout.clean.spec.json for\r\n// writing\r\n//\r\n\r\n/**\r\n * On screen keyboard description consisting of specific layouts for tablet, phone,\r\n * and desktop. Despite its name, this format is used for both touch layouts and\r\n * hardware-style layouts.\r\n */\r\nexport interface TouchLayoutFile {\r\n tablet?: TouchLayoutPlatform;\r\n phone?: TouchLayoutPlatform;\r\n desktop?: TouchLayoutPlatform;\r\n};\r\n\r\nexport type TouchLayoutFont = string;\r\nexport type TouchLayoutFontSize = string;\r\nexport type TouchLayoutDefaultHint = \"none\"|\"dot\"|\"longpress\"|\"multitap\"|\"flick\"|\"flick-n\"|\"flick-ne\"|\"flick-e\"|\"flick-se\"|\"flick-s\"|\"flick-sw\"|\"flick-w\"|\"flick-nw\";\r\n\r\n/** touch layout specification for a specific platform like phone or tablet */\r\nexport interface TouchLayoutPlatform {\r\n font?: TouchLayoutFont;\r\n fontsize?: TouchLayoutFontSize;\r\n layer: TouchLayoutLayer[];\r\n displayUnderlying?: boolean;\r\n defaultHint: TouchLayoutDefaultHint;\r\n};\r\n\r\nexport type TouchLayoutLayerId = string; // pattern = /^[a-zA-Z0-9_-]+$/\r\n\r\n/** a layer with rows of keys on a touch layout */\r\nexport interface TouchLayoutLayer {\r\n id: TouchLayoutLayerId;\r\n row: TouchLayoutRow[];\r\n};\r\n\r\nexport type TouchLayoutRowId = number;\r\n\r\n/** a row of keys on a touch layout */\r\nexport interface TouchLayoutRow {\r\n id: TouchLayoutRowId;\r\n key: TouchLayoutKey[];\r\n};\r\n\r\ntype Key_Type = 'T'|'K'|'U'|'t'|'k'|'u';\r\ntype Key_Id = string;\r\n\r\nexport type TouchLayoutKeyId = `${Key_Type}_${Key_Id}`; // pattern = /^[TKUtku]_[a-zA-Z0-9_]+$/\r\n\r\n/**\r\n * Denotes private-use identifiers that should be considered 'reserved'.\r\n */\r\nexport const PRIVATE_USE_IDS = [\r\n /**\r\n * Private-use identifiers used by KeymanWeb for the default multitap-into-caps-layer key\r\n * for keyboards with a caps layer while not defining multitaps on shift.\r\n */\r\n 'T_*_MT_SHIFT_TO_SHIFT',\r\n 'T_*_MT_SHIFT_TO_CAPS',\r\n 'T_*_MT_SHIFT_TO_DEFAULT'\r\n] as const;\r\n\r\n/* A map of key field names with values matching the `typeof` the corresponding property\r\n * exists in /web/src/engine/keyboard/src/keyboards/activeLayout.ts.\r\n *\r\n * Make sure that when one is updated, the other also is. TS types are compile-time only,\r\n * so the run-time-accessible mapping in activeLayout.ts cannot be auto-generated by TS. */\r\n/** defines a key on a touch layout */\r\nexport interface TouchLayoutKey {\r\n /** key id: used to find key in VKDictionary, or a standard key from the K_ enumeration */\r\n id?: TouchLayoutKeyId;\r\n /** text to display on key cap */\r\n text?: string;\r\n /**\r\n * the modifier combination (not layer) that should be used in key events,\r\n * for this key, overriding the layer that the key is a part of.\r\n */\r\n layer?: TouchLayoutLayerId;\r\n /** the next layer to switch to after this key is pressed */\r\n nextlayer?: TouchLayoutLayerId;\r\n /** font */\r\n font?: TouchLayoutFont;\r\n /** fontsize */\r\n fontsize?: TouchLayoutFontSize;\r\n /** the type of key */\r\n sp?: TouchLayoutKeySp;\r\n /** padding */\r\n pad?: TouchLayoutKeyPad;\r\n /** width of the key */\r\n width?: TouchLayoutKeyWidth;\r\n /** longpress keys, also known as subkeys */\r\n sk?: TouchLayoutSubKey[];\r\n /** flicks */\r\n flick?: TouchLayoutFlick;\r\n /** multitaps */\r\n multitap?: TouchLayoutSubKey[];\r\n /** hint e.g. for longpress */\r\n hint?: string;\r\n};\r\n\r\n/** key type like regular key, framekeys, deadkeys, blank, etc. */\r\nexport const enum TouchLayoutKeySp {\r\n normal=0,\r\n /** A 'frame' key, such as Shift or Enter, which is styled accordingly; uses\r\n * the 'KeymanwebOsk' font on KeymanWeb */\r\n special=1,\r\n /** A 'frame' key, such as Shift or Enter, which is styled accordingly and is\r\n * highlighted to indicate it is active, such as the shift key on a shift\r\n * layer; uses the 'KeymanwebOsk' font on KeymanWeb */\r\n specialActive=2,\r\n /** **KeymanWeb runtime private use:** a variant of `special` with the\r\n * keyboard font rather than 'KeymanwebOsk' font */\r\n customSpecial=3,\r\n /** **KeymanWeb runtime private use:** a variant of `specialActive` with the\r\n * keyboard font rather than 'KeymanwebOsk' font. */\r\n customSpecialActive=4,\r\n /** A styling signal to indicate that the key may have 'deadkey' type\r\n * behaviour. */\r\n deadkey=8,\r\n /** A key which is rendered as a blank keycap, blocks any interaction */\r\n blank=9,\r\n /** Renders the key only as a gap or spacer, blocks any interaction */\r\n spacer=10\r\n};\r\n\r\n/** padding for a key */\r\nexport type TouchLayoutKeyPad = number; // 0-100000\r\n/** width of a key */\r\nexport type TouchLayoutKeyWidth = number; // 0-100000\r\n\r\n/** defines a subkey */\r\nexport interface TouchLayoutSubKey {\r\n /** key id: used to find key in VKDictionary, or a standard key from the K_ enumeration */\r\n id: TouchLayoutKeyId;\r\n /** text to display on key cap */\r\n text?: string;\r\n /**\r\n * the modifier combination (not layer) that should be used in key events,\r\n * for this key, overriding the layer that the key is a part of.\r\n */\r\n layer?: TouchLayoutLayerId;\r\n /** the next layer to switch to after this key is pressed */\r\n nextlayer?: TouchLayoutLayerId;\r\n /** font */\r\n font?: TouchLayoutFont;\r\n /** fontsize */\r\n fontsize?: TouchLayoutFontSize;\r\n /** the type of key */\r\n sp?: TouchLayoutKeySp;\r\n /** padding */\r\n pad?: TouchLayoutKeyPad;\r\n /** width of the key */\r\n width?: TouchLayoutKeyWidth;\r\n /** use this subkey if no other selected */\r\n default?: boolean; // Only used for longpress currently\r\n};\r\n\r\n/** defines all possible flicks for a key */\r\nexport interface TouchLayoutFlick {\r\n /** flick up (north) */\r\n n?: TouchLayoutSubKey;\r\n /** flick down (south) */\r\n s?: TouchLayoutSubKey;\r\n /** flick right (east) */\r\n e?: TouchLayoutSubKey;\r\n /** flick left (west) */\r\n w?: TouchLayoutSubKey;\r\n /** flick up-right (north-east) */\r\n ne?: TouchLayoutSubKey;\r\n /** flick up-left (north-west) */\r\n nw?: TouchLayoutSubKey;\r\n /** flick down-right (south-east) */\r\n se?: TouchLayoutSubKey;\r\n /** flick down-left (south-west) */\r\n sw?: TouchLayoutSubKey;\r\n};\r\n", + "import { MATCH_HEX_ESCAPE, CONTAINS_QUAD_ESCAPE, MATCH_QUAD_ESCAPE } from './consts.js';\r\nexport { MATCH_HEX_ESCAPE, CONTAINS_QUAD_ESCAPE, MATCH_QUAD_ESCAPE };\r\n\r\n/**\r\n * xml2js will not place single-entry objects into arrays. Easiest way to fix\r\n * this is to box them ourselves as needed. Ensures that o.x is an array.\r\n *\r\n * @param o Object with property to box\r\n * @param x Name of element to box\r\n */\r\nexport function boxXmlArray(o: any, x: string): void {\r\n if(typeof o == 'object' && !Array.isArray(o[x])) {\r\n if(o[x] === null || o[x] === undefined) {\r\n o[x] = [];\r\n }\r\n else {\r\n o[x] = [o[x]];\r\n }\r\n }\r\n}\r\n\r\nexport class UnescapeError extends Error {\r\n}\r\n\r\n/**\r\n * Unescape one codepoint\r\n * @param hex one codepoint in hex, such as '0127'\r\n * @returns the unescaped codepoint\r\n */\r\nexport function unescapeOne(hex: string): string {\r\n const codepoint = Number.parseInt(hex, 16);\r\n return String.fromCodePoint(codepoint);\r\n}\r\n\r\n/**\r\n * Unescape one single quad string such as \\u0127 / \\U00000000\r\n * Throws exception if the string doesn't match MATCH_QUAD_ESCAPE\r\n * Note this does not attempt to handle or reject surrogates.\r\n * So, `\\\\uD838\\\\uDD09` will work but other combinations may not.\r\n * @param s input string\r\n * @returns output\r\n */\r\nexport function unescapeOneQuadString(s: string): string {\r\n if (!s || !s.match(MATCH_QUAD_ESCAPE)) {\r\n throw new UnescapeError(`Not a quad escape: ${s}`);\r\n }\r\n function processMatch(str: string, m16: string, m32: string): string {\r\n return unescapeOne(m16 || m32); // either \\u or \\U\r\n }\r\n s = s.replace(MATCH_QUAD_ESCAPE, processMatch);\r\n return s;\r\n}\r\n\r\n/** unscape multiple occurences of \\u0127 style strings */\r\nexport function unescapeQuadString(s: string): string {\r\n s = s.replaceAll(MATCH_QUAD_ESCAPE, (quad) => unescapeOneQuadString(quad));\r\n return s;\r\n}\r\n\r\n\r\n/**\r\n * Unescapes a string according to UTS#18§1.1, see \r\n * @param s escaped string\r\n * @returns\r\n */\r\nexport function unescapeString(s: string): string {\r\n if(!s) {\r\n return s;\r\n }\r\n try {\r\n /**\r\n * process one regex match\r\n * @param str ignored\r\n * @param matched the entire match such as '0127' or '22 22'\r\n * @returns the unescaped match\r\n */\r\n function processMatch(str: string, matched: string) : string {\r\n const codepoints = matched.split(' ');\r\n const unescaped = codepoints.map(unescapeOne);\r\n return unescaped.join('');\r\n }\r\n s = s.replaceAll(MATCH_HEX_ESCAPE, processMatch);\r\n } catch(e) {\r\n if (e instanceof RangeError) {\r\n throw new UnescapeError(`Out of range while unescaping '${s}': ${e.message}`, { cause: e });\r\n /* c8 ignore next 3 */\r\n } else {\r\n throw e; // pass through some other error\r\n }\r\n }\r\n return s;\r\n}\r\n\r\n/** 0000 … FFFF */\r\nexport function hexQuad(n: number): string {\r\n if (n < 0x0000 || n > 0xFFFF) {\r\n throw RangeError(`${n} not in [0x0000,0xFFFF]`);\r\n }\r\n return n.toString(16).padStart(4, '0');\r\n}\r\n\r\n/** 00000000 … FFFFFFFF */\r\nexport function hexOcts(n: number): string {\r\n if (n < 0x0000 || n > 0xFFFFFFFF) {\r\n throw RangeError(`${n} not in [0x00000000,0xFFFFFFFF]`);\r\n }\r\n return n.toString(16).padStart(8, '0');\r\n}\r\n\r\n/** escape one char for regex in \\uXXXX form */\r\nexport function escapeRegexChar(ch: string) {\r\n const code = ch.codePointAt(0);\r\n if (code <= 0xFFFF) {\r\n return '\\\\u' + hexQuad(code);\r\n } else {\r\n return '\\\\U' + hexOcts(code);\r\n }\r\n}\r\n\r\n/** chars that must be escaped: syntax, C0 + C1 controls */\r\nconst REGEX_SYNTAX_CHAR = /^[\\u0000-\\u001F\\u007F-\\u009F{}\\[\\]\\\\?|.^$*()/+-]$/;\r\n\r\nfunction escapeRegexCharIfSyntax(ch: string) {\r\n // escape if syntax or not valid\r\n if (REGEX_SYNTAX_CHAR.test(ch) || !isValidUnicode(ch.codePointAt(0))) {\r\n return escapeRegexChar(ch);\r\n } else {\r\n return ch; // leave unescaped\r\n }\r\n}\r\n\r\n/**\r\n * Unescape one codepoint to \\u or \\U format\r\n * @param hex one codepoint in hex, such as '0127'\r\n * @returns the unescaped codepoint\r\n */\r\nfunction regexOne(hex: string): string {\r\n const unescaped = unescapeOne(hex);\r\n // re-escape as 16 or 32 bit code units\r\n return Array.from(unescaped).map(ch => escapeRegexCharIfSyntax(ch)).join('');\r\n}\r\n/**\r\n * Escape a string (\\uxxxx form) if there are any problematic codepoints\r\n */\r\nexport function escapeStringForRegex(s: string) : string {\r\n return s.split('').map(ch => escapeRegexCharIfSyntax(ch)).join('');\r\n}\r\n\r\n/**\r\n * Unescapes a string according to UTS#18§1.1, see \r\n * @param s escaped string\r\n * @returns\r\n */\r\nexport function unescapeStringToRegex(s: string): string {\r\n if(!s) {\r\n return s;\r\n }\r\n try {\r\n /**\r\n * process one regex match\r\n * @param str ignored\r\n * @param matched the entire match such as '0127' or '22 22'\r\n * @returns the unescaped match\r\n */\r\n function processMatch(str: string, matched: string) : string {\r\n const codepoints = matched.split(' ');\r\n const unescaped = codepoints.map(regexOne);\r\n return unescaped.join('');\r\n }\r\n s = s.replaceAll(MATCH_HEX_ESCAPE, processMatch);\r\n } catch(e) {\r\n if (e instanceof RangeError) {\r\n throw new UnescapeError(`Out of range while unescaping '${s}': ${e.message}`, { cause: e });\r\n /* c8 ignore next 3 */\r\n } else {\r\n throw e; // pass through some other error\r\n }\r\n }\r\n return s;\r\n}\r\n\r\n/** True if this string *could* be a UTF-32 single char */\r\nexport function\r\nisOneChar(value: string) : boolean {\r\n return [...value].length === 1;\r\n}\r\n\r\nexport function\r\ntoOneChar(value: string) : number {\r\n if (!isOneChar(value)) {\r\n throw Error(`Not a single char: ${value}`);\r\n }\r\n return value.codePointAt(0);\r\n}\r\n\r\nexport function describeCodepoint(ch : number) : string {\r\n let s;\r\n const p = BadStringAnalyzer.getProblem(ch);\r\n if (p != null) {\r\n // for example: 'PUA (U+E010)'\r\n s = p;\r\n } else {\r\n // for example: '\"a\" (U+61)'\r\n s = `\"${String.fromCodePoint(ch)}\"`;\r\n }\r\n return `${s} (U+${Number(ch).toString(16).toUpperCase()})`;\r\n}\r\n\r\n\r\nexport enum BadStringType {\r\n pua = 'PUA',\r\n unassigned = 'Unassigned',\r\n illegal = 'Illegal',\r\n denormalized = \"Denormalized\"\r\n};\r\n\r\n// Following from kmx_xstring.h / .cpp\r\n\r\nconst Uni_LEAD_SURROGATE_START = 0xD800;\r\nconst Uni_LEAD_SURROGATE_END = 0xDBFF;\r\nconst Uni_TRAIL_SURROGATE_START = 0xDC00;\r\nconst Uni_TRAIL_SURROGATE_END = 0xDFFF;\r\nconst Uni_SURROGATE_START = Uni_LEAD_SURROGATE_START;\r\nconst Uni_SURROGATE_END = Uni_TRAIL_SURROGATE_END;\r\nconst Uni_FD_NONCHARACTER_START = 0xFDD0;\r\nconst Uni_FD_NONCHARACTER_END = 0xFDEF;\r\nconst Uni_FFFE_NONCHARACTER = 0xFFFE;\r\nconst Uni_PLANE_MASK = 0x1F0000;\r\nconst Uni_MAX_CODEPOINT = 0x10FFFF;\r\n// plane 0, 15, and 16 PUA\r\nconst Uni_PUA_00_START = 0xE000;\r\nconst Uni_PUA_00_END = 0xF8FF;\r\nconst Uni_PUA_15_START = 0x0F0000;\r\nconst Uni_PUA_15_END = 0x0FFFFD;\r\nconst Uni_PUA_16_START = 0x100000;\r\nconst Uni_PUA_16_END = 0x10FFFD;\r\n\r\n\r\n/**\r\n * @brief True if a lead surrogate\r\n * \\def Uni_IsSurrogate1\r\n */\r\nexport function Uni_IsSurrogate1(ch : number) {\r\n return ((ch) >= Uni_LEAD_SURROGATE_START && (ch) <= Uni_LEAD_SURROGATE_END);\r\n}\r\n/**\r\n * @brief True if a trail surrogate\r\n * \\def Uni_IsSurrogate2\r\n */\r\nexport function Uni_IsSurrogate2(ch : number) {\r\n return ((ch) >= Uni_TRAIL_SURROGATE_START && (ch) <= Uni_TRAIL_SURROGATE_END);\r\n}\r\n\r\n/**\r\n * @brief True if any surrogate\r\n * \\def UniIsSurrogate\r\n*/\r\nexport function Uni_IsSurrogate(ch : number) {\r\n return (Uni_IsSurrogate1(ch) || Uni_IsSurrogate2(ch));\r\n}\r\n\r\nfunction Uni_IsEndOfPlaneNonCharacter(ch : number) {\r\n return (((ch) & Uni_FFFE_NONCHARACTER) == Uni_FFFE_NONCHARACTER); // matches FFFF or FFFE\r\n}\r\n\r\nfunction Uni_IsNoncharacter(ch : number) {\r\n return (((ch) >= Uni_FD_NONCHARACTER_START && (ch) <= Uni_FD_NONCHARACTER_END) || Uni_IsEndOfPlaneNonCharacter(ch));\r\n}\r\n\r\nfunction Uni_InCodespace(ch : number) {\r\n return (ch >= 0 && ch <= Uni_MAX_CODEPOINT);\r\n};\r\n\r\nfunction Uni_IsValid1(ch: number) {\r\n return (Uni_InCodespace(ch) && !Uni_IsSurrogate(ch) && !Uni_IsNoncharacter(ch));\r\n}\r\n\r\nexport function isValidUnicode(start: number, end?: number) {\r\n if (!end) {\r\n // single char\r\n return Uni_IsValid1(start);\r\n } else if (!Uni_IsValid1(end) || !Uni_IsValid1(start) || (end < start)) {\r\n // start or end out of range, or inverted range\r\n return false;\r\n } else if ((start <= Uni_SURROGATE_END) && (end >= Uni_SURROGATE_START)) {\r\n // contains some of the surrogate range\r\n return false;\r\n } else if ((start <= Uni_FD_NONCHARACTER_END) && (end >= Uni_FD_NONCHARACTER_START)) {\r\n // contains some of the noncharacter range\r\n return false;\r\n } else if ((start & Uni_PLANE_MASK) != (end & Uni_PLANE_MASK)) {\r\n // start and end are on different planes, meaning that the U+__FFFE/U+__FFFF noncharacters\r\n // are contained.\r\n // As a reminder, we already checked that start/end are themselves valid,\r\n // so we know that 'end' is not on a noncharacter at end of plane.\r\n return false;\r\n } else {\r\n return true;\r\n }\r\n}\r\n\r\nexport function isPUA(ch: number) {\r\n return ((ch >= Uni_PUA_00_START && ch <= Uni_PUA_00_END) ||\r\n (ch >= Uni_PUA_15_START && ch <= Uni_PUA_15_END) ||\r\n (ch >= Uni_PUA_16_START && ch <= Uni_PUA_16_END));\r\n}\r\n\r\nclass BadStringMap extends Map> {\r\n public toString() : string {\r\n if (!this.size) {\r\n return \"{}\";\r\n }\r\n return Array.from(this.entries()).map(([t, s]) => `${t}: ${Array.from(s.values()).map(describeCodepoint).join(' ')}`).join(', ');\r\n }\r\n}\r\n\r\n/** abstract class for analyzing and categorizing strings */\r\nexport abstract class StringAnalyzer {\r\n /** add a string for analysis */\r\n public add(s : string) {\r\n for (const c of [...s]) {\r\n const ch = c.codePointAt(0);\r\n const problem = this.analyzeCodePoint(c, ch);\r\n if (problem) {\r\n this.addProblem(ch, problem);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * subclass interface\r\n * @param c single codepoint to analyze (string)\r\n * @param ch single codepoint to analyze (scalar)\r\n */\r\n protected abstract analyzeCodePoint(c: string, ch: number) : BadStringType;\r\n\r\n /** internal interface for the result of an analysis */\r\n protected addProblem(ch : number, type : BadStringType) {\r\n if (!this.m.has(type)) {\r\n this.m.set(type, new Set());\r\n }\r\n this.m.get(type).add(ch);\r\n }\r\n\r\n /** get the results of the analysis */\r\n public analyze() : BadStringMap {\r\n if (this.m.size == 0) {\r\n return null;\r\n } else {\r\n return this.m;\r\n }\r\n }\r\n\r\n /** internal map */\r\n private m = new BadStringMap();\r\n}\r\n\r\n/** analyze a string looking for bad unicode */\r\nexport class BadStringAnalyzer extends StringAnalyzer {\r\n /** analyze one codepoint */\r\n protected analyzeCodePoint(c: string, ch: number): BadStringType {\r\n return BadStringAnalyzer.getProblem(ch);\r\n }\r\n /** export analyzer function */\r\n public static getProblem(ch: number) {\r\n if (!isValidUnicode(ch)) {\r\n return BadStringType.illegal;\r\n } else if(isPUA(ch)) {\r\n return BadStringType.pua;\r\n } else { // TODO-LDML: unassigned\r\n return null;\r\n }\r\n }\r\n}\r\n\r\n/** Analyzer that checks if something isn't NFD */\r\nexport class NFDAnalyzer extends StringAnalyzer {\r\n protected analyzeCodePoint(c: string, ch: number): BadStringType {\r\n const nfd = c.normalize(\"NFD\");\r\n if (c !== nfd) {\r\n return BadStringType.denormalized;\r\n } else {\r\n return null;\r\n }\r\n }\r\n}\r\n", + "/*\r\n * Keyman is copyright (C) SIL International. MIT License.\r\n *\r\n * Keyboard key codes and modifier bitmasks.\r\n */\r\n\r\nimport { ModifierKeyConstants, USVirtualKeyCodes } from '@keymanapp/common-types';\r\n\r\nconst Codes = {\r\n modifierCodes: {\r\n // Debug-mode keyboards compiled before Keyman 18.0 referenced the `ModifierKeyConstants`\r\n // constants via the names established below. We must continue to support them, as they're\r\n // essentially part of the keyboard API now.\r\n \"LCTRL\": ModifierKeyConstants.LCTRLFLAG,\r\n \"RCTRL\": ModifierKeyConstants.RCTRLFLAG,\r\n \"LALT\": ModifierKeyConstants.LALTFLAG,\r\n \"RALT\": ModifierKeyConstants.RALTFLAG,\r\n \"SHIFT\": ModifierKeyConstants.K_SHIFTFLAG,\r\n \"CTRL\": ModifierKeyConstants.K_CTRLFLAG,\r\n \"ALT\": ModifierKeyConstants.K_ALTFLAG,\r\n // TENTATIVE: Represents command keys, which some OSes use for shortcuts we don't\r\n // want to block. No rule will ever target a modifier set with this bit set to 1.\r\n \"META\": ModifierKeyConstants.K_METAFLAG,\r\n \"CAPS\": ModifierKeyConstants.CAPITALFLAG,\r\n \"NO_CAPS\": ModifierKeyConstants.NOTCAPITALFLAG,\r\n \"NUM_LOCK\": ModifierKeyConstants.NUMLOCKFLAG,\r\n \"NO_NUM_LOCK\": ModifierKeyConstants.NOTNUMLOCKFLAG,\r\n \"SCROLL_LOCK\": ModifierKeyConstants.SCROLLFLAG,\r\n \"NO_SCROLL_LOCK\": ModifierKeyConstants.NOTSCROLLFLAG,\r\n \"VIRTUAL_KEY\": ModifierKeyConstants.ISVIRTUALKEY,\r\n \"VIRTUAL_CHAR_KEY\": ModifierKeyConstants.VIRTUALCHARKEY // Unused by KMW, but reserved for use by other Keyman engines.\r\n // Note: keys_mod_other = 0x10000, used by KMX+ for the\r\n // other modifier flag in layers, > 16 bit so not available here.\r\n // See keys_mod_other in keyman_core_ldml.ts\r\n } as {[name: string]: number},\r\n\r\n modifierBitmasks: {\r\n \"ALL\":0x007F,\r\n \"ALT_GR_SIM\": (0x0001 | 0x0004),\r\n \"CHIRAL\":0x001F, // The base bitmask for chiral keyboards. Includes SHIFT, which is non-chiral.\r\n \"IS_CHIRAL\":0x000F, // Used to test if a bitmask uses a chiral modifier.\r\n \"NON_CHIRAL\":0x0070, // The default bitmask, for non-chiral keyboards,\r\n // Represents all modifier codes not supported by KMW 1.0 legacy keyboards.\r\n \"NON_LEGACY\": 0x006F // ALL, but without the SHIFT bit\r\n } as {[name: string]: number},\r\n\r\n stateBitmasks: {\r\n \"ALL\":0x3F00,\r\n \"CAPS\":0x0300,\r\n \"NUM_LOCK\":0x0C00,\r\n \"SCROLL_LOCK\":0x3000\r\n } as {[name: string]: number},\r\n\r\n // Define standard keycode numbers (exposed for use by other modules)\r\n keyCodes: {\r\n ...USVirtualKeyCodes,\r\n } as {[name: string]: number},\r\n\r\n codesUS: [\r\n ['0123456789',';=,-./`', '[\\\\]\\''],\r\n [')!@#$%^&*(',':+<_>?~', '{|}\"']\r\n ],\r\n\r\n isFrameKey(keyID: string): boolean {\r\n switch(keyID) {\r\n // TODO: consider adding K_ALT, K_CTRL.\r\n // Not currently here as they typically don't show up on mobile layouts.\r\n case 'K_SHIFT':\r\n case 'K_LOPT':\r\n case 'K_ROPT':\r\n case 'K_NUMLOCK': // Often used for numeric layers.\r\n case 'K_CAPS':\r\n return true;\r\n default:\r\n // 50000: start of the range defining key-codes for special frame-key symbols\r\n // and specialized common layer-switching key IDs. See .keyCodes above.\r\n if(Codes.keyCodes[keyID] >= 50000) { // A few are used by `sil_euro_latin`.\r\n return true; // is a 'K_' key defined for layer shifting or 'control' use.\r\n }\r\n }\r\n\r\n return false;\r\n },\r\n\r\n\r\n /**\r\n * Get modifier key state from layer id\r\n *\r\n * @param {string} layerId layer id (e.g. ctrlshift)\r\n * @return {number} modifier key state (desktop keyboards)\r\n */\r\n getModifierState(layerId: string): number {\r\n var modifier=0;\r\n if(layerId.indexOf('shift') >= 0) {\r\n modifier |= ModifierKeyConstants.K_SHIFTFLAG;\r\n }\r\n\r\n // The chiral checks must not be directly exclusive due each other to visual OSK feedback.\r\n var ctrlMatched=false;\r\n if(layerId.indexOf('leftctrl') >= 0) {\r\n modifier |= ModifierKeyConstants.LCTRLFLAG;\r\n ctrlMatched=true;\r\n }\r\n if(layerId.indexOf('rightctrl') >= 0) {\r\n modifier |= ModifierKeyConstants.RCTRLFLAG;\r\n ctrlMatched=true;\r\n }\r\n if(layerId.indexOf('ctrl') >= 0 && !ctrlMatched) {\r\n modifier |= ModifierKeyConstants.K_CTRLFLAG;\r\n }\r\n\r\n var altMatched=false;\r\n if(layerId.indexOf('leftalt') >= 0) {\r\n modifier |= ModifierKeyConstants.LALTFLAG;\r\n altMatched=true;\r\n }\r\n if(layerId.indexOf('rightalt') >= 0) {\r\n modifier |= ModifierKeyConstants.RALTFLAG;\r\n altMatched=true;\r\n }\r\n if(layerId.indexOf('alt') >= 0 && !altMatched) {\r\n modifier |= ModifierKeyConstants.K_ALTFLAG;\r\n }\r\n\r\n return modifier;\r\n },\r\n\r\n /**\r\n * Get state key state from layer id\r\n *\r\n * @param {string} layerId layer id (e.g. caps)\r\n * @return {number} modifier key state (desktop keyboards)\r\n */\r\n getStateFromLayer(layerId: string): number {\r\n var modifier=0;\r\n\r\n if(layerId.indexOf('caps') >= 0) {\r\n modifier |= ModifierKeyConstants.CAPITALFLAG;\r\n } else {\r\n modifier |= ModifierKeyConstants.NOTCAPITALFLAG;\r\n }\r\n\r\n return modifier;\r\n }\r\n}\r\n\r\nexport default Codes;\r\n", + "/*\r\n * Keyman is copyright (C) SIL International. MIT License.\r\n *\r\n * Implementation of default rules\r\n */\r\n\r\nimport { ModifierKeyConstants } from '@keymanapp/common-types';\r\nimport Codes from './codes.js';\r\nimport type KeyEvent from './keyEvent.js';\r\nimport { type OutputTarget } from './outputTarget.interface.js';\r\n\r\nexport enum EmulationKeystrokes {\r\n Enter = '\\n',\r\n Backspace = '\\b'\r\n}\r\n\r\nexport class LogMessages {\r\n errorLog?: string;\r\n warningLog?: string;\r\n}\r\n\r\n/**\r\n * Defines a collection of static library functions that define KeymanWeb's default (implied) keyboard rule behaviors.\r\n */\r\nexport default class DefaultRules {\r\n codeForEvent(Lkc: KeyEvent) {\r\n return Codes.keyCodes[Lkc.kName] || Lkc.Lcode;;\r\n }\r\n\r\n /**\r\n * Serves as a default keycode lookup table. This may be referenced safely by mnemonic handling without fear of side-effects.\r\n * Also used by Processor.defaultRuleBehavior to generate output after filtering for special cases.\r\n */\r\n public forAny(Lkc: KeyEvent, isMnemonic: boolean, logMessages?: LogMessages): string {\r\n var char = '';\r\n\r\n // A pretty simple table of lookups, corresponding VERY closely to the original defaultKeyOutput.\r\n if((char = this.forSpecialEmulation(Lkc)) != null) {\r\n return char;\r\n } else if(!isMnemonic && ((char = this.forNumpadKeys(Lkc)) != null)) {\r\n return char;\r\n } else if((char = this.forUnicodeKeynames(Lkc, logMessages)) != null) {\r\n return char;\r\n } else if((char = this.forBaseKeys(Lkc, logMessages)) != null) {\r\n return char;\r\n } else {\r\n // // For headless and embeddded, we may well allow '\\t'. It's DOM mode that has other uses.\r\n // // Not originally defined for text output within defaultKeyOutput.\r\n // // We can't enable it yet, as it'll cause hardware keystrokes in the DOM to output '\\t' rather\r\n // // than rely on the browser-default handling.\r\n let code = this.codeForEvent(Lkc);\r\n switch(code) {\r\n // case Codes.keyCodes['K_TAB']:\r\n // case Codes.keyCodes['K_TABBACK']:\r\n // case Codes.keyCodes['K_TABFWD']:\r\n // return '\\t';\r\n default:\r\n return null;\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * isCommand - returns a boolean indicating if a non-text event should be triggered by the keystroke.\r\n */\r\n public isCommand(Lkc: KeyEvent): boolean {\r\n let code = this.codeForEvent(Lkc);\r\n\r\n switch(code) {\r\n // Should we ever implement them:\r\n // case Codes.keyCodes['K_LEFT']: // would not output text, but would alter the caret's position in the context.\r\n // case Codes.keyCodes['K_RIGHT']:\r\n // return true;\r\n default:\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Used when a RuleBehavior represents a non-text \"command\" within the Engine. This will generally\r\n * trigger events that require context reset - often by moving the caret or by moving what OutputTarget\r\n * the caret is in. However, we let those events perform the actual context reset.\r\n *\r\n * Note: is extended by DOM-aware KeymanWeb code.\r\n */\r\n public applyCommand(Lkc: KeyEvent, outputTarget: OutputTarget): void {\r\n // Notes for potential default-handling extensions:\r\n //\r\n // switch(code) {\r\n // // Problem: clusters, and doing them right.\r\n // // The commented-out code below should be a decent starting point, but clusters make it complex.\r\n // // Mostly based on pre-12.0 code, but the general idea should be relatively clear.\r\n //\r\n // case Codes.keyCodes['K_LEFT']:\r\n // if(touchAlias) {\r\n // var caretPos = keymanweb.getTextCaret(Lelem);\r\n // keymanweb.setTextCaret(Lelem, caretPos - 1 >= 0 ? caretPos - 1 : 0);\r\n // }\r\n // break;\r\n // case Codes.keyCodes['K_RIGHT']:\r\n // if(touchAlias) {\r\n // var caretPos = keymanweb.getTextCaret(Lelem);\r\n // keymanweb.setTextCaret(Lelem, caretPos + 1);\r\n // }\r\n // if(code == VisualKeyboard.keyCodes['K_RIGHT']) {\r\n // break;\r\n // }\r\n // }\r\n //\r\n // Note that these would be useful even outside of a DOM context.\r\n }\r\n\r\n /**\r\n * Codes matched here generally have default implementations when in a browser but require emulation\r\n * for 'synthetic' `OutputTarget`s like `Mock`s, which have no default text handling.\r\n */\r\n public forSpecialEmulation(Lkc: KeyEvent): EmulationKeystrokes {\r\n let code = this.codeForEvent(Lkc);\r\n\r\n switch(code) {\r\n case Codes.keyCodes['K_BKSP']:\r\n return EmulationKeystrokes.Backspace;\r\n case Codes.keyCodes['K_ENTER']:\r\n return EmulationKeystrokes.Enter;\r\n // case Codes.keyCodes['K_DEL']:\r\n // return '\\u007f'; // 127, ASCII / Unicode control code for DEL.\r\n default:\r\n return null;\r\n }\r\n }\r\n\r\n // Should not be used for mnenomic keyboards. forAny()'s use of this method checks first.\r\n public forNumpadKeys(Lkc: KeyEvent) {\r\n // Translate numpad keystrokes into their non-numpad equivalents\r\n if(Lkc.Lcode >= Codes.keyCodes[\"K_NP0\"] && Lkc.Lcode <= Codes.keyCodes[\"K_NPSLASH\"]) {\r\n // Number pad, numlock on\r\n if(Lkc.Lcode < 106) {\r\n var Lch = Lkc.Lcode-48;\r\n } else {\r\n Lch = Lkc.Lcode-64;\r\n }\r\n let ch = String._kmwFromCharCode(Lch); //I3319\r\n return ch;\r\n } else {\r\n return null;\r\n }\r\n }\r\n\r\n // Test for fall back to U_xxxxxx key id\r\n // For this first test, we ignore the keyCode and use the keyName\r\n public forUnicodeKeynames(Lkc: KeyEvent, logMessages?: LogMessages) {\r\n const keyName = Lkc.kName;\r\n\r\n // Test for fall back to U_xxxxxx key id\r\n // For this first test, we ignore the keyCode and use the keyName\r\n if(!keyName || keyName.substr(0,2) != 'U_') {\r\n return null;\r\n }\r\n\r\n let result = '';\r\n const codePoints = keyName.substr(2).split('_');\r\n for(let codePoint of codePoints) {\r\n const codePointValue = parseInt(codePoint, 16);\r\n if (((0x0 <= codePointValue) && (codePointValue <= 0x1F)) || ((0x80 <= codePointValue) && (codePointValue <= 0x9F)) || isNaN(codePointValue)) {\r\n // Code points [U_0000 - U_001F] and [U_0080 - U_009F] refer to Unicode C0 and C1 control codes.\r\n // Check the codePoint number and do not allow output of these codes via U_xxxxxx shortcuts.\r\n // Also handles invalid identifiers (e.g. `U_ghij`) for which parseInt returns NaN\r\n if(logMessages) {\r\n logMessages.errorLog = (\"Suppressing Unicode control code in \" + keyName);\r\n }\r\n // We'll attempt to add valid chars\r\n continue;\r\n } else {\r\n // String.fromCharCode() is inadequate to handle the entire range of Unicode\r\n // Someday after upgrading to ES2015, can use String.fromCodePoint()\r\n result += String.kmwFromCharCode(codePointValue);\r\n }\r\n }\r\n return result ? result : null;\r\n }\r\n\r\n // Test for otherwise unimplemented keys on the the base default & shift layers.\r\n // Those keys must be blocked by keyboard rules if intentionally unimplemented; otherwise, this function will trigger.\r\n public forBaseKeys(Lkc: KeyEvent, logMessages?: LogMessages) {\r\n let n = Lkc.Lcode;\r\n let keyShiftState = Lkc.Lmodifiers;\r\n\r\n // check if exact match to SHIFT's code. Only the 'default' and 'shift' layers should have default key outputs.\r\n // TODO: Extend to allow AltGr as well - better mnemonic support.\r\n if (keyShiftState == ModifierKeyConstants.K_SHIFTFLAG) {\r\n keyShiftState = 1;\r\n } else if(keyShiftState != 0) {\r\n if(logMessages) {\r\n logMessages.warningLog = \"KMW only defines default key output for the 'default' and 'shift' layers!\";\r\n }\r\n return null;\r\n }\r\n\r\n // Now that keyShiftState is either 0 or 1, we can use the following structure to determine the default output.\r\n try {\r\n if(n == Codes.keyCodes['K_SPACE']) {\r\n return ' ';\r\n } else if(n >= Codes.keyCodes['K_0'] && n <= Codes.keyCodes['K_9']) { // The number keys.\r\n return Codes.codesUS[keyShiftState][0][n-Codes.keyCodes['K_0']];\r\n } else if(n >= Codes.keyCodes['K_A'] && n <= Codes.keyCodes['K_Z']) { // The base letter keys\r\n return String.fromCharCode(n+(keyShiftState?0:32)); // 32 is the offset from uppercase to lowercase.\r\n } else if(n >= Codes.keyCodes['K_COLON'] && n <= Codes.keyCodes['K_BKQUOTE']) {\r\n return Codes.codesUS[keyShiftState][1][n-Codes.keyCodes['K_COLON']];\r\n } else if(n >= Codes.keyCodes['K_LBRKT'] && n <= Codes.keyCodes['K_QUOTE']) {\r\n return Codes.codesUS[keyShiftState][2][n-Codes.keyCodes['K_LBRKT']];\r\n } else if(n == Codes.keyCodes['K_oE2']) {\r\n return keyShiftState ? '|' : '\\\\';\r\n }\r\n } catch (e) {\r\n if(logMessages) {\r\n logMessages.errorLog = \"Error detected with default mapping for key: code = \" + n + \", shift state = \" + (keyShiftState == 1 ? 'shift' : 'default');\r\n }\r\n }\r\n\r\n return null;\r\n }\r\n}\r\n", + "// TODO: Move to separate folder: 'codes'\r\n// We should start splitting off code needed by keyboards even without a KeyboardProcessor active.\r\n// There's an upcoming `/common/web/types` package that 'codes' and 'keyboards' may fit well within.\r\n\r\n// KeyEvent may be a _little_ bit of pollution, but this IS what the Web OSK currently generates to signal\r\n// a key event. The most straightforward way to integrate Web OSK events on other platforms is to have\r\n// other platforms recognize and utilize this type.\r\n\r\nimport type Keyboard from \"./keyboards/keyboard.js\";\r\nimport { type DeviceSpec } from \"@keymanapp/web-utils\";\r\n\r\nimport Codes from './codes.js';\r\nimport DefaultRules from \"./defaultRules.js\";\r\nimport { ActiveKeyBase } from './keyboards/activeLayout.js';\r\n\r\n// Represents a probability distribution over a keyboard's keys.\r\n// Defined here to avoid compilation issues.\r\nexport type KeyDistribution = { keySpec: ActiveKeyBase, p: number }[];\r\n\r\n/**\r\n * A simple instance of the standard 'default rules' for keystroke processing from the\r\n * DefaultRules base class.\r\n */\r\nconst BASE_DEFAULT_RULES = new DefaultRules();\r\n\r\nexport interface KeyEventSpec {\r\n\r\n Lcode: number;\r\n Lstates: number;\r\n LmodifierChange?: boolean;\r\n Lmodifiers: number;\r\n LisVirtualKey?: boolean;\r\n vkCode?: number;\r\n kName: string;\r\n kLayer?: string; // The key's layer property\r\n kbdLayer?: string; // The virtual keyboard's active layer\r\n kNextLayer?: string;\r\n\r\n /**\r\n * Marks the active keyboard at the time that this KeyEvent was generated by the user.\r\n *\r\n * Note: this is NOT equivalent to the active keyboard at the time that the event handler begins\r\n * processing! It should be set via closure (or similar) on the event handler that can 100%\r\n * guarantee that the keyboard instance known to the handler has not changed during JS execution\r\n * since the user's interaction that raised the event.\r\n */\r\n srcKeyboard?: Keyboard;\r\n\r\n // Holds a generated fat-finger distribution (when appropriate)\r\n keyDistribution?: KeyDistribution;\r\n\r\n /**\r\n * The device model for web-core to follow when processing the keystroke.\r\n */\r\n device: DeviceSpec;\r\n\r\n /**\r\n * `true` if this event was produced by sources other than a DOM-based KeyboardEvent.\r\n */\r\n isSynthetic?: boolean;\r\n}\r\n\r\n/**\r\n * This class is defined within its own file so that it can be loaded by code outside of KMW without\r\n * having to actually load the entirety of KMW.\r\n */\r\nexport default class KeyEvent implements KeyEventSpec {\r\n Lcode: number;\r\n Lstates: number;\r\n LmodifierChange?: boolean;\r\n Lmodifiers: number;\r\n LisVirtualKey?: boolean;\r\n vkCode?: number;\r\n kName: string;\r\n kLayer?: string; // The key's layer property\r\n kbdLayer?: string; // The virtual keyboard's active layer\r\n kNextLayer?: string;\r\n baseTranscriptionToken?: number;\r\n\r\n /**\r\n * Marks the active keyboard at the time that this KeyEvent was generated by the user.\r\n *\r\n * Note: this is NOT equivalent to the active keyboard at the time that the event handler begins\r\n * processing! It should be set via closure (or similar) on the event handler that can 100%\r\n * guarantee that the keyboard instance known to the handler has not changed during JS execution\r\n * since the user's interaction that raised the event.\r\n */\r\n srcKeyboard?: Keyboard;\r\n\r\n // Holds relevant event properties leading to construction of this KeyEvent.\r\n source?: any; // Technically, KeyEvent|MouseEvent|Touch - but those are DOM types that must be kept out of headless mode.\r\n // Holds a generated fat-finger distribution (when appropriate)\r\n keyDistribution?: KeyDistribution;\r\n\r\n /**\r\n * The device model for web-core to follow when processing the keystroke.\r\n */\r\n device: DeviceSpec;\r\n\r\n /**\r\n * `true` if this event was produced by sources other than a DOM-based KeyboardEvent.\r\n */\r\n isSynthetic: boolean = true;\r\n\r\n public constructor(keyEventSpec: KeyEventSpec) {\r\n for(let key in keyEventSpec) {\r\n // @ts-ignore\r\n if(keyEventSpec[key] !== undefined) {\r\n // @ts-ignore\r\n this[key] = keyEventSpec[key];\r\n }\r\n }\r\n }\r\n\r\n public static constructNullKeyEvent(device: DeviceSpec): KeyEvent {\r\n const keyEvent = new KeyEvent({\r\n Lcode: 0,\r\n kName: '',\r\n device: device,\r\n Lstates: undefined,\r\n Lmodifiers: undefined,\r\n vkCode: undefined,\r\n LisVirtualKey: undefined\r\n });\r\n return keyEvent;\r\n }\r\n\r\n get isModifier(): boolean {\r\n switch(this.Lcode) {\r\n case 16: //\"K_SHIFT\":16,\"K_CONTROL\":17,\"K_ALT\":18\r\n case 17:\r\n case 18:\r\n case 20: //\"K_CAPS\":20, \"K_NUMLOCK\":144,\"K_SCROLL\":145\r\n case 144:\r\n case 145:\r\n return true;\r\n default:\r\n return false;\r\n }\r\n }\r\n\r\n // FIXME: makes some bad assumptions.\r\n setMnemonicCode(shifted: boolean, capsActive: boolean) {\r\n // K_SPACE is not handled by defaultKeyOutput for physical keystrokes unless using touch-aliased elements.\r\n // It's also a \"exception required, March 2013\" for clickKey, so at least they both have this requirement.\r\n if(this.Lcode != Codes.keyCodes['K_SPACE']) {\r\n // So long as the key name isn't prefixed with 'U_', we'll get a default mapping based on the Lcode value.\r\n // We need to determine the mnemonic base character - for example, SHIFT + K_PERIOD needs to map to '>'.\r\n let mappingEvent: KeyEvent = new KeyEvent(this);\r\n for(let key in (this as KeyEvent)) {\r\n // @ts-ignore\r\n mappingEvent[key as keyof KeyEvent] = this[key];\r\n }\r\n\r\n // To facilitate storing relevant commands, we should probably reverse-lookup\r\n // the actual keyname instead.\r\n mappingEvent.kName = 'K_xxxx';\r\n mappingEvent.Lmodifiers = (shifted ? 0x10 : 0); // mnemonic lookups only exist for default & shift layers.\r\n var mappedChar: string = BASE_DEFAULT_RULES.forAny(mappingEvent, true);\r\n\r\n /* First, save a backup of the original code. This one won't needlessly trigger keyboard\r\n * rules, but allows us to replicate/emulate commands after rule processing if needed.\r\n * (Like backspaces)\r\n */\r\n this.vkCode = this.Lcode;\r\n if(mappedChar) {\r\n // Will return 96 for 'a', which is a keycode corresponding to Codes.keyCodes('K_NP1') - a numpad key.\r\n // That stated, we're in mnemonic mode - this keyboard's rules are based on the char codes.\r\n this.Lcode = mappedChar.charCodeAt(0);\r\n } else {\r\n // Don't let command-type keys (like K_DEL, which will output '.' otherwise!)\r\n // trigger keyboard rules.\r\n //\r\n // However, DO make sure modifier keys pass through safely.\r\n // (https://github.com/keymanapp/keyman/issues/3744)\r\n if(!this.isModifier) {\r\n delete this.Lcode;\r\n }\r\n }\r\n }\r\n\r\n if(capsActive) {\r\n // TODO: Needs fixing - does not properly mirror physical keystrokes, as Lcode range 96-111 corresponds\r\n // to numpad keys! (Physical keyboard section has its own issues here.)\r\n if((this.Lcode >= 65 && this.Lcode <= 90) /* 'A' - 'Z' */ || (this.Lcode >= 97 && this.Lcode <= 122) /* 'a' - 'z' */) {\r\n this.Lmodifiers ^= 0x10; // Flip the 'shifted' bit, so it'll act as the opposite key.\r\n this.Lcode ^= 0x20; // Flips the 'upper' vs 'lower' bit for the base 'a'-'z' ASCII alphabetics.\r\n }\r\n }\r\n }\r\n};\r\n", + "/***\r\n KeymanWeb 11.0\r\n Copyright 2019 SIL International\r\n***/\r\n\r\nimport type KeyEvent from \"./keyEvent.js\";\r\nimport { KeyEventSpec } from \"./keyEvent.js\";\r\n\r\nclass KeyMap {\r\n [keycode: string]: number;\r\n}\r\n\r\nclass BrowserKeyMaps {\r\n FF: KeyMap = new KeyMap();\r\n Safari: KeyMap = new KeyMap();\r\n Opera: KeyMap = new KeyMap();\r\n\r\n constructor() {\r\n // All three have been around since at least May 2014 / FF 29.\r\n // It'd hard to find precise history, but at least that much has been confirmed.\r\n // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode, on Feb 26 2021.\r\n this.FF['k61'] = 187; // = // FF 2.0\r\n this.FF['k59'] = 186; // ;\r\n this.FF['k173'] = 189; // -/_\r\n }\r\n}\r\n\r\nclass LanguageKeyMaps {\r\n [languageCode: string]: KeyMap;\r\n\r\n // // Here are some old legacy definitions that were no longer referenced but are likely related:\r\n // static _BaseLayoutEuro: {[code: string]: string} = {\r\n // 'se': '\\u00a71234567890+´~~~QWERTYUIOP\\u00c5\\u00a8\\'~~~ASDFGHJKL\\u00d6\\u00c4~~~~~ ` ~\r\n this['uk']['k192'] = 222; // ' @ => ' \"\r\n this['uk']['k222'] = 226; // # ~ => K_oE2 // I1504 - UK keyboard mixup #, \\\r\n this['uk']['k220'] = 220; // \\ | => \\ | // I1504 - UK keyboard mixup #, \\\r\n }\r\n}\r\n\r\nexport default class KeyMapping {\r\n static readonly browserMap: BrowserKeyMaps = new BrowserKeyMaps();\r\n static readonly languageMap: LanguageKeyMaps = new LanguageKeyMaps();\r\n\r\n private static _usCharCodes: KeyMap[];\r\n\r\n private constructor() {\r\n // Do not construct this class.\r\n }\r\n\r\n private static _usCodeInit() {\r\n var s0=new KeyMap(),s1=new KeyMap();\r\n\r\n s0['k192'] = 96;\r\n s0['k49'] = 49;\r\n s0['k50'] = 50;\r\n s0['k51'] = 51;\r\n s0['k52'] = 52;\r\n s0['k53'] = 53;\r\n s0['k54'] = 54;\r\n s0['k55'] = 55;\r\n s0['k56'] = 56;\r\n s0['k57'] = 57;\r\n s0['k48'] = 48;\r\n s0['k189'] = 45;\r\n s0['k187'] = 61;\r\n s0['k81'] = 113;\r\n s0['k87'] = 119;\r\n s0['k69'] = 101;\r\n s0['k82'] = 114;\r\n s0['k84'] = 116;\r\n s0['k89'] = 121;\r\n s0['k85'] = 117;\r\n s0['k73'] = 105;\r\n s0['k79'] = 111;\r\n s0['k80'] = 112;\r\n s0['k219'] = 91;\r\n s0['k221'] = 93;\r\n s0['k220'] = 92;\r\n s0['k65'] = 97;\r\n s0['k83'] = 115;\r\n s0['k68'] = 100;\r\n s0['k70'] = 102;\r\n s0['k71'] = 103;\r\n s0['k72'] = 104;\r\n s0['k74'] = 106;\r\n s0['k75'] = 107;\r\n s0['k76'] = 108;\r\n s0['k186'] = 59;\r\n s0['k222'] = 39;\r\n s0['k90'] = 122;\r\n s0['k88'] = 120;\r\n s0['k67'] = 99;\r\n s0['k86'] = 118;\r\n s0['k66'] = 98;\r\n s0['k78'] = 110;\r\n s0['k77'] = 109;\r\n s0['k188'] = 44;\r\n s0['k190'] = 46;\r\n s0['k191'] = 47;\r\n\r\n s1['k192'] = 126;\r\n s1['k49'] = 33;\r\n s1['k50'] = 64;\r\n s1['k51'] = 35;\r\n s1['k52'] = 36;\r\n s1['k53'] = 37;\r\n s1['k54'] = 94;\r\n s1['k55'] = 38;\r\n s1['k56'] = 42;\r\n s1['k57'] = 40;\r\n s1['k48'] = 41;\r\n s1['k189'] = 95;\r\n s1['k187'] = 43;\r\n s1['k81'] = 81;\r\n s1['k87'] = 87;\r\n s1['k69'] = 69;\r\n s1['k82'] = 82;\r\n s1['k84'] = 84;\r\n s1['k89'] = 89;\r\n s1['k85'] = 85;\r\n s1['k73'] = 73;\r\n s1['k79'] = 79;\r\n s1['k80'] = 80;\r\n s1['k219'] = 123;\r\n s1['k221'] = 125;\r\n s1['k220'] = 124;\r\n s1['k65'] = 65;\r\n s1['k83'] = 83;\r\n s1['k68'] = 68;\r\n s1['k70'] = 70;\r\n s1['k71'] = 71;\r\n s1['k72'] = 72;\r\n s1['k74'] = 74;\r\n s1['k75'] = 75;\r\n s1['k76'] = 76;\r\n s1['k186'] = 58;\r\n s1['k222'] = 34;\r\n s1['k90'] = 90;\r\n s1['k88'] = 88;\r\n s1['k67'] = 67;\r\n s1['k86'] = 86;\r\n s1['k66'] = 66;\r\n s1['k78'] = 78;\r\n s1['k77'] = 77;\r\n s1['k188'] = 60;\r\n s1['k190'] = 62;\r\n s1['k191'] = 63;\r\n\r\n KeyMapping._usCharCodes = [s0,s1];\r\n }\r\n\r\n /**\r\n * Function _USKeyCodeToCharCode\r\n * Scope Private\r\n * @param {Event} Levent KMW event object\r\n * @return {number} Character code\r\n * Description Translate keyboard codes to standard US layout codes\r\n */\r\n static _USKeyCodeToCharCode(Levent: KeyEvent | KeyEventSpec) {\r\n return KeyMapping.usCharCodes[Levent.Lmodifiers & 0x10 ? 1 : 0]['k'+Levent.Lcode];\r\n };\r\n\r\n public static get usCharCodes() {\r\n if(!KeyMapping._usCharCodes) {\r\n KeyMapping._usCodeInit();\r\n }\r\n\r\n return KeyMapping._usCharCodes;\r\n }\r\n}", + "/**\r\n * Function deepCopy\r\n * Scope Private\r\n * @param {Object} p object to copy\r\n * @return {Object} clone ('deep copy') of object\r\n * Description Makes an actual copy (not a reference) of an object, copying simple members,\r\n * arrays and member objects but not functions, so use with care!\r\n */\r\nexport default function deepCopy)>(p:T): T {\r\n // typeof undefined == 'undefined', ![] == false, !{} == false\r\n if(typeof p != 'object' || !p) {\r\n return p;\r\n } else {\r\n const clone = Array.isArray(p) ? [] : {};\r\n // For arrays, skips over sparse entries. Not that we use sparse arrays, but still.\r\n const keys = Object.keys(p);\r\n\r\n for(let key of keys) {\r\n // @ts-ignore\r\n if(p[key] !== undefined) {\r\n // @ts-ignore\r\n clone[key] = deepCopy(p[key]);\r\n }\r\n }\r\n return clone as T;\r\n }\r\n}", + "/**\r\n * This class provides an abstract version of com.keyman.Device that is core-friendly,\r\n * containing only the information needed by web-core for text processing use, devoid\r\n * of any direct references to the DOM.\r\n */\r\nexport class DeviceSpec {\r\n readonly browser: DeviceSpec.Browser;\r\n readonly formFactor: DeviceSpec.FormFactor;\r\n readonly OS: DeviceSpec.OperatingSystem;\r\n readonly touchable: boolean;\r\n\r\n constructor(browser: string, formFactor: string, OS: string, touchable: boolean) {\r\n switch(browser.toLowerCase() as DeviceSpec.Browser) {\r\n case DeviceSpec.Browser.Chrome:\r\n case DeviceSpec.Browser.Edge:\r\n case DeviceSpec.Browser.Firefox:\r\n case DeviceSpec.Browser.Native:\r\n case DeviceSpec.Browser.Opera:\r\n case DeviceSpec.Browser.Safari:\r\n this.browser = browser.toLowerCase() as DeviceSpec.Browser;\r\n break;\r\n default:\r\n this.browser = DeviceSpec.Browser.Other;\r\n }\r\n\r\n switch(formFactor.toLowerCase() as DeviceSpec.FormFactor) {\r\n case DeviceSpec.FormFactor.Desktop:\r\n case DeviceSpec.FormFactor.Phone:\r\n case DeviceSpec.FormFactor.Tablet:\r\n this.formFactor = formFactor.toLowerCase() as DeviceSpec.FormFactor;\r\n break;\r\n default:\r\n throw (\"Invalid form factor specified for device: \" + formFactor);\r\n }\r\n\r\n switch(OS.toLowerCase() as DeviceSpec.OperatingSystem) {\r\n case DeviceSpec.OperatingSystem.Windows.toLowerCase():\r\n case DeviceSpec.OperatingSystem.macOS.toLowerCase():\r\n case DeviceSpec.OperatingSystem.Linux.toLowerCase():\r\n case DeviceSpec.OperatingSystem.Android.toLowerCase():\r\n case DeviceSpec.OperatingSystem.iOS.toLowerCase():\r\n this.OS = OS.toLowerCase() as DeviceSpec.OperatingSystem;\r\n break;\r\n default:\r\n this.OS = DeviceSpec.OperatingSystem.Other;\r\n }\r\n\r\n this.touchable = touchable;\r\n }\r\n}\r\n\r\n// Namespaces these under DeviceSpec, as each is primarily used with it.\r\nexport namespace DeviceSpec {\r\n export enum Browser {\r\n Chrome = 'chrome',\r\n Edge = 'edge',\r\n Firefox = 'firefox',\r\n Native = 'native', // Used by embedded mode\r\n Opera = 'opera',\r\n Safari = 'safari',\r\n Other = 'other'\r\n }\r\n\r\n export enum OperatingSystem {\r\n Windows = 'windows',\r\n macOS = 'macosx',\r\n Linux = 'linux',\r\n Android = 'android',\r\n iOS = 'ios',\r\n Other = 'other'\r\n }\r\n\r\n export enum FormFactor {\r\n Desktop = 'desktop',\r\n Phone = 'phone',\r\n Tablet = 'tablet'\r\n }\r\n}\r\n\r\nexport function physicalKeyDeviceAlias(device: DeviceSpec) {\r\n return new DeviceSpec(device.browser, DeviceSpec.FormFactor.Desktop, device.OS, false);\r\n}\r\n\r\nexport default DeviceSpec;", + "\n// Generated by common/web/keyman-version/build.sh\n//\n// Note: does not use the 'default' keyword so that the export name is\n// correct when converted to a CommonJS module with `esbuild`.\nexport class KEYMAN_VERSION {\n static readonly VERSION = \"18.0.115\";\n static readonly VERSION_RELEASE =\"18.0\";\n static readonly VERSION_MAJOR = \"18\";\n static readonly VERSION_MINOR = \"0\";\n static readonly VERSION_PATCH = \"115\";\n static readonly TIER =\"alpha\";\n static readonly VERSION_TAG = \"-alpha\";\n static readonly VERSION_WITH_TAG = \"18.0.115-alpha\";\n static readonly VERSION_ENVIRONMENT = \"alpha\";\n static readonly VERSION_GIT_TAG = \"release@18.0.115-alpha\";\n}\n\n// Also provides it as a 'default' export.\nexport default KEYMAN_VERSION;\n \n", + "import KEYMAN_VERSION from \"@keymanapp/keyman-version\";\r\n\r\n// Dotted-decimal version\r\nexport default class Version {\r\n public static readonly CURRENT = new Version(KEYMAN_VERSION.VERSION_RELEASE);\r\n\r\n // Represents a default version value for keyboards compiled before this was compiled into keyboards.\r\n // The exact version is unknown at this point, but the value is \"good enough\" for what we need.\r\n public static readonly DEVELOPER_VERSION_FALLBACK = new Version([9, 0, 0]);\r\n\r\n // For 12.0, the old default behavior of adding missing keycaps to the default layers was removed,\r\n // as it results in unexpected, bug-like behavior for keyboard designers when it is unwanted.\r\n public static readonly NO_DEFAULT_KEYCAPS = new Version([12, 0]);\r\n\r\n public static readonly MAC_POSSIBLE_IPAD_ALIAS = new Version([10, 15]);\r\n\r\n private readonly components: number[]\r\n\r\n /**\r\n * Parses version information, preparing it for use in comparisons.\r\n * @param text Either a string representing a version number (ex: \"9.0.0\") or an array representing\r\n * its components (ex: [9, 0, 0]).\r\n */\r\n constructor(text: String | number[]) {\r\n // If a keyboard doesn't specify a version, use the DEVELOPER_VERSION_FALLBACK values.\r\n if(text === undefined || text === null) {\r\n this.components = [].concat(Version.DEVELOPER_VERSION_FALLBACK.components);\r\n return;\r\n }\r\n\r\n if(Array.isArray(text)) {\r\n let components = text as number[];\r\n if(components.length < 2) {\r\n throw new Error(\"Version string must have at least a major and minor component!\");\r\n } else {\r\n this.components = [].concat(components);\r\n return;\r\n }\r\n }\r\n\r\n // else, standard constructor path.\r\n let parts = text.split('.');\r\n let componentArray: number[] = [];\r\n\r\n if(parts.length < 2) {\r\n throw new Error(\"Version string must have at least a major and minor component!\");\r\n }\r\n\r\n for(let i=0; i < parts.length; i++) {\r\n let value = parseInt(parts[i], 10);\r\n if(isNaN(value)) {\r\n throw new Error(\"Version string components must be numerical!\");\r\n }\r\n\r\n componentArray.push(value);\r\n }\r\n\r\n this.components = componentArray;\r\n }\r\n\r\n get major(): number {\r\n return this.components[0];\r\n }\r\n\r\n get minor(): number {\r\n return this.components[1];\r\n }\r\n\r\n toString(): string {\r\n return this.components.join('.');\r\n }\r\n\r\n toJSON(): string {\r\n return this.toString();\r\n }\r\n\r\n equals(other: Version): boolean {\r\n return this.compareTo(other) == 0;\r\n }\r\n\r\n precedes(other: Version): boolean {\r\n return this.compareTo(other) < 0;\r\n }\r\n\r\n compareTo(other: Version): number {\r\n // If the version info depth differs, we need a flag to indicate which instance is shorter.\r\n var isShorter: boolean = this.components.length < other.components.length;\r\n var maxDepth: number = (this.components.length < other.components.length) ? this.components.length : other.components.length;\r\n\r\n var i: number;\r\n for(i = 0; i < maxDepth; i++) {\r\n let delta = this.components[i] - other.components[i];\r\n if(delta != 0) {\r\n return delta;\r\n }\r\n }\r\n\r\n var longList = isShorter ? other.components : this.components;\r\n do {\r\n if(longList[i] > 0) {\r\n return isShorter ? -1 : 1;\r\n }\r\n i++;\r\n } while (i < longList.length);\r\n\r\n // Equal.\r\n return 0;\r\n }\r\n}", + "/**\r\n * Returns the base global object available to the current JS platform.\r\n * - In browsers, returns `window`.\r\n * - In WebWorkers, returns `self`.\r\n * - In Node, returns `global`.\r\n */\r\nexport default function getGlobalObject(): typeof globalThis {\r\n // Evergreen browsers have started defining 'globalThis'.\r\n // Refer to https://devblogs.microsoft.com/typescript/announcing-typescript-3-4/#type-checking-for-globalthis\r\n // and its referenced polyfill. Said polyfill is very complex, so we opt for this far leaner variant.\r\n if(typeof globalThis != 'undefined') {\r\n return globalThis; // Not available in IE or older Edge versions\r\n // @ts-ignore (TS will throw errors for whatever platform we're not compiling for.)\r\n } else if(typeof window != 'undefined') {\r\n // @ts-ignore\r\n return window; // The browser-based classic\r\n // @ts-ignore\r\n } else if(typeof self != 'undefined') {\r\n // @ts-ignore\r\n return self; // WebWorker global\r\n } else {\r\n // Assumption - if neither of the above exist, we're in Node, for unit-testing.\r\n // Node doesn't have as many methods and properties as the other two, but what\r\n // matters for us is that it's the base global.\r\n //\r\n // Some other headless JS solutions use 'this' instead, but Node's enough for our needs.\r\n // @ts-ignore\r\n return (global as any) as typeof globalThis;\r\n }\r\n}", + "/***\r\n KeymanWeb 14.0\r\n Copyright 2020 SIL International\r\n***/\r\n\r\n\r\n/*\r\n * TODO: Remove this file as part of addressing https://github.com/keymanapp/keyman/issues/2492.\r\n */\r\n\r\ndeclare global {\r\n interface StringConstructor {\r\n kmwFromCharCode(cp0: number): string,\r\n _kmwFromCharCode(cp0: number): string,\r\n kmwEnableSupplementaryPlane(bEnable: boolean): void\r\n }\r\n\r\n interface String {\r\n kmwCharCodeAt(codePointIndex: number): number,\r\n kmwCharAt(codePointIndex: number) : string,\r\n kmwIndexOf(searchValue: string, fromIndex?: number) : number,\r\n kmwLastIndexOf(searchValue: string, fromIndex?: number) : number,\r\n kmwSlice(beginSlice: number, endSlice: number) : string,\r\n kmwSubstring(start: number, length: number) : string,\r\n kmwSubstr(start: number, length?: number) : string,\r\n kmwBMPSubstr(start: number, length?: number) : string,\r\n kmwLength(): number,\r\n kmwBMPLength(): number,\r\n kmwNextChar(codeUnitIndex: number): number,\r\n kmwBMPNextChar(codeUnitIndex: number): number,\r\n kmwPrevChar(codeUnitIndex: number): number,\r\n kmwBMPPrevChar(codeUnitIndex: number): number,\r\n kmwCodePointToCodeUnit(codePointIndex: number) : number,\r\n kmwBMPCodePointToCodeUnit(codePointIndex: number) : number,\r\n kmwCodeUnitToCodePoint(codeUnitIndex: number) : number,\r\n kmwBMPCodeUnitToCodePoint(codeUnitIndex: number) : number,\r\n _kmwCharCodeAt(codePointIndex: number): number,\r\n _kmwCharAt(codePointIndex: number) : string,\r\n _kmwIndexOf(searchValue: string, fromIndex?: number) : number,\r\n _kmwLastIndexOf(searchValue: string, fromIndex?: number) : number,\r\n _kmwSlice(beginSlice: number, endSlice: number) : string,\r\n _kmwSubstring(start: number, length?: number) : string,\r\n _kmwSubstr(start: number, length?: number) : string,\r\n _kmwLength(): number,\r\n _kmwNextChar(codeUnitIndex: number): number,\r\n _kmwPrevChar(codeUnitIndex: number): number,\r\n _kmwCodePointToCodeUnit(codePointIndex: number) : number,\r\n _kmwCodeUnitToCodePoint(codeUnitIndex: number) : number\r\n }\r\n}\r\n\r\nexport default function extendString() {\r\n /**\r\n * Constructs a string from one or more Unicode character codepoint values\r\n * passed as integer parameters.\r\n *\r\n * @param {number} cp0,... 1 or more Unicode codepoints, e.g. 0x0065, 0x10000\r\n * @return {string|null} The new String object.\r\n */\r\n String.kmwFromCharCode = function(cp0) {\r\n var chars = [], i;\r\n for (i = 0; i < arguments.length; i++) {\r\n var c = Number(arguments[i]);\r\n if (!isFinite(c) || c < 0 || c > 0x10FFFF || Math.floor(c) !== c) {\r\n throw new RangeError(\"Invalid code point \" + c);\r\n }\r\n if (c < 0x10000) {\r\n chars.push(c);\r\n } else {\r\n c -= 0x10000;\r\n chars.push((c >> 10) + 0xD800);\r\n chars.push((c % 0x400) + 0xDC00);\r\n }\r\n }\r\n return String.fromCharCode.apply(undefined, chars);\r\n }\r\n\r\n /**\r\n * Returns a number indicating the Unicode value of the character at the given\r\n * code point index, with support for supplementary plane characters.\r\n *\r\n * @param {number} codePointIndex The code point index into the string (not\r\n the code unit index) to return\r\n * @return {number} The Unicode character value\r\n */\r\n String.prototype.kmwCharCodeAt = function(codePointIndex) {\r\n var str = String(this);\r\n var codeUnitIndex = 0;\r\n\r\n if (codePointIndex < 0 || codePointIndex >= str.length) {\r\n return NaN;\r\n }\r\n\r\n for(var i = 0; i < codePointIndex; i++) {\r\n codeUnitIndex = str.kmwNextChar(codeUnitIndex);\r\n if(codeUnitIndex === null) return NaN;\r\n }\r\n\r\n var first = str.charCodeAt(codeUnitIndex);\r\n if (first >= 0xD800 && first <= 0xDBFF && str.length > codeUnitIndex + 1) {\r\n var second = str.charCodeAt(codeUnitIndex + 1);\r\n if (second >= 0xDC00 && second <= 0xDFFF) {\r\n return ((first - 0xD800) << 10) + (second - 0xDC00) + 0x10000;\r\n }\r\n }\r\n return first;\r\n }\r\n\r\n /**\r\n * Returns the code point index within the calling String object of the first occurrence\r\n * of the specified value, or -1 if not found.\r\n *\r\n * @param {string} searchValue The value to search for\r\n * @param {number} [fromIndex] Optional code point index to start searching from\r\n * @return {number} The code point index of the specified search value\r\n */\r\n String.prototype.kmwIndexOf = function(searchValue, fromIndex) {\r\n var str = String(this);\r\n var codeUnitIndex = str.indexOf(searchValue, fromIndex);\r\n\r\n if(codeUnitIndex < 0) {\r\n return codeUnitIndex;\r\n }\r\n\r\n var codePointIndex = 0;\r\n for(var i = 0; i !== null && i < codeUnitIndex; i = str.kmwNextChar(i)) codePointIndex++;\r\n return codePointIndex;\r\n }\r\n\r\n /**\r\n * Returns the code point index within the calling String object of the last occurrence\r\n * of the specified value, or -1 if not found.\r\n *\r\n * @param {string} searchValue The value to search for\r\n * @param {number} fromIndex Optional code point index to start searching from\r\n * @return {number} The code point index of the specified search value\r\n */\r\n String.prototype.kmwLastIndexOf = function(searchValue, fromIndex)\r\n {\r\n var str = String(this);\r\n var codeUnitIndex = str.lastIndexOf(searchValue, fromIndex);\r\n\r\n if(codeUnitIndex < 0) {\r\n return codeUnitIndex;\r\n }\r\n\r\n var codePointIndex = 0;\r\n for(var i = 0; i !== null && i < codeUnitIndex; i = str.kmwNextChar(i)) codePointIndex++;\r\n return codePointIndex;\r\n }\r\n\r\n /**\r\n * Returns the length of the string in code points, as opposed to code units.\r\n *\r\n * @return {number} The length of the string in code points\r\n */\r\n String.prototype.kmwLength = function() {\r\n var str = String(this);\r\n\r\n if(str.length == 0) return 0;\r\n\r\n for(var i = 0, codeUnitIndex = 0; codeUnitIndex !== null; i++)\r\n codeUnitIndex = str.kmwNextChar(codeUnitIndex);\r\n return i;\r\n }\r\n\r\n /**\r\n * Extracts a section of a string and returns a new string.\r\n *\r\n * @param {number} beginSlice The start code point index in the string to\r\n * extract from\r\n * @param {number} endSlice Optional end code point index in the string\r\n * to extract to\r\n * @return {string} The substring as selected by beginSlice and\r\n * endSlice\r\n */\r\n String.prototype.kmwSlice = function(beginSlice, endSlice) {\r\n var str = String(this);\r\n var beginSliceCodeUnit = str.kmwCodePointToCodeUnit(beginSlice);\r\n var endSliceCodeUnit = str.kmwCodePointToCodeUnit(endSlice);\r\n if(beginSliceCodeUnit === null || endSliceCodeUnit === null)\r\n return '';\r\n else\r\n return str.slice(beginSliceCodeUnit, endSliceCodeUnit);\r\n }\r\n\r\n /**\r\n * Returns the characters in a string beginning at the specified location through\r\n * the specified number of characters.\r\n *\r\n * @param {number} start The start code point index in the string to\r\n * extract from\r\n * @param {number=} length Optional length to extract\r\n * @return {string} The substring as selected by start and length\r\n */\r\n String.prototype.kmwSubstr = function(start, length?)\r\n {\r\n var str = String(this);\r\n if(start < 0)\r\n {\r\n start = str.kmwLength() + start;\r\n }\r\n if(start < 0) start = 0;\r\n var startCodeUnit = str.kmwCodePointToCodeUnit(start);\r\n var endCodeUnit = startCodeUnit;\r\n\r\n if(startCodeUnit === null) return '';\r\n\r\n if(arguments.length < 2) {\r\n endCodeUnit = str.length;\r\n } else {\r\n for(var i = 0; i < length; i++) endCodeUnit = str.kmwNextChar(endCodeUnit);\r\n }\r\n if(endCodeUnit === null)\r\n return str.substring(startCodeUnit);\r\n else\r\n return str.substring(startCodeUnit, endCodeUnit);\r\n }\r\n\r\n /**\r\n * Returns the characters in a string between two indexes into the string.\r\n *\r\n * @param {number} indexA The start code point index in the string to\r\n * extract from\r\n * @param {number} indexB The end code point index in the string to\r\n * extract to\r\n * @return {string} The substring as selected by indexA and indexB\r\n */\r\n String.prototype.kmwSubstring = function(indexA, indexB)\r\n {\r\n var str = String(this),indexACodeUnit,indexBCodeUnit;\r\n\r\n if(typeof(indexB) == 'undefined')\r\n {\r\n indexACodeUnit = str.kmwCodePointToCodeUnit(indexA);\r\n indexBCodeUnit = str.length;\r\n }\r\n else\r\n {\r\n if(indexA > indexB) { var c = indexA; indexA = indexB; indexB = c; }\r\n\r\n indexACodeUnit = str.kmwCodePointToCodeUnit(indexA);\r\n indexBCodeUnit = str.kmwCodePointToCodeUnit(indexB);\r\n }\r\n if(isNaN(indexACodeUnit) || indexACodeUnit === null) indexACodeUnit = 0;\r\n if(isNaN(indexBCodeUnit) || indexBCodeUnit === null) indexBCodeUnit = str.length;\r\n\r\n return str.substring(indexACodeUnit, indexBCodeUnit);\r\n }\r\n\r\n /*\r\n Helper functions\r\n */\r\n\r\n /**\r\n * Returns the code unit index for the next code point in the string, accounting for\r\n * supplementary pairs\r\n *\r\n * @param {number|null} codeUnitIndex The code unit position to increment\r\n * @return {number|null} The index of the next code point in the string,\r\n * in code units\r\n */\r\n String.prototype.kmwNextChar = function(codeUnitIndex) {\r\n var str = String(this);\r\n\r\n if(codeUnitIndex === null || codeUnitIndex < 0 || codeUnitIndex >= str.length - 1) {\r\n return null;\r\n }\r\n\r\n var first = str.charCodeAt(codeUnitIndex);\r\n if (first >= 0xD800 && first <= 0xDBFF && str.length > codeUnitIndex + 1) {\r\n var second = str.charCodeAt(codeUnitIndex + 1);\r\n if (second >= 0xDC00 && second <= 0xDFFF) {\r\n if(codeUnitIndex == str.length - 2) {\r\n return null;\r\n }\r\n return codeUnitIndex + 2;\r\n }\r\n }\r\n return codeUnitIndex + 1;\r\n }\r\n\r\n /**\r\n * Returns the code unit index for the previous code point in the string, accounting\r\n * for supplementary pairs\r\n *\r\n * @param {number|null} codeUnitIndex The code unit position to decrement\r\n * @return {number|null} The index of the previous code point in the\r\n * string, in code units\r\n */\r\n String.prototype.kmwPrevChar = function(codeUnitIndex) {\r\n var str = String(this);\r\n\r\n if(codeUnitIndex == null || codeUnitIndex <= 0 || codeUnitIndex > str.length) {\r\n return null;\r\n }\r\n\r\n var second = str.charCodeAt(codeUnitIndex - 1);\r\n if (second >= 0xDC00 && second <= 0xDFFF && codeUnitIndex > 1) {\r\n var first = str.charCodeAt(codeUnitIndex - 2);\r\n if(first >= 0xD800 && first <= 0xDBFF) {\r\n return codeUnitIndex - 2;\r\n }\r\n }\r\n return codeUnitIndex - 1;\r\n }\r\n\r\n /**\r\n * Returns the corresponding code unit index to the code point index passed\r\n *\r\n * @param {number|null} codePointIndex A code point index in the string\r\n * @return {number|null} The corresponding code unit index\r\n */\r\n String.prototype.kmwCodePointToCodeUnit = function(codePointIndex) {\r\n\r\n if(codePointIndex === null) return null;\r\n\r\n var str = String(this);\r\n var codeUnitIndex = 0;\r\n\r\n if(codePointIndex < 0) {\r\n codeUnitIndex = str.length;\r\n for(var i = 0; i > codePointIndex; i--)\r\n codeUnitIndex = str.kmwPrevChar(codeUnitIndex);\r\n return codeUnitIndex;\r\n }\r\n\r\n if(codePointIndex == str.kmwLength()) return str.length;\r\n\r\n for(var i = 0; i < codePointIndex; i++)\r\n codeUnitIndex = str.kmwNextChar(codeUnitIndex);\r\n return codeUnitIndex;\r\n }\r\n\r\n /**\r\n * Returns the corresponding code point index to the code unit index passed\r\n *\r\n * @param {number|null} codeUnitIndex A code unit index in the string\r\n * @return {number|null} The corresponding code point index\r\n */\r\n String.prototype.kmwCodeUnitToCodePoint = function(codeUnitIndex) {\r\n var str = String(this);\r\n\r\n if(codeUnitIndex === null)\r\n return null;\r\n else if(codeUnitIndex == 0)\r\n return 0;\r\n else if(codeUnitIndex < 0)\r\n return str.substr(codeUnitIndex).kmwLength();\r\n else\r\n return str.substr(0,codeUnitIndex).kmwLength();\r\n }\r\n\r\n /**\r\n * Returns the character at a the code point index passed\r\n *\r\n * @param {number} codePointIndex A code point index in the string\r\n * @return {string} The corresponding character\r\n */\r\n String.prototype.kmwCharAt = function(codePointIndex) {\r\n var str = String(this);\r\n\r\n if(codePointIndex >= 0) return str.kmwSubstr(codePointIndex,1); else return '';\r\n }\r\n\r\n /**\r\n * String prototype library extensions for basic plane characters,\r\n * to simplify enabling or disabling supplementary plane functionality (I3319)\r\n */\r\n\r\n /**\r\n * Returns the code unit index for the next code point in the string\r\n *\r\n * @param {number} codeUnitIndex A code point index in the string\r\n * @return {number|null} The corresponding character\r\n */\r\n String.prototype.kmwBMPNextChar = function(codeUnitIndex)\r\n {\r\n var str = String(this);\r\n if(codeUnitIndex < 0 || codeUnitIndex >= str.length - 1) {\r\n return null;\r\n }\r\n return codeUnitIndex + 1;\r\n }\r\n\r\n /**\r\n * Returns the code unit index for the previous code point in the string\r\n *\r\n * @param {number} codeUnitIndex A code unit index in the string\r\n * @return {number|null} The corresponding character\r\n */\r\n String.prototype.kmwBMPPrevChar = function(codeUnitIndex)\r\n {\r\n var str = String(this);\r\n\r\n if(codeUnitIndex <= 0 || codeUnitIndex > str.length) {\r\n return null;\r\n }\r\n return codeUnitIndex - 1;\r\n }\r\n\r\n /**\r\n * Returns the code unit index for a code point index\r\n *\r\n * @param {number} codePointIndex A code point index in the string\r\n * @return {number} The corresponding character\r\n */\r\n String.prototype.kmwBMPCodePointToCodeUnit = function(codePointIndex)\r\n {\r\n return codePointIndex;\r\n }\r\n\r\n /**\r\n * Returns the code point index for a code unit index\r\n *\r\n * @param {number} codeUnitIndex A code point index in the string\r\n * @return {number} The corresponding character\r\n */\r\n String.prototype.kmwBMPCodeUnitToCodePoint = function(codeUnitIndex)\r\n {\r\n return codeUnitIndex;\r\n }\r\n\r\n /**\r\n * Returns the length of a BMP string\r\n *\r\n * @return {number} The length in code points\r\n */\r\n String.prototype.kmwBMPLength = function()\r\n {\r\n var str = String(this);\r\n return str.length;\r\n }\r\n\r\n /**\r\n * Returns a substring\r\n *\r\n * @param {number} n\r\n * @param {number=} ln\r\n * @return {string}\r\n */\r\n String.prototype.kmwBMPSubstr = function(n,ln?)\r\n {\r\n var str=String(this);\r\n if(n > -1)\r\n return str.substr(n,ln);\r\n else\r\n return str.substr(str.length+n,-n);\r\n }\r\n\r\n /**\r\n * Enable or disable supplementary plane string handling\r\n *\r\n * @param {boolean} bEnable\r\n */\r\n String.kmwEnableSupplementaryPlane = function(bEnable)\r\n {\r\n var p=String.prototype;\r\n String._kmwFromCharCode = bEnable ? String.kmwFromCharCode : String.fromCharCode;\r\n p._kmwCharAt = bEnable ? p.kmwCharAt : p.charAt;\r\n p._kmwCharCodeAt = bEnable ? p.kmwCharCodeAt : p.charCodeAt;\r\n p._kmwIndexOf = bEnable ? p.kmwIndexOf :p.indexOf;\r\n p._kmwLastIndexOf = bEnable ? p.kmwLastIndexOf : p.lastIndexOf ;\r\n p._kmwSlice = bEnable ? p.kmwSlice : p.slice;\r\n p._kmwSubstring = bEnable ? p.kmwSubstring : p.substring;\r\n p._kmwSubstr = bEnable ? p.kmwSubstr : p.kmwBMPSubstr;\r\n p._kmwLength = bEnable ? p.kmwLength : p.kmwBMPLength;\r\n p._kmwNextChar = bEnable ? p.kmwNextChar : p.kmwBMPNextChar;\r\n p._kmwPrevChar = bEnable ? p.kmwPrevChar : p.kmwBMPPrevChar;\r\n p._kmwCodePointToCodeUnit = bEnable ? p.kmwCodePointToCodeUnit : p.kmwBMPCodePointToCodeUnit;\r\n p._kmwCodeUnitToCodePoint = bEnable ? p.kmwCodeUnitToCodePoint : p.kmwBMPCodeUnitToCodePoint;\r\n }\r\n\r\n // Ensure that _all_ String extensions are established, even if disabled by default.\r\n if(!String._kmwFromCharCode) {\r\n String.kmwEnableSupplementaryPlane(false);\r\n }\r\n}\r\n\r\n// For side-effect imports:\r\nextendString();", + "type ResolveSignature = (value: Type | PromiseLike) => void;\r\ntype RejectSignature = (reason?: any) => void;\r\n\r\nexport default class ManagedPromise {\r\n /**\r\n * Calling this function will fulfill the Promise represented by this class.\r\n */\r\n public get resolve(): ResolveSignature {\r\n return this._resolve;\r\n }\r\n\r\n /**\r\n * Calling this function will reject the Promise represented by this class.\r\n */\r\n public get reject(): RejectSignature {\r\n return this._reject;\r\n }\r\n\r\n protected _resolve: ResolveSignature;\r\n protected _reject: RejectSignature;\r\n\r\n private _isFulfilled: boolean = false;\r\n private _isRejected: boolean = false;\r\n\r\n /**\r\n * Indicates that the promise has been fulfilled; the underlying `resolve` function has\r\n * already been called and \"locked in\".\r\n */\r\n public get isFulfilled(): boolean {\r\n return this._isFulfilled;\r\n }\r\n\r\n /**\r\n * Indicates that the promise has been rejected; the underlying `reject` function has\r\n * already been called and \"locked in\".\r\n */\r\n public get isRejected(): boolean {\r\n return this._isRejected;\r\n }\r\n\r\n /**\r\n * Indicates that the promise itself has either been resolved or rejected. It may not be fully\r\n * settled if resolved or rejected with a \"thenable\" that has not yet fully resolved itself.\r\n */\r\n public get isResolved(): boolean {\r\n return this.isFulfilled || this.isRejected;\r\n }\r\n\r\n private _promise: Promise;\r\n\r\n constructor();\r\n constructor(executor: (resolve: ResolveSignature, reject: RejectSignature) => void);\r\n constructor(executor?: (resolve: ResolveSignature, reject: RejectSignature) => void) {\r\n this._promise = new Promise((resolve, reject) => {\r\n this._resolve = (value) => {\r\n this._isFulfilled = true;\r\n resolve(value);\r\n };\r\n\r\n this._reject = (reason) => {\r\n this._isRejected = true;\r\n reject(reason);\r\n };\r\n\r\n if(executor) {\r\n executor(this._resolve, this._reject);\r\n }\r\n });\r\n }\r\n\r\n // Cannot actually extend the Promise class in ES5; attempt to use it will throw errors.\r\n // So, we just implement a Promise-like interface.\r\n\r\n then(onfulfilled?: ((value: Type) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise {\r\n return this._promise.then(onfulfilled, onrejected);\r\n }\r\n\r\n catch(onrejected?: (reason: any) => TResult1 | PromiseLike): Promise {\r\n return this._promise.catch(onrejected);\r\n }\r\n\r\n finally(onfinally?: () => void): Promise {\r\n return this._promise.finally(onfinally);\r\n }\r\n\r\n // And for things that actually need to provide something typed to Promise... well...\r\n get corePromise(): Promise {\r\n return this._promise;\r\n }\r\n}", + "import ManagedPromise from \"./managedPromise.js\";\r\n\r\n/**\r\n * This class represents a cancelable timeout, wrapped in Promise form.\r\n *\r\n * It will resolve to `true` when the timer completes unless `resolve` or\r\n * `reject` is called earlier. Call `.resolve(false)` for early cancellation\r\n * or `.resolve(true)` to cancel the timer while resolving the Promise early.\r\n */\r\nexport default class TimeoutPromise extends ManagedPromise {\r\n private timerHandle: number | NodeJS.Timeout;\r\n constructor(timeoutInMillis: number) {\r\n // Helps marshal the internal timer handle to its member field despite being\r\n // initialized in a closure passed to `super`, which cannot access `this`.\r\n let timerHandleCapture: (number | NodeJS.Timeout) = null;\r\n\r\n super((resolve) => {\r\n const timerId = setTimeout(() => {\r\n if(!this.isResolved) {\r\n resolve(true)\r\n }\r\n }, timeoutInMillis);\r\n\r\n // Forwards the timer handle outside of the closure.\r\n timerHandleCapture = timerId;\r\n });\r\n\r\n // \"Lands\" the timer handle in its final destination.\r\n this.timerHandle = timerHandleCapture;\r\n\r\n const resolve = this._resolve;\r\n this._resolve = (val) => {\r\n // b/c of the mismatch between the return types of DOM's window.setTimeout & Node's version\r\n clearTimeout(this.timerHandle as any);\r\n resolve(val);\r\n }\r\n\r\n // Not a standard use-case; it's just here to ensure that the timeout resource is cleaned up\r\n // even if `reject` gets used for whatever reason.\r\n /* c8 ignore next 6 */\r\n const reject = this._reject;\r\n this._reject = (val) => {\r\n // b/c of the mismatch between the return types of DOM's window.setTimeout & Node's version\r\n clearTimeout(this.timerHandle as any);\r\n reject(val);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * A simplified, but non-cancelable, version of `TimeoutPromise`. Returns a simple,\r\n * Promise that resolves after the specified timeout duration.\r\n */\r\nexport const timedPromise = (time: number) => {\r\n const promise = new TimeoutPromise(time);\r\n return promise.corePromise;\r\n}", + "/***\r\n KeymanWeb 10.0\r\n Copyright 2017 SIL International\r\n***/\r\n\r\nimport { Version, deepCopy } from \"@keymanapp/web-utils\";\r\nimport { ModifierKeyConstants, TouchLayout } from \"@keymanapp/common-types\";\r\n\r\nimport LayoutFormFactorSpec = TouchLayout.TouchLayoutPlatform;\r\nimport LayoutLayerBase = TouchLayout.TouchLayoutLayer;\r\nexport type LayoutRow = TouchLayout.TouchLayoutRow;\r\nexport type LayoutKey = TouchLayout.TouchLayoutKey;\r\nexport type LayoutSubKey = TouchLayout.TouchLayoutSubKey;\r\n\r\nimport ButtonClasses = TouchLayout.TouchLayoutKeySp;\r\n\r\nexport { ButtonClasses };\r\n\r\nimport Codes from \"../codes.js\";\r\nimport type Keyboard from \"./keyboard.js\";\r\n\r\nexport interface EncodedVisualKeyboard {\r\n /** Represents CSS font styling to use for VisualKeyboard text */\r\n F: string;\r\n /** Should there be a 102nd key? */\r\n K102?: boolean,\r\n /**\r\n * Keyboard Layer Specification: an object-based map of layer name to the keycaps for its\r\n * 65 keys. The 65 keys are ordered from left to right, then top to bottom.\r\n *\r\n * The key ID corresponding to each index of the array is specified within `Codes.dfltCodes`.\r\n * Entries corresponding to `K_*` in `Codes.dfltCodes` are reserved for future use.\r\n */\r\n KLS?: {[layerName: string]: string[]},\r\n /**\r\n * @deprecated\r\n * The older form for data in KLS - defines keycaps for 'default' keys, then 'shift' keys,\r\n * in a single concatenated array.\r\n */\r\n BK?: string[];\r\n}\r\n\r\n// The following types provide type definitions for the full JSON format we use for visual keyboard definitions.\r\nexport type ButtonClass = 0 | 1 | 2 | 3 | 4 | /*5 | 6 | 7 |*/ 8 | 9 | 10;\r\n\r\nexport interface LayoutLayer extends LayoutLayerBase {\r\n // Post-processing elements.\r\n shiftKey?: LayoutKey,\r\n capsKey?: LayoutKey,\r\n numKey?: LayoutKey,\r\n scrollKey?: LayoutKey,\r\n aligned?: boolean,\r\n nextlayer?: string\r\n};\r\nexport interface LayoutFormFactor extends LayoutFormFactorSpec {\r\n // To facilitate those post-processing elements.\r\n layer: LayoutLayer[]\r\n};\r\n\r\nexport type LayoutSpec = {\r\n \"desktop\"?: LayoutFormFactorSpec,\r\n \"phone\"?: LayoutFormFactorSpec,\r\n \"tablet\"?: LayoutFormFactorSpec\r\n}\r\n\r\nconst KEY_102_WIDTH = 200;\r\n\r\n// This class manages default layout construction for consumption by OSKs without a specified layout.\r\nexport class Layouts {\r\n static readonly dfltCodes: ReadonlyArray = [\r\n \"K_BKQUOTE\",\"K_1\",\"K_2\",\"K_3\",\"K_4\",\"K_5\",\"K_6\",\"K_7\",\"K_8\",\"K_9\",\"K_0\",\r\n \"K_HYPHEN\",\"K_EQUAL\",\"K_*\",\"K_*\",\"K_*\",\"K_Q\",\"K_W\",\"K_E\",\"K_R\",\"K_T\",\r\n \"K_Y\",\"K_U\",\"K_I\",\"K_O\",\"K_P\",\"K_LBRKT\",\"K_RBRKT\",\"K_BKSLASH\",\"K_*\",\r\n \"K_*\",\"K_*\",\"K_A\",\"K_S\",\"K_D\",\"K_F\",\"K_G\",\"K_H\",\"K_J\",\"K_K\",\"K_L\",\r\n \"K_COLON\",\"K_QUOTE\",\"K_*\",\"K_*\",\"K_*\",\"K_*\",\"K_*\",\"K_oE2\",\r\n \"K_Z\",\"K_X\",\"K_C\",\"K_V\",\"K_B\",\"K_N\",\"K_M\",\"K_COMMA\",\"K_PERIOD\",\r\n \"K_SLASH\",\"K_*\",\"K_*\",\"K_*\",\"K_*\",\"K_*\",\"K_SPACE\"\r\n ];\r\n\r\n static readonly dfltText='`1234567890-=\\xA7~~qwertyuiop[]\\\\~~~asdfghjkl;\\'~~~~~?zxcvbnm,./~~~~~ '\r\n +'~!@#$%^&*()_+\\xA7~~QWERTYUIOP{}\\\\~~~ASDFGHJKL:\"~~~~~?ZXCVBNM<>?~~~~~ ';\r\n\r\n // The function baked into keyboards by the current Web compiler creates an\r\n // array of single-char strings for BK. Refer to\r\n // developer/src/kmc-kmn/src/kmw-compiler/visual-keyboard-compiler.ts.\r\n static readonly DEFAULT_RAW_SPEC = {'F':'Tahoma', 'BK': Layouts.dfltText.split('')} as const;\r\n\r\n static modifierSpecials = {\r\n 'leftalt': '*LAlt*',\r\n 'rightalt': '*RAlt*',\r\n 'alt': '*Alt*',\r\n 'leftctrl': '*LCtrl*',\r\n 'rightctrl': '*RCtrl*',\r\n 'ctrl': '*Ctrl*',\r\n 'ctrl-alt': '*AltGr*',\r\n 'leftctrl-leftalt': '*LAltCtrl*',\r\n 'rightctrl-rightalt': '*RAltCtrl*',\r\n 'leftctrl-leftalt-shift': '*LAltCtrlShift*',\r\n 'rightctrl-rightalt-shift': '*RAltCtrlShift*',\r\n 'shift': '*Shift*',\r\n 'shift-alt': '*AltShift*',\r\n 'shift-ctrl': '*CtrlShift*',\r\n 'shift-ctrl-alt': '*AltCtrlShift*',\r\n 'leftalt-shift': '*LAltShift*',\r\n 'rightalt-shift': '*RAltShift*',\r\n 'leftctrl-shift': '*LCtrlShift*',\r\n 'rightctrl-shift': '*RCtrlShift*'\r\n } as const;\r\n\r\n /**\r\n * Build a default layout for keyboards with no explicit layout\r\n *\r\n * @param {Object} PVK raw specifications\r\n * @param {Keyboard} keyboard keyboard object (as loaded)\r\n * @param {string} formFactor (really utils.FormFactor)\r\n * @return {LayoutFormFactor}\r\n */\r\n static buildDefaultLayout(PVK: EncodedVisualKeyboard, keyboard: Keyboard, formFactor: string): LayoutFormFactor {\r\n // Build a layout using the default for the device\r\n let layoutType = formFactor as keyof TouchLayout.TouchLayoutFile;\r\n\r\n if(typeof Layouts.dfltLayout[layoutType] != 'object') {\r\n layoutType = 'desktop';\r\n }\r\n\r\n let kbdBitmask = Codes.modifierBitmasks['NON_CHIRAL'];\r\n // An unfortunate dependency there. Should probably also set a version within web-core for use.\r\n let kbdDevVersion = Version.CURRENT;\r\n if(keyboard) {\r\n kbdBitmask = keyboard.modifierBitmask;\r\n kbdDevVersion = keyboard.compilerVersion;\r\n }\r\n\r\n if(!PVK) {\r\n PVK = this.DEFAULT_RAW_SPEC;\r\n }\r\n\r\n // Clone the default layout object for this device\r\n var layout: LayoutFormFactorSpec = deepCopy(Layouts.dfltLayout[layoutType]);\r\n\r\n var n,layers=layout['layer'] as LayoutLayer[], keyLabels: EncodedVisualKeyboard['KLS'] = PVK['KLS'], key102=PVK['K102'];\r\n var i, j, k, rows: LayoutRow[], key: LayoutKey, keys: LayoutKey[];\r\n var chiral: boolean = (kbdBitmask & Codes.modifierBitmasks.IS_CHIRAL) != 0;\r\n\r\n if(PVK['F']) {\r\n // The KeymanWeb compiler generates a string of the format `[italic ][bold ] 1em \"\"`\r\n // We will ignore the bold, italic and font size spec\r\n let legacyFontSpec = /^(?:(?:italic|bold) )* *[0-9.eE-]+(?:[a-z]+) \"(.+)\"$/.exec(PVK['F']);\r\n if(legacyFontSpec) {\r\n layout.font = legacyFontSpec[1];\r\n }\r\n }\r\n\r\n var kmw10Plus = !(typeof keyLabels == 'undefined' || !keyLabels);\r\n if(!kmw10Plus) {\r\n // Save the processed key label information to the keyboard's general data.\r\n // Makes things more efficient elsewhere and for reloading after keyboard swaps.\r\n keyLabels = PVK['KLS'] = Layouts.processLegacyDefinitions(PVK['BK']);\r\n }\r\n\r\n // *** Step 1: instantiate the layer objects. ***\r\n\r\n // Get the list of valid layers, enforcing that the 'default' layer must be the first one processed.\r\n var validIdList = Object.getOwnPropertyNames(keyLabels), invalidIdList: string[] = [];\r\n validIdList.splice(validIdList.indexOf('default'), 1);\r\n validIdList = [ 'default' ].concat(validIdList);\r\n\r\n // Automatic AltGr emulation if the 'leftctrl-leftalt' layer is otherwise undefined.\r\n if(keyboard && keyboard.emulatesAltGr) {\r\n // We insert only the layers that need to be emulated.\r\n if((validIdList.indexOf('leftctrl-leftalt') == -1) && validIdList.indexOf('rightalt') != -1) {\r\n validIdList.push('leftctrl-leftalt');\r\n keyLabels['leftctrl-leftalt'] = keyLabels['rightalt'];\r\n }\r\n\r\n if((validIdList.indexOf('leftctrl-leftalt-shift') == -1) && validIdList.indexOf('rightalt-shift') != -1) {\r\n validIdList.push('leftctrl-leftalt-shift');\r\n keyLabels['leftctrl-leftalt-shift'] = keyLabels['rightalt-shift'];\r\n }\r\n }\r\n\r\n // If there is no predefined layout, even touch layouts will follow the desktop's\r\n // setting for the displayUnderlying flag. As the desktop layout uses a different\r\n // format for its layout spec, that's found at the field referenced below.\r\n layout[\"displayUnderlying\"] = keyboard ? !!keyboard.scriptObject['KDU'] : false;\r\n\r\n // For desktop devices, we must create all layers, even if invalid.\r\n if(formFactor == 'desktop') {\r\n invalidIdList = Layouts.generateLayerIds(chiral);\r\n\r\n // Filter out all ids considered valid. (We also don't want duplicates in the following list...)\r\n for(n=0; n 0) {\r\n layers[n]=deepCopy(layers[0]);\r\n }\r\n layers[n]['id']=idList[n];\r\n layers[n]['nextlayer']=idList[n]; // This would only be different for a dynamic keyboard\r\n\r\n // Extraced into a helper method to improve readability.\r\n Layouts.formatDefaultLayer(layers[n], chiral, formFactor, !!key102);\r\n }\r\n\r\n // *** Step 2: Layer objects now exist; time to fill them with the appropriate key labels and key styles ***\r\n for(n=0; n= 0 && kx < layerSpec.length) key['text']=layerSpec[kx];\r\n }\r\n\r\n // Legacy (pre 12.0) behavior: fall back to US English keycap text as default for the base two layers\r\n // if a key cap is not otherwise defined. (Any intentional 'ghost' keys must be explicitly defined.)\r\n if(isDefault && kbdDevVersion.precedes(Version.NO_DEFAULT_KEYCAPS)) {\r\n if(key['id'] != 'K_SPACE' && kx+65 * isShift < Layouts.dfltText.length && key['text'] !== null) {\r\n key['text'] = key['text'] || Layouts.dfltText[kx+65*isShift];\r\n }\r\n }\r\n }\r\n\r\n // Leave any unmarked key caps as null strings\r\n if(key['text'] !== null) {\r\n key['text'] = key['text'] || '';\r\n }\r\n\r\n // Detect important tracking keys.\r\n switch(key['id']) {\r\n case \"K_SHIFT\":\r\n shiftKey=key;\r\n break;\r\n case \"K_CAPS\":\r\n capsKey=key;\r\n break;\r\n case \"K_NUMLOCK\":\r\n numKey=key;\r\n break;\r\n case \"K_SCROLL\":\r\n scrollKey=key;\r\n break;\r\n }\r\n\r\n // Remove pop-up shift keys referencing invalid layers (Build 349)\r\n if(key['sk'] != null) {\r\n for(k=0; k 0 && shiftKey != null) {\r\n shiftKey['sp']=ButtonClasses.specialActive;\r\n shiftKey['sk']=null;\r\n shiftKey['text'] = Layouts.modifierSpecials[layerId] ?? \"*Shift*\";\r\n }\r\n }\r\n }\r\n\r\n return layout;\r\n }\r\n\r\n /**\r\n * Function getLayerId\r\n * Scope Private\r\n * @param {number} m shift modifier code\r\n * @return {string} layer string from shift modifier code (desktop keyboards)\r\n * Description Get name of layer from code, where the modifer order is determined by ascending bit-flag value.\r\n */\r\n static getLayerId(m: number): string {\r\n var s='';\r\n if(m == 0) {\r\n return 'default';\r\n } else {\r\n if(m & ModifierKeyConstants.LCTRLFLAG) {\r\n s = (s.length > 0 ? s + '-' : '') + 'leftctrl';\r\n }\r\n if(m & ModifierKeyConstants.RCTRLFLAG) {\r\n s = (s.length > 0 ? s + '-' : '') + 'rightctrl';\r\n }\r\n if(m & ModifierKeyConstants.LALTFLAG) {\r\n s = (s.length > 0 ? s + '-' : '') + 'leftalt';\r\n }\r\n if(m & ModifierKeyConstants.RALTFLAG) {\r\n s = (s.length > 0 ? s + '-' : '') + 'rightalt';\r\n }\r\n if(m & ModifierKeyConstants.K_SHIFTFLAG) {\r\n s = (s.length > 0 ? s + '-' : '') + 'shift';\r\n }\r\n if(m & ModifierKeyConstants.K_CTRLFLAG) {\r\n s = (s.length > 0 ? s + '-' : '') + 'ctrl';\r\n }\r\n if(m & ModifierKeyConstants.K_ALTFLAG) {\r\n s = (s.length > 0 ? s + '-' : '') + 'alt';\r\n }\r\n return s;\r\n }\r\n }\r\n\r\n /**\r\n * Generates a list of potential layer ids for the specified chirality mode.\r\n *\r\n * @param {boolean} chiral // Does the keyboard use chiral modifiers or not?\r\n */\r\n static generateLayerIds(chiral: boolean): string[] {\r\n var layerCnt, offset;\r\n\r\n if(chiral) {\r\n layerCnt=32;\r\n offset=0x01;\r\n } else {\r\n layerCnt=8;\r\n offset=0x10;\r\n }\r\n\r\n var layerIds = [];\r\n\r\n for(var i=0; i < layerCnt; i++) {\r\n layerIds.push(Layouts.getLayerId(i * offset));\r\n }\r\n\r\n return layerIds;\r\n }\r\n\r\n /**\r\n * Sets a formatting property for the modifier keys when constructing a default layout for a keyboard.\r\n *\r\n * @param {Object} layer // One layer specification\r\n * @param {boolean} chiral // Whether or not the keyboard uses chiral modifier information.\r\n * @param {string} formFactor // The form factor of the device the layout is being constructed for.\r\n * @param {boolean} key102 // Whether or not the extended key 102 should be hidden.\r\n */\r\n static formatDefaultLayer(layer: LayoutLayer, chiral: boolean, formFactor: string, key102: boolean) {\r\n var layerId = layer['id'];\r\n\r\n // Correct appearance of state-dependent modifier keys according to group\r\n for(var i=0; i(rawObj: RawType, defaults: Type) {\r\n const proto = Object.getPrototypeOf(defaults);\r\n\r\n for(let prop in defaults) {\r\n if(!rawObj.hasOwnProperty(prop)) {\r\n let descriptor = Object.getOwnPropertyDescriptor(proto, prop);\r\n if(descriptor) {\r\n // It's a computed property! Copy the descriptor onto the key's object.\r\n Object.defineProperty(rawObj, prop, descriptor);\r\n } else {\r\n // Type 'Extract' cannot be used to index type\r\n // 'RawType'. (ts2536)\r\n // @ts-ignore\r\n // the whole point of this function is to polyfill `rawObj` so that it's\r\n // duck-typable to `Type`.\r\n rawObj[prop] = defaults[prop];\r\n }\r\n }\r\n }\r\n\r\n return rawObj as Type;\r\n}\r\n\r\nexport class ActiveKeyBase {\r\n static readonly DEFAULT_PAD=15; // Padding to left of key, in virtual units\r\n static readonly DEFAULT_RIGHT_MARGIN=15; // Padding to right of right-most key, in virtual units\r\n static readonly DEFAULT_KEY_WIDTH=100; // Width of a key, if not specified, in virtual units\r\n\r\n // Defines key defaults\r\n static readonly DEFAULT_KEY = {\r\n text: '',\r\n width: ActiveKeyBase.DEFAULT_KEY_WIDTH,\r\n sp: ButtonClasses.normal,\r\n pad: ActiveKeyBase.DEFAULT_PAD\r\n };\r\n\r\n /** WARNING - DO NOT USE DIRECTLY outside of keyman/engine/keyboard! */\r\n id: TouchLayout.TouchLayoutKeyId;\r\n text: string;\r\n hint?: string;\r\n hintSrc?: TouchLayout.TouchLayoutSubKey | TouchLayout.TouchLayoutKey;\r\n\r\n font?: string;\r\n fontsize?: string;\r\n\r\n // These are fine.\r\n width?: number;\r\n pad?: number;\r\n\r\n layer: string;\r\n displayLayer: string;\r\n nextlayer: string;\r\n sp?: TouchLayoutKeySp;\r\n\r\n private _baseKeyEvent: KeyEvent | (() => KeyEvent);\r\n isMnemonic: boolean = false;\r\n\r\n /**\r\n * Only available on subkeys, but we don't distinguish between base keys and subkeys\r\n * at this level yet in KMW.\r\n */\r\n default?: boolean;\r\n\r\n proportionalPad: number;\r\n proportionalX: number;\r\n proportionalWidth: number;\r\n\r\n // While they're only valid on ActiveKey, spec'ing them here makes references more concise within the OSK.\r\n sk?: ActiveSubKey[];\r\n multitap?: ActiveSubKey[];\r\n flick?: TouchLayout.TouchLayoutFlick;\r\n\r\n // Keeping things simple here, as this was added LATE in 14.0 beta.\r\n // Could definitely extend in the future to instead return an object\r\n // that denotes the 'nature' of the key.\r\n // - isUnicode\r\n // - isHardwareKey\r\n // - etc.\r\n\r\n // Reference for the terminology in the comments below:\r\n // https://help.keyman.com/developer/current-version/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty\r\n\r\n /**\r\n * Matches the key code as set within Keyman Developer for the layout.\r\n * For example, K_R or U_0020. Denotes either physical keys or virtual keys with custom output,\r\n * with no additional metadata like layer or active modifiers.\r\n *\r\n * Is used to determine the keycode for input events, rule-matching, and keystroke processing.\r\n */\r\n @Enumerable\r\n public get baseKeyID(): string {\r\n if(typeof this.id === 'undefined') {\r\n return undefined;\r\n }\r\n\r\n return this.id;\r\n }\r\n\r\n @Enumerable\r\n public get isPadding(): boolean {\r\n // Does not include 9 (class: blank) as that may be an intentional 'catch' for misplaced\r\n // keystrokes.\r\n return this.sp == ButtonClasses.spacer; // Button class: hidden.\r\n }\r\n\r\n /**\r\n * A unique identifier based on both the key ID & the 'desktop layer' to be used for the key.\r\n *\r\n * Allows diambiguation of scenarios where the same key ID is used twice within a layer, but\r\n * with different innate modifiers. (Refer to https://github.com/keymanapp/keyman/issues/4617)\r\n * The 'desktop layer' may be omitted if it matches the key's display layer.\r\n *\r\n * Examples, given a 'default' display layer, matching keys to Keyman keyboard language:\r\n *\r\n * ```\r\n * \"K_Q\"\r\n * + [K_Q]\r\n * \"K_Q+shift\"\r\n * + [K_Q SHIFT]\r\n * ```\r\n *\r\n * Useful when the active layer of an input-event is already known.\r\n */\r\n @Enumerable\r\n public get coreID(): string {\r\n if(typeof this.id === 'undefined') {\r\n return undefined;\r\n }\r\n\r\n let baseID = this.id || '';\r\n\r\n if(this.displayLayer != this.layer) {\r\n baseID = baseID + '+' + this.layer;\r\n }\r\n\r\n return baseID;\r\n }\r\n\r\n /**\r\n * A keyboard-unique identifier to be used for any display elements representing this key\r\n * in user interfaces and/or on-screen keyboards.\r\n *\r\n * Distinguishes between otherwise-identical keys on different layers of an OSK.\r\n * Includes identifying information about the key's display layer.\r\n *\r\n * Examples, given a 'default' display layer, matching keys to Keyman keyboard language:\r\n *\r\n * ```\r\n * \"default-K_Q\"\r\n * + [K_Q]\r\n * \"default-K_Q+shift\"\r\n * + [K_Q SHIFT]\r\n * ```\r\n *\r\n * Useful when only the active keyboard is known about an input event.\r\n */\r\n @Enumerable\r\n public get elementID(): string {\r\n if(typeof this.id === 'undefined') {\r\n return undefined;\r\n }\r\n\r\n return this.displayLayer + '-' + this.coreID;\r\n }\r\n\r\n @Enumerable\r\n public get baseKeyEvent(): KeyEvent {\r\n let val = this._baseKeyEvent;\r\n if(typeof val == 'function') {\r\n val = val();\r\n }\r\n return new KeyEvent(val);\r\n }\r\n\r\n constructor();\r\n constructor(spec: LayoutKey | LayoutSubKey, layout: ActiveLayout, displayLayer: string);\r\n constructor(spec?: LayoutKey | LayoutSubKey, layout?: ActiveLayout, displayLayer?: string) {\r\n // First things first: this class's fields are designed to match that of the spec.\r\n Object.assign(this, spec);\r\n\r\n if(!this.text && typeof this.id == 'string') {\r\n this.text = ActiveKey.unicodeIDToText(this.id);\r\n }\r\n\r\n this.displayLayer = displayLayer;\r\n this.layer = this.layer || displayLayer;\r\n\r\n // Compute the key's base KeyEvent properties for use in future event generation\r\n // It's actually somewhat expensive to do this at the start, so we do a lazy-init.\r\n this._baseKeyEvent = () => this.constructBaseKeyEvent(layout, displayLayer);\r\n }\r\n\r\n /**\r\n * Converts key IDs of the U_* form to their corresponding UTF-16 text.\r\n * If an ID not matching the pattern is received, returns null.\r\n * @param id\r\n * @returns\r\n */\r\n static unicodeIDToText(id: string, errorCallback?: (codeAsString: string) => void) {\r\n if(!id || id.substring(0,2) != 'U_') {\r\n return null;\r\n }\r\n\r\n let result = '';\r\n const codePoints = id.substring(2).split('_');\r\n for(let codePoint of codePoints) {\r\n const codePointValue = parseInt(codePoint, 16);\r\n if (((0x0 <= codePointValue) && (codePointValue <= 0x1F)) ||\r\n ((0x80 <= codePointValue) && (codePointValue <= 0x9F)) ||\r\n isNaN(codePointValue)) {\r\n if(errorCallback) {\r\n errorCallback(codePoint);\r\n }\r\n continue;\r\n } else {\r\n // String.fromCharCode() is inadequate to handle the entire range of Unicode\r\n // Someday after upgrading to ES2015, can use String.fromCodePoint()\r\n result += String.kmwFromCharCode(codePointValue);\r\n }\r\n }\r\n return result ? result : null;\r\n }\r\n\r\n static sanitize(rawKey: LayoutKey) {\r\n // In older versions of KeymanWeb, we specified these three properties as strings...\r\n // despite them holding a numerical value.\r\n if(typeof rawKey.width == 'string') {\r\n rawKey.width = parseInt(rawKey.width, 10);\r\n }\r\n // Handles NaN cases as well as 'set to 0' cases; both are intentional here.\r\n rawKey.width ||= ActiveKey.DEFAULT_KEY_WIDTH;\r\n\r\n if(typeof rawKey.pad == 'string') {\r\n rawKey.pad = parseInt(rawKey.pad, 10);\r\n }\r\n rawKey.pad ||= ActiveKey.DEFAULT_PAD;\r\n\r\n if(typeof rawKey.sp == 'string') {\r\n rawKey.sp = Number.parseInt(rawKey.sp, 10) as ButtonClass;\r\n }\r\n rawKey.sp ||= ActiveKey.DEFAULT_KEY.sp; // The default button class.\r\n\r\n // And now for generalized type validation. -----------------------------------------\r\n\r\n // WARNING: Object.values and Object.entries is NOT polyfilled by es6-shim and thus\r\n // is NOT available within the Android app in extremely early APIs.\r\n // Object.entries requires Android 54.\r\n\r\n for(const key of Object.keys(KeyTypesOfKeyMap)) {\r\n const value = KeyTypesOfKeyMap[key as keyof typeof KeyTypesOfKeyMap];\r\n switch(value) {\r\n case 'subkeys':\r\n const arr = rawKey[key as 'sk' | 'multitap'] as LayoutSubKey[];\r\n if(arr === undefined) {\r\n // `delete` has a small yet significant performance cost; bypass it.\r\n break;\r\n } else if(!Array.isArray(arr)) {\r\n delete rawKey[key as 'sk' | 'multitap'];\r\n } else {\r\n for(let i=0; i < arr.length; i++) {\r\n const sk = arr[i];\r\n if(typeof sk != 'object') {\r\n arr.splice(i--, 1);\r\n } else {\r\n ActiveKey.sanitize(sk);\r\n }\r\n }\r\n }\r\n break;\r\n case 'flicks':\r\n const flickObj = rawKey[key as 'flick'];\r\n if(flickObj === undefined) {\r\n // `delete` has a small yet significant performance cost; bypass it.\r\n break;\r\n } else if(typeof flickObj != 'object') {\r\n delete rawKey[key as 'flick'];\r\n } else {\r\n for(const flickKey of KeyTypesOfFlickList) {\r\n const sk = flickObj[flickKey];\r\n if(sk === undefined) {\r\n break;\r\n } else if(typeof sk != 'object') {\r\n delete flickObj[flickKey];\r\n } else {\r\n ActiveKey.sanitize(sk);\r\n }\r\n }\r\n }\r\n break;\r\n default:\r\n const prop = rawKey[key as keyof (LayoutKey | LayoutSubKey)];\r\n if(prop !== undefined && typeof prop != value) {\r\n delete rawKey[key as keyof (LayoutKey | LayoutSubKey)];\r\n }\r\n }\r\n }\r\n\r\n rawKey.text ||= ActiveKey.DEFAULT_KEY.text;\r\n }\r\n\r\n @Enumerable\r\n private constructBaseKeyEvent(layout: ActiveLayout, displayLayer: string) {\r\n // Get key name and keyboard shift state (needed only for default layouts and physical keyboard handling)\r\n // Note - virtual keys should be treated case-insensitive, so we force uppercasing here.\r\n let layer = this.layer || displayLayer || '';\r\n let keyName= this.id ? this.id.toUpperCase() : null;\r\n\r\n // Start: mirrors _GetKeyEventProperties\r\n\r\n // First check the virtual key, and process shift, control, alt or function keys\r\n let props: KeyEventSpec = {\r\n // Override key shift state if specified for key in layout (corrected for popup keys KMEW-93)\r\n Lmodifiers: Codes.getModifierState(layer),\r\n Lstates: Codes.getStateFromLayer(layer),\r\n Lcode: keyName ? Codes.keyCodes[keyName] : 0,\r\n LisVirtualKey: true,\r\n vkCode: 0,\r\n kName: keyName,\r\n kLayer: layer,\r\n kbdLayer: displayLayer,\r\n kNextLayer: this.nextlayer,\r\n device: null,\r\n isSynthetic: true\r\n };\r\n\r\n let Lkc: KeyEvent = new KeyEvent(props);\r\n\r\n if(layout.keyboard) {\r\n let keyboard = layout.keyboard;\r\n\r\n // Include *limited* support for mnemonic keyboards (Sept 2012)\r\n // If a touch layout has been defined for a mnemonic keyout, do not perform mnemonic mapping for rules on touch devices.\r\n if(keyboard.isMnemonic && !(layout.isDefault && layout.formFactor != 'desktop')) {\r\n if(Lkc.Lcode != Codes.keyCodes['K_SPACE']) { // exception required, March 2013\r\n // Jan 2019 - interesting that 'K_SPACE' also affects the caps-state check...\r\n Lkc.vkCode = Lkc.Lcode;\r\n this.isMnemonic = true;\r\n }\r\n } else {\r\n Lkc.vkCode=Lkc.Lcode;\r\n }\r\n\r\n // Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic\r\n if(!keyboard.definesPositionalOrMnemonic) {\r\n Lkc.Lcode = KeyMapping._USKeyCodeToCharCode(Lkc);\r\n Lkc.LisVirtualKey=false;\r\n }\r\n }\r\n\r\n return Lkc;\r\n }\r\n}\r\n\r\nexport class ActiveKey extends ActiveKeyBase implements LayoutKey {\r\n public getSubkey(coreID: string): ActiveSubKey {\r\n if(this.sk) {\r\n for(let key of this.sk) {\r\n if(key.coreID == coreID) {\r\n return key;\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n }\r\n\r\n constructor();\r\n constructor(spec: LayoutKey, layout: ActiveLayout, displayLayer: string);\r\n constructor(spec?: LayoutKey, layout?: ActiveLayout, displayLayer?: string) {\r\n super(spec, layout, displayLayer);\r\n\r\n // Ensure subkeys are also properly extended.\r\n const sk = this.sk;\r\n if(sk) {\r\n for(let i=0; i < sk.length; i++) {\r\n sk[i] = new ActiveSubKey(sk[i], layout, displayLayer);\r\n }\r\n }\r\n\r\n // Also multitap keys.\r\n const multitap = this.multitap;\r\n if(multitap) {\r\n for(let i=0; i < multitap.length; i++) {\r\n multitap[i] = new ActiveSubKey(multitap[i], layout, displayLayer);\r\n }\r\n }\r\n\r\n const flick = this.flick;\r\n if(flick) {\r\n for(let flickKey in flick) {\r\n flick[flickKey as keyof TouchLayoutFlick] = new ActiveSubKey(flick[flickKey as keyof TouchLayoutFlick], layout, displayLayer);\r\n }\r\n }\r\n\r\n ActiveKey.determineHint(this, layout.defaultHint);\r\n }\r\n\r\n private static determineHint(spec: ActiveKey, defaultHint: TouchLayout.TouchLayoutDefaultHint): void {\r\n // If a hint was directly specified, don't override it.\r\n if(spec.hint) {\r\n spec.hintSrc = spec;\r\n return;\r\n }\r\n\r\n // Is more compact than writing 8 separate cases.\r\n if(defaultHint?.includes('flick-')) {\r\n if(spec.flick) {\r\n // 6 = length of 'flick-'\r\n const dir = defaultHint.substring(6) as keyof TouchLayoutFlick;\r\n\r\n if(spec.flick[dir]?.text) {\r\n spec.hintSrc = spec.flick[dir];\r\n }\r\n }\r\n\r\n return;\r\n }\r\n\r\n switch(defaultHint) {\r\n case 'none':\r\n return;\r\n case 'multitap':\r\n if(spec.multitap) {\r\n spec.hintSrc = spec.multitap[0];\r\n }\r\n return;\r\n case 'flick':\r\n if(spec.flick) {\r\n for(const key of KeyTypesOfFlickList) {\r\n if(spec.flick[key]) {\r\n spec.hintSrc = spec.flick[key];\r\n return;\r\n }\r\n }\r\n }\r\n return;\r\n case 'longpress':\r\n if(spec.sk) {\r\n spec.hintSrc = spec.sk[0];\r\n }\r\n return;\r\n case 'dot':\r\n default:\r\n if(spec.sk) {\r\n spec.hint = '\\u2022';\r\n spec.hintSrc = spec;\r\n }\r\n return;\r\n }\r\n }\r\n}\r\n\r\n\r\nexport class ActiveSubKey extends ActiveKeyBase implements LayoutSubKey {\r\n //\r\n}\r\n\r\nexport class ActiveRow implements LayoutRow {\r\n // Identify key labels (e.g. *Shift*) that require the special OSK font\r\n static readonly SPECIAL_LABEL=/\\*\\w+\\*/;\r\n\r\n id: number;\r\n key: ActiveKey[];\r\n\r\n /**\r\n * Used for calculating fat-fingering offsets.\r\n */\r\n proportionalY: number;\r\n\r\n private constructor() {\r\n\r\n }\r\n\r\n static sanitize(rawRow: LayoutRow) {\r\n for(const key of rawRow.key) {\r\n // Test for a trailing comma included in spec, added as null object by IE\r\n // It has only ever appeared at the end of a row's spec.\r\n if(key == null) {\r\n rawRow.key.length = rawRow.key.length-1;\r\n } else {\r\n ActiveKey.sanitize(key);\r\n }\r\n }\r\n\r\n if(typeof rawRow.id == 'string') {\r\n rawRow.id = Number.parseInt(rawRow.id, 10);\r\n }\r\n }\r\n\r\n static polyfill(\r\n row: LayoutRow,\r\n layout: ActiveLayout,\r\n displayLayer: string,\r\n totalWidth: number,\r\n proportionalY: number\r\n ) {\r\n // Apply defaults, setting the width and other undefined properties for each key\r\n let keys=row['key'];\r\n const DEFAULT_KEY = ActiveKeyBase.DEFAULT_KEY;\r\n for(let j=0; j 0) {\r\n const finalKey = keys[keys.length-1] as ActiveKey;\r\n\r\n // If a single key, and padding is negative, add padding to right align the key\r\n if(keys.length == 1 && finalKey.pad < 0) {\r\n const keyPercent = finalKey.width/totalWidth;\r\n const padPercent = 1-(totalPercent + keyPercent + rightMargin);\r\n\r\n // compute center's default x-coord (used in headless modes)\r\n setProportions(finalKey, padPercent, keyPercent, totalPercent);\r\n } else {\r\n const padPercent = finalKey.pad/totalWidth;\r\n const keyPercent = 1-(totalPercent + padPercent + rightMargin);\r\n\r\n // compute center's default x-coord (used in headless modes)\r\n setProportions(finalKey, padPercent, keyPercent, totalPercent);\r\n }\r\n }\r\n\r\n assignDefaultsWithPropDefs(row, new ActiveRow());\r\n\r\n let aRow = row as ActiveRow;\r\n aRow.proportionalY = proportionalY;\r\n }\r\n\r\n @Enumerable\r\n populateKeyMap(map: {[keyId: string]: ActiveKey}) {\r\n this.key.forEach(function(key: ActiveKey) {\r\n if(key.coreID) {\r\n map[key.coreID] = key;\r\n }\r\n });\r\n }\r\n}\r\n\r\nexport class ActiveLayer implements LayoutLayer {\r\n row: ActiveRow[];\r\n id: string;\r\n\r\n // These already exist on the objects, pre-polyfill...\r\n // but they still need to be proactively declared on this type.\r\n capsKey?: ActiveKey;\r\n numKey?: ActiveKey;\r\n scrollKey?: ActiveKey;\r\n\r\n totalWidth: number;\r\n\r\n defaultKeyProportionalWidth: number;\r\n rowProportionalHeight: number;\r\n\r\n /**\r\n * Facilitates mapping key id strings to their specification objects.\r\n */\r\n keyMap: {[keyId: string]: ActiveKey};\r\n\r\n constructor() {\r\n\r\n }\r\n\r\n static sanitize(rawLayer: LayoutLayer) {\r\n for(const row of rawLayer.row) {\r\n ActiveRow.sanitize(row);\r\n }\r\n }\r\n\r\n static polyfill(layer: LayoutLayer, layout: ActiveLayout) {\r\n layer.aligned=false;\r\n\r\n // Create a DIV for each row of the group\r\n let rows=layer['row'];\r\n\r\n // Calculate the maximum row width (in layout units)\r\n let totalWidth=0;\r\n for(const row of rows) {\r\n let width=0;\r\n const keys=row['key'];\r\n\r\n for(const key of keys) {\r\n // So long as `sanitize` is called first, these coercions are safe.\r\n width += (key.width as number) + (key.pad as number);\r\n }\r\n\r\n if(width > totalWidth) {\r\n totalWidth = width;\r\n }\r\n }\r\n\r\n // Add default right margin\r\n if(layout.formFactor == 'desktop') {\r\n totalWidth += 5; // TODO: resolve difference between touch and desktop; why don't we use ActiveKey.DEFAULT_RIGHT_MARGIN?\r\n } else {\r\n totalWidth += ActiveKey.DEFAULT_RIGHT_MARGIN;\r\n }\r\n\r\n let rowCount = layer.row.length;\r\n for(let i=0; i 1) {\r\n let baseKey = this.keyMap[idComponents[0]];\r\n return baseKey.getSubkey(idComponents[1]);\r\n } else {\r\n return this.keyMap[keyId];\r\n }\r\n }\r\n}\r\n\r\nexport class ActiveLayout implements LayoutFormFactor{\r\n /**\r\n * Holds all layer specifications for the layout. There is no guarantee that they\r\n * have been fully preprocessed.\r\n */\r\n layer: TouchLayerSpec[];\r\n font: string;\r\n keyLabels: boolean;\r\n isDefault?: boolean;\r\n keyboard: Keyboard;\r\n formFactor: DeviceSpec.FormFactor;\r\n defaultHint: TouchLayoutDefaultHint;\r\n displayUnderlying?: boolean;\r\n fontsize?: string;\r\n\r\n hasFlicks: boolean = false;\r\n hasLongpresses: boolean = false;\r\n hasMultitaps: boolean = false;\r\n\r\n /**\r\n * Facilitates mapping layer id strings to their specification objects.\r\n */\r\n private layerMap: {[layerId: string]: ActiveLayer};\r\n\r\n private constructor() {\r\n\r\n }\r\n\r\n /**\r\n * Returns a fully preprocessed version of the specified layer spec.\r\n * @param layerId\r\n * @returns\r\n */\r\n @Enumerable\r\n getLayer(layerId: string): ActiveLayer {\r\n if(!this.layerMap[layerId]) {\r\n const spec = this.layer.find((layerSpec) => layerSpec.id == layerId);\r\n if(!spec) {\r\n return null;\r\n }\r\n\r\n // Prepare the layer-spec for actual use.\r\n ActiveLayer.sanitize(spec);\r\n ActiveLayer.polyfill(spec, this);\r\n this.layerMap[layerId] = spec as ActiveLayer;\r\n }\r\n\r\n return this.layerMap[layerId];\r\n }\r\n\r\n /**\r\n * Refer to https://github.com/keymanapp/keyman/issues/254, which mentions\r\n * KD-11 from a prior issue-tracking system from the closed-source days that\r\n * resulted in an unintended extra empty row.\r\n *\r\n * It'll be pretty rare to see a keyboard affected by the bug, but we don't\r\n * 100% control all keyboards out there, so it's best we make sure the edge\r\n * case is covered.\r\n *\r\n * @param layers The layer group to be loaded for the form factor. Will be\r\n * mutated by this operation.\r\n */\r\n static correctLayerEmptyRowBug(layers: LayoutLayer[]) {\r\n for(let n=0; n=0; i--) {\r\n if(!Array.isArray(rows[i].key) || rows[i].key.length == 0) {\r\n rows.splice(i, 1)\r\n }\r\n }\r\n }\r\n }\r\n\r\n static sanitize(rawLayout: TouchLayoutSpec) {\r\n ActiveLayout.correctLayerEmptyRowBug(rawLayout.layer);\r\n }\r\n\r\n /**\r\n *\r\n * @param layout\r\n * @param formFactor\r\n */\r\n static polyfill(layout: TouchLayoutSpec, keyboard: Keyboard, formFactor: DeviceSpec.FormFactor): ActiveLayout {\r\n /* c8 ignore start */\r\n if(layout == null) {\r\n throw new Error(\"Cannot build an ActiveLayout for a null specification.\");\r\n }\r\n /* c8 ignore end */\r\n\r\n const analysisMetadata: AnalysisMetadata = {\r\n hasFlicks: false,\r\n hasLongpresses: false,\r\n hasMultitaps: false\r\n };\r\n\r\n /* Standardize the layout object's data types.\r\n *\r\n * In older versions of KMW, some numeric properties were long represented as strings instead,\r\n * and that lives on within a _lot_ of keyboards. The data should be sanitized before it\r\n * is processed by this method.\r\n */\r\n this.sanitize(layout);\r\n\r\n // This bit of preprocessing is a must; we need to know what gestures are available\r\n // across all layers, \"out of the gate\".\r\n for(let layer of layout.layer) {\r\n for(let row of layer.row) {\r\n for(let key of row.key) {\r\n analysisMetadata.hasLongpresses ||= !!key.sk;\r\n analysisMetadata.hasFlicks ||= !!key.flick;\r\n analysisMetadata.hasMultitaps ||= !!key.multitap;\r\n }\r\n }\r\n }\r\n\r\n // Create a separate OSK div for each OSK layer, only one of which will ever be visible\r\n let layerMap: {[layerId: string]: ActiveLayer} = {};\r\n\r\n // Add class functions to the existing layout object, allowing it to act as an ActiveLayout.\r\n assignDefaultsWithPropDefs(layout, new ActiveLayout());\r\n\r\n let aLayout = layout as unknown as ActiveLayout;\r\n aLayout.keyboard = keyboard;\r\n aLayout.formFactor = formFactor;\r\n aLayout.layerMap = layerMap;\r\n\r\n // The default-layer shift key & shift-layer shift key on mobile platforms should have a\r\n // default multitap re: a 'caps' layer under select conditions.\r\n //\r\n // Note: whether or not any other keys have multitaps doesn't matter here. Just THESE.\r\n if(formFactor != 'desktop' && !!layout.layer.find((entry) => entry.id == 'caps')) {\r\n // Triggers preprocessing for both default and shift layers. They're the\r\n // most-frequently referenced, at least.\r\n const defaultLayer = aLayout.getLayer('default') as ActiveLayer;\r\n const shiftLayer = aLayout.getLayer('shift') as ActiveLayer;\r\n\r\n const defaultShift = defaultLayer.getKey('K_SHIFT');\r\n const shiftShift = shiftLayer ?.getKey('K_SHIFT');\r\n\r\n // If BOTH default & shift layer SHIFT keys lack multitaps & longpresses, proceed.\r\n if(defaultShift && shiftShift && // doesn't make much sense if there's no shift layer or SHIFT on either\r\n !defaultShift.multitap && !shiftShift.multitap &&\r\n !defaultShift.sk && !shiftShift.sk\r\n ) {\r\n // May cause the layout to gain its first multitaps, which does matter for the next lines after the block.\r\n analysisMetadata.hasMultitaps = true;\r\n\r\n defaultShift.multitap = [{...Layouts.dfltShiftToCaps}, {...Layouts.dfltShiftToDefault}] as ActiveSubKey[];\r\n shiftShift.multitap = [{...Layouts.dfltShiftToCaps}, {...Layouts.dfltShiftToShift}] as ActiveSubKey[];\r\n\r\n defaultShift.multitap.forEach((sk, index) => defaultShift.multitap[index] = new ActiveSubKey(sk, aLayout, 'default'));\r\n shiftShift .multitap.forEach((sk, index) => shiftShift.multitap[index] = new ActiveSubKey(sk, aLayout, 'shift'));\r\n } // else no default shift -> caps multitaps.\r\n }\r\n\r\n aLayout.hasFlicks = analysisMetadata.hasFlicks;\r\n aLayout.hasLongpresses = analysisMetadata.hasLongpresses;\r\n aLayout.hasMultitaps = analysisMetadata.hasMultitaps;\r\n\r\n // All layers are lazy-processed, with the usual processing applied when first referenced.\r\n\r\n return aLayout;\r\n }\r\n}\r\n", + "import Codes from \"../codes.js\";\r\nimport { EncodedVisualKeyboard, LayoutSpec, Layouts } from \"./defaultLayouts.js\";\r\nimport { ActiveKey, ActiveLayout, ActiveSubKey } from \"./activeLayout.js\";\r\nimport KeyEvent from \"../keyEvent.js\";\r\nimport { type OutputTarget } from \"../outputTarget.interface.js\";\r\nimport { ModifierKeyConstants, TouchLayout } from \"@keymanapp/common-types\";\r\ntype TouchLayoutSpec = TouchLayout.TouchLayoutPlatform & { isDefault?: boolean};\r\n\r\nimport { Version, DeviceSpec } from \"@keymanapp/web-utils\";\r\nimport StateKeyMap from \"./stateKeyMap.js\";\r\n\r\ntype ComplexKeyboardStore = ( string | { t: 'd', d: number } | { ['t']: 'b' })[];\r\n\r\n/**\r\n * Stores preprocessed properties of a keyboard for quick retrieval later.\r\n */\r\nclass CacheTag {\r\n stores: {[storeName: string]: ComplexKeyboardStore};\r\n\r\n constructor() {\r\n this.stores = {};\r\n }\r\n}\r\n\r\nexport enum LayoutState {\r\n NOT_LOADED = undefined,\r\n POLYFILLED = 1,\r\n CALIBRATED = 2\r\n}\r\n\r\nexport interface VariableStoreDictionary {\r\n [name: string]: string;\r\n};\r\n\r\nexport type KeyboardObject = {\r\n /**\r\n * Used internally by Keyman Engine for Web to hold preprocessed stores.\r\n */\r\n _kmw?: CacheTag;\r\n\r\n /**\r\n * group-start: the function triggering processing for the keyboard's\r\n * \"Unicode\" start group, corresponding to `begin Unicode > use(_____)` in\r\n * Keyman keyboard language.\r\n * @param outputTarget The context to which the keystroke applies\r\n * @param keystroke The full, pre-processed keystroke triggering\r\n * keyboard-rule application.\r\n */\r\n gs(outputTarget: OutputTarget, keystroke: KeyEvent): boolean;\r\n\r\n /**\r\n * group-newcontext: the function triggering processing for the keyboard's\r\n * \"NewContext\" start group, corresponding to `begin NewContext > use(_____)`\r\n * in Keyman keyboard language.\r\n * @param outputTarget The new context to be used with future keystrokes\r\n * @param keystroke A 'null' `KeyEvent` providing current modifier + state information.\r\n */\r\n gn?(outputTarget: OutputTarget, keystroke: KeyEvent): boolean;\r\n\r\n /**\r\n * group-postkeystroke: the function triggering processing for the keyboard's\r\n * \"PostKeystroke\" start group, corresponding to `begin PostKeystroke >\r\n * use(_____)` in Keyman keyboard language.\r\n * @param outputTarget The context altered by a recent keystroke. As a\r\n * precondition, all changes due to `gs` / `begin Unicode` should already be\r\n * applied.\r\n * @param keystroke A 'null' `KeyEvent` providing current modifier + state information.\r\n */\r\n gpk?(outputTarget: OutputTarget, keystroke: KeyEvent): boolean;\r\n\r\n /**\r\n * Keyboard ID: the uniquely-identifying name for this keyboard. Includes the standard\r\n * `Keyboard_` prefix. May be 'namespaced' with a prefix corresponding to a package name\r\n * within app/webview.\r\n */\r\n KI: string;\r\n /**\r\n * Keyboard Name: the human-readable name of the keyboard.\r\n */\r\n KN: string;\r\n /**\r\n * Encoded data usable to construct a desktop/hardware-oriented on-screen keyboard.\r\n */\r\n KV: EncodedVisualKeyboard;\r\n /**\r\n * Keyboard Language Code: set within select keyboards.\r\n *\r\n * Currently, it's only used to determine the need for CJK-picker support. Is missing\r\n * in most compiled keyboards.\r\n */\r\n KLC?: string;\r\n /**\r\n * @deprecated\r\n * Keyboard Language Code: set within select keyboards.\r\n *\r\n * Currently, it's only used to determine the need for CJK-picker support.\r\n * Is (probably) an older name of KLC with the identical purpose. Is missing\r\n * in most compiled keyboards.\r\n */\r\n LanguageCode?: string;\r\n /**\r\n * Keyboard CSS: provides the definition for custom keyboard style sheets\r\n */\r\n KCSS?: string;\r\n /**\r\n * Keyboard is RTL: a simple flag noting if the keyboard's script is RTL.\r\n */\r\n KRTL?: boolean;\r\n /**\r\n * Keyboard Modifier BitMask: a set of bitflags indicating which modifiers\r\n * the keyboard's rules utilize. See also: `ModifierKeyConstants`.\r\n */\r\n KMBM?: number;\r\n /**\r\n * Keyboard Supplementary plane: set to 1 if the keyboard uses non-BMP Unicode\r\n * characters.\r\n */\r\n KS?: number;\r\n /**\r\n * Keyman Visual Keyboard Layout: defines the touch-layout definitions used for\r\n * 'phone' and 'tablet' form-factors.\r\n */\r\n KVKL?: LayoutSpec;\r\n /**\r\n * Keyboard is Mnemonic: set to 1 if the keyboard uses a mnemonic layout.\r\n */\r\n KM?: number;\r\n /**\r\n * KeyBoard VERsion: the version of this keyboard.\r\n */\r\n KBVER?: string;\r\n /**\r\n * Keyman VERsion: the version of Keyman Developer used to compile this keyboard.\r\n */\r\n KVER?: string;\r\n /**\r\n * Keyman Variable Stores: an array of the names of all variable stores used by the\r\n * keyboard.\r\n */\r\n KVS?: (`s${number}`)[];\r\n /**\r\n * Keyboard Help: HTML help text, as specified by either the &kmw_helptext or &kmw_helpfile system stores.\r\n *\r\n * Reference: https://help.keyman.com/developer/language/reference/kmw_helptext,\r\n * https://help.keyman.com/developer/language/reference/kmw_helpfile\r\n */\r\n KH?: string;\r\n /**\r\n * Keyboard Virtual Key Dictionary: the Developer-compiled, minified dictionary of virtual-key codes\r\n */\r\n KVKD?: string;\r\n /**\r\n * Keyboard Display Underlying: set to 1 if the desktop form of the keyboard\r\n * should show the US QWERTY underlying keycaps. These may also appear on\r\n * touch layouts if set and no touch-layout information is available.\r\n */\r\n KDU?: number;\r\n /**\r\n * Virtual Key Dictionary: the engine pre-processed, unminified dictionary. This is built within\r\n * Keyman Engine for Web at runtime as needed based on the definitions in `KVKD`.\r\n */\r\n VKDictionary?: Record,\r\n /**\r\n * Keyboard Help File: Embedded JS script designed for use with a keyboard's\r\n * HTML help text. Always defined within the file referenced by &kmw_embedjs\r\n * in a keyboard's source, though that file may also contain _other_ script\r\n * definitions as well. (`KHF` must be explicitly defined within that file.)\r\n * @param e Will be provided with the root element (a
) of the On-Screen Keyboard.\r\n * @returns\r\n */\r\n KHF?: (e: any) => string;\r\n\r\n /**\r\n * Keyboard Notify Shift: Provided by CJK-picker keyboards to properly\r\n * interface them with Keyman Engine for Web.\r\n * @param {number} _PCommand event code (16,17,18) or 0; 16-18\r\n * correspond to modifier codes when pressed, while 0 corresponds to loss of focus\r\n * @param {Object} _PTarget target element\r\n * @param {number} _PData 1 or 0\r\n * @returns\r\n */\r\n KNS?: (_PCommand: number, _PTarget: OutputTarget, _PData: number) => void;\r\n} & Record<`s${number}`, string>\r\n\r\n\r\n/**\r\n * Acts as a wrapper class for Keyman keyboards compiled to JS, providing type information\r\n * and keyboard-centered functionality in an object-oriented way without modifying the\r\n * wrapped keyboard itself.\r\n */\r\nexport default class Keyboard {\r\n public static DEFAULT_SCRIPT_OBJECT: KeyboardObject = {\r\n 'gs': function(outputTarget: OutputTarget, keystroke: KeyEvent) { return false; }, // no matching rules; rely on defaultRuleOutput entirely\r\n 'KI': '', // The currently-existing default keyboard ID; we already have checks that focus against this.\r\n 'KN': '',\r\n 'KV': Layouts.DEFAULT_RAW_SPEC,\r\n 'KM': 0 // May not be the best default, but this matches current behavior when there is no activeKeyboard.\r\n }\r\n\r\n /**\r\n * This is the object provided to KeyboardInterface.registerKeyboard - that is, the keyboard\r\n * being wrapped.\r\n *\r\n * TODO: Make this private instead. But there are a LOT of references that must be rooted out first.\r\n */\r\n public readonly scriptObject: KeyboardObject;\r\n private layoutStates: {[layout: string]: LayoutState};\r\n\r\n constructor(keyboardScript: any) {\r\n if(keyboardScript) {\r\n this.scriptObject = keyboardScript;\r\n } else {\r\n this.scriptObject = Keyboard.DEFAULT_SCRIPT_OBJECT;\r\n }\r\n this.layoutStates = {};\r\n }\r\n\r\n /**\r\n * Calls the keyboard's `gs` function, which represents the keyboard source's begin Unicode group.\r\n */\r\n process(outputTarget: OutputTarget, keystroke: KeyEvent): boolean {\r\n return this.scriptObject['gs'](outputTarget, keystroke);\r\n }\r\n\r\n /**\r\n * Calls the keyboard's `gn` function, which represents the keyboard source's begin newContext group.\r\n */\r\n processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): boolean {\r\n return this.scriptObject['gn'] ? this.scriptObject['gn'](outputTarget, keystroke) : false;\r\n }\r\n\r\n /**\r\n * Calls the keyboard's `gpk` function, which represents the keyboard source's begin postKeystroke group.\r\n */\r\n processPostKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): boolean {\r\n return this.scriptObject['gpk'] ? this.scriptObject['gpk'](outputTarget, keystroke) : false;\r\n }\r\n\r\n get isHollow(): boolean {\r\n return this.scriptObject == Keyboard.DEFAULT_SCRIPT_OBJECT;\r\n }\r\n\r\n get id(): string {\r\n return this.scriptObject['KI'];\r\n }\r\n\r\n get name(): string {\r\n return this.scriptObject['KN'];\r\n }\r\n\r\n /**\r\n * Cache variable store values\r\n *\r\n * Primarily used for predictive text to prevent variable store\r\n * values from being changed in 'fat finger' processing.\r\n *\r\n * KVS is available in keyboards compiled with Keyman Developer 15\r\n * and later versions. See #2924.\r\n *\r\n * @returns an object with each property referencing a variable store\r\n */\r\n get variableStores(): VariableStoreDictionary {\r\n const storeNames = this.scriptObject['KVS'];\r\n let values: VariableStoreDictionary = {};\r\n if(Array.isArray(storeNames)) {\r\n for(let store of storeNames) {\r\n values[store] = this.scriptObject[store];\r\n }\r\n }\r\n return values;\r\n }\r\n\r\n /**\r\n * Restore variable store values from cache\r\n *\r\n * KVS is available in keyboards compiled with Keyman Developer 15\r\n * and later versions. See #2924.\r\n *\r\n * @param values name-value pairs for each store value\r\n */\r\n set variableStores(values: VariableStoreDictionary) {\r\n const storeNames = this.scriptObject['KVS'];\r\n if(Array.isArray(storeNames)) {\r\n for(let store of storeNames) {\r\n // If the value is not present in the cache, don't overwrite it;\r\n // while this is not used in initial implementation, we could use\r\n // it in future to update a single variable store value rather than\r\n // the whole cache.\r\n if(typeof values[store] == 'string') {\r\n this.scriptObject[store] = values[store];\r\n }\r\n }\r\n }\r\n }\r\n\r\n private get _legacyLayoutSpec() {\r\n return this.scriptObject['KV']; // used with buildDefaultLayout; layout must be constructed at runtime.\r\n }\r\n\r\n // May return null if no layouts exist or have been initialized.\r\n private get _layouts(): LayoutSpec {\r\n return this.scriptObject['KVKL']; // This one is compiled by Developer's visual keyboard layout editor.\r\n }\r\n\r\n private set _layouts(value: LayoutSpec) {\r\n this.scriptObject['KVKL'] = value;\r\n }\r\n\r\n get compilerVersion(): Version {\r\n return new Version(this.scriptObject['KVER']);\r\n }\r\n\r\n get isMnemonic(): boolean {\r\n return !!this.scriptObject['KM'];\r\n }\r\n\r\n get definesPositionalOrMnemonic(): boolean {\r\n return typeof this.scriptObject['KM'] != 'undefined';\r\n }\r\n\r\n /**\r\n * HTML help text, as specified by either the &kmw_helptext or &kmw_helpfile system stores.\r\n *\r\n * Reference: https://help.keyman.com/developer/language/reference/kmw_helptext,\r\n * https://help.keyman.com/developer/language/reference/kmw_helpfile\r\n */\r\n get helpText(): string {\r\n return this.scriptObject['KH'];\r\n }\r\n\r\n /**\r\n * Embedded JS script designed for use with a keyboard's HTML help text. Always defined\r\n * within the file referenced by &kmw_embedjs in a keyboard's source, though that file\r\n * may also contain _other_ script definitions as well. (`KHF` must be explicitly defined\r\n * within that file.)\r\n */\r\n get hasScript(): boolean {\r\n return !!this.scriptObject['KHF'];\r\n }\r\n\r\n /**\r\n * Embeds a custom script for use by the OSK, which may be interactive (like with sil_euro_latin).\r\n * Note: this must be called AFTER any contents of `helpText` have been inserted into the DOM.\r\n * (See sil_euro_latin's source -> sil_euro_latin_js.txt)\r\n *\r\n * Reference: https://help.keyman.com/developer/language/reference/kmw_embedjs\r\n */\r\n embedScript(e: any) {\r\n // e: Expects the OSKManager's _Box element. We don't add type info here b/c it would\r\n // reference the DOM.\r\n this.scriptObject['KHF'](e);\r\n }\r\n\r\n get oskStyling(): string {\r\n return this.scriptObject['KCSS'];\r\n }\r\n\r\n /**\r\n * true if this keyboard uses a (legacy) pick list (Chinese, Japanese, Korean, etc.)\r\n *\r\n * TODO: Make a property on keyboards (say, `isPickList` / `KPL`) to signal this when we\r\n * get around to better, generalized picker-list support.\r\n */\r\n get isCJK(): boolean { // I3363 (Build 301)\r\n var lg: string;\r\n if(typeof(this.scriptObject['KLC']) != 'undefined') {\r\n lg = this.scriptObject['KLC'];\r\n } else if(typeof(this.scriptObject['LanguageCode']) != 'undefined') {\r\n lg = this.scriptObject['LanguageCode'];\r\n }\r\n\r\n // While some of these aren't proper BCP-47 language codes, the CJK keyboards predate our use of BCP-47.\r\n // So, we preserve the old ISO 639-3 codes, as that's what the keyboards are matching against.\r\n return ((lg == 'cmn') || (lg == 'jpn') || (lg == 'kor'));\r\n }\r\n\r\n get isRTL(): boolean {\r\n return !!this.scriptObject['KRTL'];\r\n }\r\n\r\n /**\r\n * Obtains the currently-active modifier bitmask for the active keyboard.\r\n */\r\n get modifierBitmask(): number {\r\n // NON_CHIRAL is the default bitmask if KMBM is not defined.\r\n // We always need a bitmask to compare against, as seen in `isChiral`.\r\n return this.scriptObject['KMBM'] || Codes.modifierBitmasks['NON_CHIRAL'];\r\n }\r\n\r\n get isChiral(): boolean {\r\n return !!(this.modifierBitmask & Codes.modifierBitmasks['IS_CHIRAL']);\r\n }\r\n\r\n get desktopFont(): string {\r\n if(this.scriptObject['KV']) {\r\n return this.scriptObject['KV']['F'];\r\n } else {\r\n return null;\r\n }\r\n }\r\n\r\n private get cacheTag(): CacheTag {\r\n let tag = this.scriptObject['_kmw'];\r\n\r\n if(!tag) {\r\n tag = new CacheTag();\r\n this.scriptObject['_kmw'] = tag;\r\n }\r\n\r\n return tag;\r\n }\r\n\r\n get explodedStores(): {[storeName: string]: ComplexKeyboardStore} {\r\n return this.cacheTag.stores;\r\n }\r\n\r\n /**\r\n * Signifies whether or not a layout or OSK should include AltGr / Right-alt emulation for this keyboard.\r\n * @param {Object=} keyLabels\r\n * @return {boolean}\r\n */\r\n get emulatesAltGr(): boolean {\r\n // If we're not chiral, we're not emulating.\r\n if(!this.isChiral) {\r\n return false;\r\n }\r\n\r\n if(this._legacyLayoutSpec == null) {\r\n return false;\r\n }\r\n\r\n // Only exists in KMW 10.0+, but before that Web had no chirality support, so... return false.\r\n let layers = this._legacyLayoutSpec['KLS'];\r\n if(!layers) {\r\n return false;\r\n }\r\n\r\n var emulationMask = ModifierKeyConstants.LCTRLFLAG | ModifierKeyConstants.LALTFLAG;\r\n var unshiftedEmulationLayer = layers[Layouts.getLayerId(emulationMask)];\r\n var shiftedEmulationLayer = layers[Layouts.getLayerId(ModifierKeyConstants.K_SHIFTFLAG | emulationMask)];\r\n\r\n // buildDefaultLayout ensures that these are aliased to the original modifier set being emulated.\r\n // As a result, we can directly test for reference equality.\r\n //\r\n // This allows us to still return `true` after creating the layers for emulation; during keyboard\r\n // construction, the two layers should be null for AltGr emulation to succeed.\r\n if(unshiftedEmulationLayer != null &&\r\n unshiftedEmulationLayer != layers[Layouts.getLayerId(ModifierKeyConstants.RALTFLAG)]) {\r\n return false;\r\n }\r\n\r\n if(shiftedEmulationLayer != null &&\r\n shiftedEmulationLayer != layers[Layouts.getLayerId(ModifierKeyConstants.RALTFLAG | ModifierKeyConstants.K_SHIFTFLAG)]) {\r\n return false;\r\n }\r\n\r\n // It's technically possible for the OSK to not specify anything while allowing chiral input. A last-ditch catch:\r\n var bitmask = this.modifierBitmask;\r\n if((bitmask & emulationMask) != emulationMask) {\r\n // At least one of the emulation modifiers is never used by the keyboard! We can confirm everything's safe.\r\n return true;\r\n }\r\n\r\n if(unshiftedEmulationLayer == null && shiftedEmulationLayer == null) {\r\n // We've run out of things to go on; we can't detect if chiral AltGr emulation is intended or not.\r\n // TODO: handle this again!\r\n // if(!osk.altGrWarning) {\r\n // console.warn(\"Could not detect if AltGr emulation is safe, but defaulting to active emulation!\")\r\n // // Avoid spamming the console with warnings on every call of the method.\r\n // osk.altGrWarning = true;\r\n // }\r\n return true;\r\n }\r\n return true;\r\n }\r\n\r\n get usesSupplementaryPlaneChars(): boolean {\r\n let kbd = this.scriptObject;\r\n // I3319 - SMP extension, I3363 (Build 301)\r\n return kbd && ((kbd['KS'] && kbd['KS'] == 1) || kbd['KN'] == 'Hieroglyphic');\r\n }\r\n\r\n get version(): string {\r\n return this.scriptObject['KBVER'] || '';\r\n }\r\n\r\n usesDesktopLayoutOnDevice(device: DeviceSpec) {\r\n if(this.scriptObject['KVKL']) {\r\n // A custom mobile layout is defined... but are we using it?\r\n return device.formFactor == DeviceSpec.FormFactor.Desktop;\r\n } else {\r\n return true;\r\n }\r\n }\r\n\r\n /**\r\n * @param {number} _PCommand event code (16,17,18) or 0\r\n * @param {Object} _PTarget target element\r\n * @param {number} _PData 1 or 0\r\n * Notifies keyboard of keystroke or other event\r\n */\r\n notify(_PCommand: number, _PTarget: OutputTarget, _PData: number) { // I2187\r\n // Good example use case - the Japanese CJK-picker keyboard\r\n if(typeof(this.scriptObject['KNS']) == 'function') {\r\n this.scriptObject['KNS'](_PCommand, _PTarget, _PData);\r\n }\r\n }\r\n\r\n private findOrConstructLayout(formFactor: DeviceSpec.FormFactor): TouchLayoutSpec {\r\n if(this._layouts) {\r\n // Search for viable layouts. `null` is allowed for desktop form factors when help text is available,\r\n // so we check explicitly against `undefined`.\r\n if(this._layouts[formFactor] !== undefined) {\r\n return this._layouts[formFactor];\r\n } else if(formFactor == DeviceSpec.FormFactor.Phone && this._layouts[DeviceSpec.FormFactor.Tablet]) {\r\n return this._layouts[DeviceSpec.FormFactor.Phone] = this._layouts[DeviceSpec.FormFactor.Tablet];\r\n } else if(formFactor == DeviceSpec.FormFactor.Tablet && this._layouts[DeviceSpec.FormFactor.Phone]) {\r\n return this._layouts[DeviceSpec.FormFactor.Tablet] = this._layouts[DeviceSpec.FormFactor.Phone];\r\n }\r\n }\r\n\r\n // No pre-built layout available; time to start constructing it via defaults.\r\n // First, if we have non-default keys specified by the ['BK'] array, we've got\r\n // enough to work with to build a default layout.\r\n let rawSpecifications: any = null; // TODO: better typing, same type as this._legacyLayoutSpec.\r\n if(this._legacyLayoutSpec != null && this._legacyLayoutSpec['KLS']) { // KLS is only specified whenever there are non-default keys.\r\n rawSpecifications = this._legacyLayoutSpec;\r\n } else if(this._legacyLayoutSpec != null && this._legacyLayoutSpec['BK'] != null) {\r\n var keyCaps=this._legacyLayoutSpec['BK'];\r\n for(var i=0; i 0) {\r\n rawSpecifications = this._legacyLayoutSpec;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // If we don't have key definitions to use for a layout but also lack help text or are a touch-based layout,\r\n // we make a default layout anyway. We have to show display something usable.\r\n if(!rawSpecifications && (this.helpText == '' || formFactor != DeviceSpec.FormFactor.Desktop)) {\r\n rawSpecifications = {'F':'Tahoma', 'BK': Layouts.dfltText};\r\n }\r\n\r\n // Regardless of success, we'll want to initialize the field that backs the property;\r\n // may as well cache the default layout we just built, or a 'null' if it shouldn't exist..\r\n if(!this._layouts) {\r\n this._layouts = {};\r\n }\r\n\r\n // Final check - do we construct a layout, or is this a case where helpText / insertHelpHTML should take over?\r\n if(rawSpecifications) {\r\n // Now to generate a layout from our raw specifications.\r\n let layout: TouchLayoutSpec = this._layouts[formFactor] = Layouts.buildDefaultLayout(rawSpecifications, this, formFactor);\r\n layout.isDefault = true;\r\n return layout;\r\n } else {\r\n // The fact that it doesn't exist will indicate that help text/HTML should be inserted instead.\r\n this._layouts[formFactor] = null; // provides a cached value for the check at the top of this method.\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Returns an ActiveLayout object representing the keyboard's layout for this form factor. May return null if a custom desktop \"help\" OSK is defined, as with sil_euro_latin.\r\n *\r\n * In such cases, please use either `helpText` or `insertHelpHTML` instead.\r\n * @param formFactor {string} The desired form factor for the layout.\r\n */\r\n public layout(formFactor: DeviceSpec.FormFactor): ActiveLayout {\r\n let rawLayout = this.findOrConstructLayout(formFactor);\r\n\r\n if(rawLayout) {\r\n // Prevents accidentally reprocessing layouts; it's a simple enough check.\r\n if(this.layoutStates[formFactor] == LayoutState.NOT_LOADED) {\r\n const layout = ActiveLayout.polyfill(rawLayout, this, formFactor);\r\n this.layoutStates[formFactor] = LayoutState.POLYFILLED;\r\n return layout;\r\n } else {\r\n return rawLayout as unknown as ActiveLayout;\r\n }\r\n\r\n } else {\r\n return null;\r\n }\r\n }\r\n\r\n public refreshLayouts() {\r\n let formFactors = [ DeviceSpec.FormFactor.Desktop, DeviceSpec.FormFactor.Phone, DeviceSpec.FormFactor.Tablet ];\r\n\r\n let _this = this;\r\n\r\n formFactors.forEach(function(form) {\r\n // Currently doesn't work if we reset it to POLYFILLED, likely due to how 'calibration'\r\n // currently works.\r\n _this.layoutStates[form] = LayoutState.NOT_LOADED;\r\n });\r\n }\r\n\r\n public markLayoutCalibrated(formFactor: DeviceSpec.FormFactor) {\r\n if(this.layoutStates[formFactor] != LayoutState.NOT_LOADED) {\r\n this.layoutStates[formFactor] = LayoutState.CALIBRATED;\r\n }\r\n }\r\n\r\n public getLayoutState(formFactor: DeviceSpec.FormFactor) {\r\n return this.layoutStates[formFactor];\r\n }\r\n\r\n\r\n constructNullKeyEvent(device: DeviceSpec, stateKeys?: StateKeyMap): KeyEvent {\r\n stateKeys = stateKeys || {\r\n K_CAPS: false,\r\n K_NUMLOCK: false,\r\n K_SCROLL: false\r\n }\r\n\r\n const keyEvent = KeyEvent.constructNullKeyEvent(device);\r\n this.setSyntheticEventDefaults(keyEvent, stateKeys);\r\n return keyEvent;\r\n }\r\n\r\n constructKeyEvent(key: ActiveKey | ActiveSubKey, device: DeviceSpec, stateKeys: StateKeyMap): KeyEvent {\r\n // Make a deep copy of our preconstructed key event, filling it out from there.\r\n const Lkc = key.baseKeyEvent;\r\n Lkc.device = device;\r\n\r\n if(this.isMnemonic) {\r\n Lkc.setMnemonicCode(key.layer.indexOf('shift') != -1, stateKeys['K_CAPS']);\r\n }\r\n\r\n // Performs common pre-analysis for both 'native' and 'embedded' OSK key & subkey input events.\r\n // This part depends on the keyboard processor's active state.\r\n this.setSyntheticEventDefaults(Lkc, stateKeys);\r\n\r\n // If it's a state key modifier, trigger its effects as part of the\r\n // keystroke.\r\n const bitmap = {\r\n 'K_CAPS': Codes.stateBitmasks.CAPS,\r\n 'K_NUMLOCK': Codes.stateBitmasks.NUM_LOCK,\r\n 'K_SCROLL': Codes.stateBitmasks.SCROLL_LOCK\r\n };\r\n const bitmask = bitmap[Lkc.kName as keyof typeof bitmap];\r\n\r\n if(bitmask) {\r\n Lkc.Lstates ^= bitmask;\r\n Lkc.LmodifierChange = true;\r\n }\r\n\r\n return Lkc;\r\n }\r\n\r\n setSyntheticEventDefaults(Lkc: KeyEvent, stateKeys: StateKeyMap) {\r\n // Set the flags for the state keys - for desktop devices. For touch\r\n // devices, the only state key in use currently is Caps Lock, which is set\r\n // when the 'caps' layer is active in ActiveKey::constructBaseKeyEvent.\r\n if(!Lkc.device.touchable) {\r\n /*\r\n * For desktop-style keyboards, start from a blank slate. They have a 'default'\r\n * (implicit 'NO_CAPS') layer but not a 'caps' layer. With caps set, it just\r\n * highlights the key on the 'default' layer instead.\r\n *\r\n * We should never set both `CAPS` and `NO_CAPS` at the same time, and\r\n * same for the other modifiers.\r\n */\r\n Lkc.Lstates = 0;\r\n Lkc.Lstates |= stateKeys['K_CAPS'] ? ModifierKeyConstants.CAPITALFLAG : ModifierKeyConstants.NOTCAPITALFLAG;\r\n Lkc.Lstates |= stateKeys['K_NUMLOCK'] ? ModifierKeyConstants.NUMLOCKFLAG : ModifierKeyConstants.NOTNUMLOCKFLAG;\r\n Lkc.Lstates |= stateKeys['K_SCROLL'] ? ModifierKeyConstants.SCROLLFLAG : ModifierKeyConstants.NOTSCROLLFLAG;\r\n }\r\n\r\n // Set LisVirtualKey to false to ensure that nomatch rule does fire for U_xxxx keys\r\n if(Lkc.kName && Lkc.kName.substr(0,2) == 'U_') {\r\n Lkc.LisVirtualKey=false;\r\n }\r\n\r\n // Get code for non-physical keys (T_KOKAI, U_05AB etc)\r\n if(typeof Lkc.Lcode == 'undefined') {\r\n Lkc.Lcode = this.getVKDictionaryCode(Lkc.kName);// Updated for Build 347\r\n if(!Lkc.Lcode) {\r\n // Special case for U_xxxx keys. This vk code will never be used\r\n // in a keyboard, so we use this to ensure that keystroke processing\r\n // occurs for the key.\r\n Lkc.Lcode = 1;\r\n }\r\n }\r\n\r\n // Handles modifier states when the OSK is emulating rightalt through the leftctrl-leftalt layer.\r\n if((Lkc.Lmodifiers & Codes.modifierBitmasks['ALT_GR_SIM']) == Codes.modifierBitmasks['ALT_GR_SIM'] && this.emulatesAltGr) {\r\n Lkc.Lmodifiers &= ~Codes.modifierBitmasks['ALT_GR_SIM'];\r\n Lkc.Lmodifiers |= ModifierKeyConstants.RALTFLAG;\r\n }\r\n }\r\n\r\n /**\r\n * @summary Look up a custom virtual key code in the virtual key code dictionary KVKD.\r\n * On first run, will build the dictionary.\r\n *\r\n * `VKDictionary` is constructed from the keyboard's `KVKD` member. This list is constructed\r\n * at compile-time and is a list of 'additional' virtual key codes, starting at 256 (i.e.\r\n * outside the range of standard virtual key codes). These additional codes are both\r\n * `[T_xxx]` and `[U_xxxx]` custom key codes from the Keyman keyboard language. However,\r\n * `[U_xxxx]` keys only generate an entry in `KVKD` if there is a corresponding rule that\r\n * is associated with them in the keyboard rules. If the `[U_xxxx]` key code is only\r\n * referenced as the id of a key in the touch layout, then it does not get an entry in\r\n * the `KVKD` property.\r\n *\r\n * @private\r\n * @param {string} keyName custom virtual key code to lookup in the dictionary\r\n * @return {number} key code > 255 on success, or 0 if not found\r\n */\r\n getVKDictionaryCode(keyName: string) {\r\n const dict = this.scriptObject['VKDictionary'] || {} as KeyboardObject['VKDictionary'];\r\n if(!this.scriptObject['VKDictionary']) {\r\n if(typeof this.scriptObject['KVKD'] == 'string') {\r\n // Build the VK dictionary\r\n // TODO: Move the dictionary build into the compiler -- so compiler generates code such as following.\r\n // Makes the VKDictionary member unnecessary.\r\n // this.KVKD={\"K_ABC\":256,\"K_DEF\":257,...};\r\n const s=this.scriptObject['KVKD'].split(' ');\r\n for(var i=0; i {\r\n this.harness.install();\r\n const promise = this.loadKeyboardInternal(uri, new UriBasedErrorBuilder(uri));\r\n\r\n return promise;\r\n }\r\n\r\n public loadKeyboardFromStub(stub: KeyboardStub) {\r\n this.harness.install();\r\n let promise = this.loadKeyboardInternal(stub.filename, new StubBasedErrorBuilder(stub), stub.id);\r\n\r\n return promise;\r\n }\r\n\r\n protected abstract loadKeyboardInternal(\r\n uri: string,\r\n errorBuilder: KeyboardLoadErrorBuilder,\r\n id?: string\r\n ): Promise;\r\n}", + "// Compiles completely out if `const enum`, making it unavailable in JS-based unit tests.\r\nenum SpacebarText {\r\n KEYBOARD = 'keyboard',\r\n LANGUAGE = 'language',\r\n LANGUAGE_KEYBOARD = 'languageKeyboard',\r\n BLANK = 'blank'\r\n};\r\n\r\nexport default SpacebarText;", + "import SpacebarText from './spacebarText.js';\r\n\r\nexport interface InternalKeyboardFont {\r\n family: string;\r\n filename?: never;\r\n files: string | string[]; // internal\r\n source?: never;\r\n path: string;\r\n}\r\n\r\ninterface CloudKeyboardFont1 {\r\n family: string;\r\n filename: string | string[];\r\n files?: never;\r\n source?: never;\r\n}\r\n\r\ninterface CloudKeyboardFont2 {\r\n family: string;\r\n filename?: never;\r\n files?: never;\r\n source: string | string[];\r\n}\r\n\r\nexport type CloudKeyboardFont = CloudKeyboardFont1 | CloudKeyboardFont2;\r\n\r\n/**\r\n * Converts one of three public-facing font-specification formats into a consistent structure\r\n * used generally among the Keyman JS/TS modules.\r\n * @param fontObj\r\n * @param fontPath\r\n * @returns\r\n */\r\nexport function internalizeFont(fontObj: CloudKeyboardFont, fontPath: string): InternalKeyboardFont {\r\n if(!fontObj) {\r\n return undefined;\r\n } else {\r\n return {\r\n family: fontObj.family,\r\n path: fontPath,\r\n files: fontObj.filename || fontObj.source\r\n }\r\n }\r\n}\r\n\r\nexport type KeyboardFont = CloudKeyboardFont | InternalKeyboardFont;\r\n\r\n// Filename properties are deliberately omitted here; we can add that at higher-levels where it matters\r\n// via 'mix-in'.\r\n//\r\n// For example, the OSK module doesn't care about the filename of a loaded keyboard. It doesn't do\r\n// keyboard loading on its own whatsoever.\r\n\r\n// Corresponds to Keyman Engine for Web's internal \"keyboard stub\" format.\r\n// Also referred to by KMW 2.0-era loaders: https://help.keyman.com/developer/8.0/docs/reference_kmw20_example\r\nexport interface KeyboardInternalPropertySpec {\r\n KI: string,\r\n KFont: InternalKeyboardFont,\r\n KOskFont: InternalKeyboardFont,\r\n displayName?: string,\r\n KN?: string,\r\n KL?: string,\r\n KLC?: string\r\n};\r\n\r\nexport type LanguageAPIPropertySpec = {\r\n id: string,\r\n name: string,\r\n font: CloudKeyboardFont,\r\n oskFont: CloudKeyboardFont,\r\n region?: number|string\r\n}\r\n\r\n/**\r\n * Corresponds to the documented API for the Web engine's `addKeyboards` function\r\n * when a single language object is specified - not an array.\r\n *\r\n * See https://help.keyman.com/DEVELOPER/ENGINE/WEB/15.0/reference/core/addKeyboards,\r\n * \"Using an `object`\".\r\n */\r\nexport type KeyboardAPIPropertySpec = {\r\n id: string,\r\n name: string,\r\n\r\n /**\r\n * @deprecated Replaced with `languages`.\r\n */\r\n language?: LanguageAPIPropertySpec;\r\n languages: LanguageAPIPropertySpec;\r\n}\r\n\r\n/**\r\n * Corresponds to the documented API for the Web engine's `addKeyboards` function\r\n * when a language array is specified for the object.\r\n *\r\n * See https://help.keyman.com/DEVELOPER/ENGINE/WEB/15.0/reference/core/addKeyboards,\r\n * \"Using an `object`\".\r\n */\r\nexport type KeyboardAPIPropertyMultilangSpec = {\r\n id: string,\r\n name: string,\r\n\r\n /**\r\n * @deprecated Replaced with `languages`.\r\n */\r\n language?: LanguageAPIPropertySpec[];\r\n languages: LanguageAPIPropertySpec[];\r\n}\r\n\r\nexport type MetadataObj = KeyboardInternalPropertySpec | KeyboardAPIPropertySpec | KeyboardAPIPropertyMultilangSpec;\r\n\r\nexport default class KeyboardProperties implements KeyboardInternalPropertySpec {\r\n KI: string;\r\n KN: string;\r\n KL: string;\r\n KLC: string;\r\n KFont: InternalKeyboardFont;\r\n KOskFont: InternalKeyboardFont;\r\n _displayName?: string;\r\n\r\n private static spacebarTextModeSrc: SpacebarText | (() => SpacebarText) = SpacebarText.KEYBOARD;\r\n\r\n public static get spacebarTextMode(): SpacebarText {\r\n if(typeof this.spacebarTextModeSrc == 'string') {\r\n return this.spacebarTextModeSrc;\r\n } else {\r\n return this.spacebarTextModeSrc();\r\n }\r\n }\r\n\r\n public static set spacebarTextMode(source: typeof KeyboardProperties.spacebarTextModeSrc) {\r\n this.spacebarTextModeSrc = source;\r\n }\r\n\r\n public constructor(metadataObj: MetadataObj, fontPath?: string);\r\n public constructor(keyboardId: string, languageCode: string);\r\n public constructor(arg1: MetadataObj | string, arg2?: string) {\r\n if(!(typeof arg1 == 'string')) {\r\n // @ts-ignore\r\n if(arg1['KI'] || arg1['KL'] || arg1['KLC'] || arg1['KFont'] || arg1['KOskFont']) {\r\n const other = arg1 as KeyboardInternalPropertySpec;\r\n this.KI = other.KI;\r\n this.KN = other.KN;\r\n this.KL = other.KL;\r\n this.KLC = other.KLC;\r\n // Do NOT apply fontPath here; the mobile apps will have font issues if you do!\r\n this.KFont = other.KFont;\r\n this.KOskFont = other.KOskFont;\r\n this._displayName = (other instanceof KeyboardProperties) ? other._displayName : other.displayName;\r\n } else {\r\n let apiStub = arg1 as KeyboardAPIPropertySpec; // TODO: could be an array, as currently specified. :(\r\n\r\n apiStub.languages ||= apiStub.language;\r\n\r\n this.KI = apiStub.id,\r\n this.KN = apiStub.name,\r\n this.KL = apiStub.languages.name,\r\n this.KLC = apiStub.languages.id,\r\n this.KFont = internalizeFont(apiStub.languages.font, arg2),\r\n this.KOskFont = internalizeFont(apiStub.languages.oskFont, arg2)\r\n }\r\n } else {\r\n this.KI = arg1;\r\n this.KLC = arg2;\r\n }\r\n }\r\n\r\n public static fromMultilanguageAPIStub(apiStub: KeyboardAPIPropertyMultilangSpec): KeyboardProperties[] {\r\n let stubs: KeyboardProperties[] = [];\r\n\r\n apiStub.languages ||= apiStub.language;\r\n\r\n for(let langSpec of apiStub.languages) {\r\n let stub: KeyboardAPIPropertySpec = {\r\n id: apiStub.id,\r\n name: apiStub.name,\r\n languages: langSpec\r\n };\r\n\r\n stubs.push(new KeyboardProperties(stub));\r\n }\r\n\r\n return stubs;\r\n }\r\n\r\n public get id(): string {\r\n return this.KI;\r\n }\r\n\r\n public get name(): string {\r\n return this.KN;\r\n }\r\n\r\n public get langId(): string {\r\n return this.KLC;\r\n }\r\n\r\n public get langName(): string {\r\n return this.KL;\r\n }\r\n\r\n public get displayName(): string {\r\n if(this._displayName) {\r\n return this._displayName;\r\n }\r\n\r\n // else, construct it.\r\n const kbdName = this.KN;\r\n const lgName = this.KL;\r\n\r\n switch (KeyboardProperties.spacebarTextMode) {\r\n case SpacebarText.KEYBOARD:\r\n return kbdName;\r\n case SpacebarText.LANGUAGE:\r\n return lgName;\r\n case SpacebarText.LANGUAGE_KEYBOARD:\r\n return (kbdName == lgName) ? lgName : lgName + ' - ' + kbdName;\r\n case SpacebarText.BLANK:\r\n return '';\r\n default:\r\n return kbdName;\r\n }\r\n }\r\n\r\n public set displayName(name: string) {\r\n this._displayName = name;\r\n }\r\n\r\n public get textFont() {\r\n return this.KFont;\r\n }\r\n\r\n public get oskFont() {\r\n return this.KOskFont;\r\n }\r\n\r\n /**\r\n * Generates an error for objects with specification levels insufficient for use in the on-screen-keyboard\r\n * module, complete with a message about one or more details in need of correction.\r\n * @returns A preconstructed `Error` instance that may be thrown by the caller.\r\n */\r\n public validateForOSK(): Error {\r\n if(!this.KLC) {\r\n if(this.KI || this.KN) {\r\n return new Error(`No language code was specified for use with the ${this.KI || this.KN} keyboard`);\r\n } else {\r\n return new Error(\"No language code was specified for use with the corresponding keyboard\")\r\n }\r\n }\r\n\r\n if(this.displayName === undefined || (KeyboardProperties.spacebarTextMode != SpacebarText.BLANK && !this.displayName)) {\r\n return new Error(\"A display name is missing for this keyboard and cannot be generated under current settings.\")\r\n }\r\n\r\n return null;\r\n }\r\n\r\n public validateForCustomKeyboard(): Error {\r\n if(!this.KI || !this.KN || !this.KL || !this.KLC) {\r\n return new Error(\"To use a custom keyboard, you must specify keyboard id, keyboard name, language and language code.\");\r\n } else {\r\n return null;\r\n }\r\n }\r\n}", + "import { PathOptionSpec } from \"./optionSpec.interface.js\";\r\nimport { OSKResourcePathConfiguration } from './oskResourcePathConfiguration.interface.js';\r\n\r\nconst addDelimiter = (p: string) => {\r\n // Add delimiter if missing\r\n if(p.substring(p.length-1) != '/') {\r\n return p + '/';\r\n } else {\r\n return p;\r\n }\r\n}\r\n\r\nexport default class PathConfiguration implements OSKResourcePathConfiguration {\r\n private readonly sourcePath: string;\r\n private _root: string;\r\n private _resources: string;\r\n private _keyboards: string;\r\n\r\n // May get its initial value from the Keyman Cloud API after a query if not\r\n // otherwise specified.\r\n private _fonts: string;\r\n readonly protocol: string;\r\n\r\n /*\r\n * Pre-modularization code corresponding to `sourcePath`:\r\n ```\r\n // Determine path and protocol of executing script, setting them as\r\n // construction defaults.\r\n //\r\n // This can only be done during load when the active script will be the\r\n // last script loaded. Otherwise the script must be identified by name.\r\n\r\n var scripts = document.getElementsByTagName('script');\r\n var ss = scripts[scripts.length-1].src;\r\n var sPath = ss.substr(0,ss.lastIndexOf('/')+1);\r\n ```\r\n */\r\n constructor(pathSpec: Required, sourcePath: string) {\r\n sourcePath = addDelimiter(sourcePath);\r\n this.sourcePath = sourcePath;\r\n this.protocol = sourcePath.replace(/(.{3,5}:)(.*)/,'$1');\r\n\r\n this.updateFromOptions(pathSpec);\r\n }\r\n\r\n updateFromOptions(pathSpec: Required) {\r\n const _rootPath = this.sourcePath.replace(/(https?:\\/\\/)([^\\/]*)(.*)/,'$1$2/');\r\n\r\n // Get default paths and device options\r\n this._root = _rootPath;\r\n if(pathSpec.root != '') {\r\n this._root = this.fixPath(pathSpec.root);\r\n } else {\r\n this._root = this.fixPath(_rootPath);\r\n }\r\n\r\n // Resources are located with respect to the engine by default\r\n let resources = pathSpec.resources; // avoid mutating the parameter!\r\n if(resources == '') {\r\n resources = this.sourcePath;\r\n }\r\n\r\n // Convert resource, keyboard and font paths to absolute URLs\r\n this._resources = this.fixPath(resources);\r\n this._keyboards = this.fixPath(pathSpec.keyboards);\r\n this._fonts = this.fixPath(pathSpec.fonts);\r\n }\r\n\r\n // Local function to convert relative to absolute URLs\r\n // with respect to the source path, server root and protocol\r\n fixPath(p: string) {\r\n if(p.length == 0) {\r\n return p;\r\n }\r\n\r\n p = addDelimiter(p);\r\n\r\n // Absolute\r\n if((p.replace(/^(http)s?:.*/,'$1') == 'http') || (p.replace(/^(file):.*/,'$1') == 'file')) {\r\n return p;\r\n }\r\n\r\n // Absolute (except for protocol)\r\n if(p.substring(0,2) == '//') {\r\n return this.protocol + p;\r\n }\r\n\r\n // Relative to server root\r\n if(p.substring(0,1) == '/') {\r\n return this.root + p.substring(1);\r\n }\r\n\r\n // Otherwise, assume relative to source path\r\n return this.sourcePath + p;\r\n }\r\n\r\n get fonts(): string {\r\n return this._fonts;\r\n }\r\n\r\n updateFontPath(path: string) {\r\n this._fonts = this.fixPath(path);\r\n }\r\n\r\n get root(): string {\r\n return this._root;\r\n }\r\n\r\n get resources(): string {\r\n return this._resources;\r\n }\r\n\r\n get keyboards(): string {\r\n return this._keyboards;\r\n }\r\n}", + "export interface PathOptionSpec {\r\n /**\r\n * If defined, specifies the root path of the default location hosting KMW resources.\r\n * Is typically just the protocol + domain name.\r\n */\r\n root?: string;\r\n\r\n /**\r\n * The base path to prepend on relative paths for other types of resources.\r\n */\r\n resources?: string;\r\n\r\n /**\r\n * The base path to prepend on relative paths when loading keyboards.\r\n */\r\n keyboards?: string;\r\n\r\n /**\r\n * The base path to prepend on relative paths when loading fonts.\r\n */\r\n fonts?: string;\r\n}\r\n\r\nexport const PathOptionDefaults: Required = {\r\n root: '',\r\n resources: '',\r\n keyboards: '',\r\n fonts: ''\r\n}", + "import { Suggestion, Reversion } from '@keymanapp/common-types';\r\nimport { EventEmitter } from \"eventemitter3\";\r\nimport { OutputTarget } from \"keyman/engine/keyboard\";\r\n\r\nexport class ReadySuggestions {\r\n suggestions: Suggestion[];\r\n transcriptionID: number;\r\n\r\n constructor(suggestions: Suggestion[], id: number) {\r\n this.suggestions = suggestions;\r\n this.transcriptionID = id;\r\n }\r\n}\r\n\r\n/**\r\n * Corresponds to the 'suggestionsready' LanguageProcessor event.\r\n */\r\nexport type ReadySuggestionsHandler = (prediction: ReadySuggestions) => boolean;\r\n\r\nexport type InvalidateSourceEnum = 'new' | 'context';\r\n\r\n/**\r\n * Corresponds to the 'invalidatesuggestions' LanguageProcessor event.\r\n */\r\nexport type InvalidateSuggestionsHandler = (source: InvalidateSourceEnum) => boolean;\r\n\r\nexport type StateChangeEnum = 'active' | 'configured' | 'inactive';\r\n/**\r\n * Corresponds to the 'statechange' LanguageProcessor event.\r\n */\r\nexport type StateChangeHandler = (state: StateChangeEnum) => any;\r\n\r\n/**\r\n * Covers 'tryaccept' events.\r\n */\r\nexport type TryUIHandler = (source: string, returnObj: { shouldSwallow: boolean }) => boolean;\r\n\r\nexport interface LanguageProcessorEventMap {\r\n 'suggestionsready': ReadySuggestionsHandler,\r\n 'invalidatesuggestions': InvalidateSuggestionsHandler,\r\n 'statechange': StateChangeHandler,\r\n 'tryaccept': TryUIHandler,\r\n 'tryrevert': () => void,\r\n\r\n /**\r\n * Is called synchronously once suggestion application is successful and the context has been updated.\r\n *\r\n * @param outputTarget The `OutputTarget` representation of the context the suggestion was applied to.\r\n * @returns\r\n */\r\n 'suggestionapplied': (outputTarget: OutputTarget) => boolean\r\n}\r\n\r\n\r\nexport interface LanguageProcessorSpec extends EventEmitter {\r\n\r\n get state(): StateChangeEnum;\r\n\r\n invalidateContext(outputTarget: OutputTarget, layerId: string): Promise;\r\n\r\n /**\r\n *\r\n * @param suggestion\r\n * @param outputTarget\r\n * @param getLayerId a function that returns the current layerId,\r\n * required because layerid can be changed by PostKeystroke\r\n * @returns\r\n */\r\n applySuggestion(suggestion: Suggestion, outputTarget: OutputTarget, getLayerId: () => string): Promise;\r\n\r\n applyReversion(reversion: Reversion, outputTarget: OutputTarget): Promise;\r\n\r\n get wordbreaksAfterSuggestions(): boolean;\r\n}\r\n", + "import { EventEmitter } from \"eventemitter3\";\r\nimport { Keep, Reversion, Suggestion } from '@keymanapp/common-types';\r\nimport { type LanguageProcessorSpec , ReadySuggestions, type InvalidateSourceEnum, StateChangeHandler } from './languageProcessor.interface.js';\r\nimport { type OutputTarget } from \"keyman/engine/keyboard\";\r\n\r\ninterface PredictionContextEventMap {\r\n update: (suggestions: Suggestion[]) => void;\r\n}\r\n\r\n/**\r\n * Maintains predictive-text state information corresponding to the current context.\r\n */\r\nexport default class PredictionContext extends EventEmitter {\r\n // Historical note: before 17.0, this code was intertwined with /web/source/osk/banner.ts's\r\n // SuggestionBanner class. This class serves as the main implementation of the banner's core logic.\r\n\r\n // Designed for use with auto-correct behavior\r\n selected: Suggestion;\r\n\r\n private initNewContext: boolean = true;\r\n\r\n private _currentSuggestions: Suggestion[] = [];\r\n private keepSuggestion: Keep;\r\n private revertSuggestion: Reversion;\r\n\r\n // Set to null/undefined if there was no recent acceptance.\r\n private recentAcceptCause: 'key' | 'banner';\r\n private revertAcceptancePromise: Promise;\r\n\r\n private swallowPrediction: boolean = false;\r\n\r\n private doRevert: boolean = false;\r\n private recentRevert: boolean = false;\r\n\r\n private langProcessor: LanguageProcessorSpec;\r\n private getLayerId: () => string;\r\n\r\n /**\r\n * Represents the active context used when requesting and applying predictive-text operations.\r\n */\r\n private _currentTarget: OutputTarget;\r\n\r\n public get currentTarget(): OutputTarget {\r\n return this._currentTarget;\r\n }\r\n\r\n public setCurrentTarget(target: OutputTarget): Promise {\r\n const originalTarget = this._currentTarget;\r\n this._currentTarget = target;\r\n\r\n if(originalTarget != target) {\r\n // Note: should be triggered after the corresponding new-context event rule has been processed,\r\n // as that may affect the value of layerId here.\r\n return this.resetContext();\r\n } else {\r\n return Promise.resolve([]);\r\n }\r\n }\r\n\r\n private readonly suggestionApplier: (suggestion: Suggestion) => Promise;\r\n private readonly suggestionReverter: (reversion: Reversion) => void;\r\n\r\n public constructor(langProcessor: LanguageProcessorSpec, getLayerId: () => string) {\r\n super();\r\n\r\n this.langProcessor = langProcessor;\r\n this.getLayerId = getLayerId;\r\n\r\n const validSuggestionState: () => boolean = () =>\r\n this.currentTarget && langProcessor.state == 'configured';\r\n\r\n this.suggestionApplier = (suggestion) => {\r\n if(validSuggestionState()) {\r\n return langProcessor.applySuggestion(suggestion, this.currentTarget, getLayerId);\r\n } else {\r\n return null;\r\n }\r\n }\r\n\r\n this.suggestionReverter = async (reversion) => {\r\n if(validSuggestionState()) {\r\n let suggestions = await langProcessor.applyReversion(reversion, this.currentTarget);\r\n // We want to avoid altering flags that indicate our post-reversion state.\r\n this.swallowPrediction = true;\r\n this.updateSuggestions(new ReadySuggestions(suggestions, reversion.id ? -reversion.id : undefined));\r\n }\r\n }\r\n\r\n this.connect();\r\n }\r\n\r\n private connect() {\r\n this.langProcessor.addListener('invalidatesuggestions', this.invalidateSuggestions);\r\n this.langProcessor.addListener('suggestionsready', this.updateSuggestions);\r\n this.langProcessor.addListener('tryaccept', this.doTryAccept);\r\n this.langProcessor.addListener('tryrevert', this.doTryRevert);\r\n this.langProcessor.addListener('statechange', this.onModelStateChange);\r\n }\r\n\r\n public disconnect() {\r\n this.langProcessor.removeListener('invalidatesuggestions', this.invalidateSuggestions);\r\n this.langProcessor.removeListener('suggestionsready', this.updateSuggestions);\r\n this.langProcessor.removeListener('tryaccept', this.doTryAccept);\r\n this.langProcessor.removeListener('tryrevert', this.doTryRevert);\r\n this.langProcessor.removeListener('statechange', this.onModelStateChange);\r\n this.clearSuggestions();\r\n }\r\n\r\n public get currentSuggestions(): Suggestion[] {\r\n let suggestions: Suggestion[] = [];\r\n // Insert 'current text' if/when valid as the leading option.\r\n // Since we don't yet do auto-corrections, we only show 'keep' whenever it's\r\n // a valid word (according to the model).\r\n const mayShowKeep = this.activateKeep() && this.keepSuggestion;\r\n\r\n // If there is an auto-select option that doesn't match the current context,\r\n // we need to present the user a way to preserve the current context instead.\r\n const keepNeeded = this.selected && (this.keepSuggestion != this.selected);\r\n\r\n if(mayShowKeep && (keepNeeded || this.keepSuggestion.matchesModel)) {\r\n suggestions.push(this.keepSuggestion);\r\n } else if(this.doRevert) {\r\n suggestions.push(this.revertSuggestion);\r\n }\r\n\r\n return suggestions.concat(this._currentSuggestions);\r\n }\r\n\r\n /**\r\n * Function apply\r\n * Description Applies the predictive `Suggestion` represented by this `BannerSuggestion`.\r\n */\r\n private acceptInternal(suggestion: Suggestion): Promise {\r\n if(!suggestion) {\r\n return null;\r\n }\r\n\r\n // Should be safe to convert into an event handled externally.\r\n // layerID can be obtained by whoever/whatever holds the InputProcessor instance.\r\n if(suggestion.tag == 'revert') {\r\n this.suggestionReverter(suggestion as Reversion);\r\n return null;\r\n } else {\r\n return this.suggestionApplier(suggestion);\r\n }\r\n }\r\n\r\n /**\r\n * Applies predictive-text suggestions and post-acceptance reversions to the current\r\n * prediction context.\r\n *\r\n * Note that both cases will additionally trigger a new asynchronous `predict` operation,\r\n * though no corresponding Promise is returned by this function. As such, the current\r\n * suggestions should be considered outdated after calling this method, pending replacement\r\n * upon the completed async `predict`.\r\n *\r\n * @param suggestion Either a `Suggestion` or `Reversion`.\r\n * @returns if `suggestion` is a `Suggestion`, will return a `Promise`; else, `null`.\r\n */\r\n public accept(suggestion: Suggestion): Promise | Promise {\r\n let _this = this;\r\n\r\n // Selecting a suggestion or a reversion should both clear selection\r\n // and clear the reversion-displaying state of the banner.\r\n this.selected = null;\r\n this.doRevert = false;\r\n\r\n this.revertAcceptancePromise = this.acceptInternal(suggestion);\r\n if(!this.revertAcceptancePromise) {\r\n // We get here either if suggestion acceptance fails or if it was a reversion.\r\n if(suggestion && suggestion.tag == 'revert') {\r\n // Reversion state management\r\n this.recentAcceptCause = null;\r\n this.recentRevert = true;\r\n }\r\n\r\n return Promise.resolve(null);\r\n }\r\n\r\n this.revertAcceptancePromise.then(function(suggestion) {\r\n // Always null-check!\r\n if(suggestion) {\r\n _this.revertSuggestion = suggestion;\r\n }\r\n });\r\n\r\n // By default, we assume we were triggered by the banner.\r\n // Acceptance by keystroke will overwrite this later (in `tryAccept`)\r\n this.recentAcceptCause = 'banner';\r\n this.recentRevert = false;\r\n\r\n this.swallowPrediction = true;\r\n\r\n return this.revertAcceptancePromise;\r\n }\r\n\r\n private showRevert() {\r\n // Construct a 'revert suggestion' to facilitate a reversion UI component.\r\n this.doRevert = true;\r\n this.sendUpdateEvent();\r\n }\r\n\r\n /**\r\n * Receives messages from the keyboard that the 'accept' keystroke has been entered.\r\n * Should return 'false' if the current state allows accepting a suggestion and act accordingly.\r\n * Otherwise, return true.\r\n */\r\n private doTryAccept = (source: string, returnObj: {shouldSwallow: boolean}): void => {\r\n const recentAcceptCause = this.recentAcceptCause;\r\n\r\n if(!recentAcceptCause && this.selected) {\r\n this.accept(this.selected);\r\n // If there is right-context, DO emit the space instead of swallowing it.\r\n // It's not auto-added by the predictive-text worker for such cases.\r\n returnObj.shouldSwallow = !this.currentTarget.getTextAfterCaret();\r\n\r\n // doTryAccept is the path for keystroke-based auto-acceptance.\r\n // Overwrite the cause to reflect this.\r\n this.recentAcceptCause = 'key';\r\n } else if(recentAcceptCause && source == 'space') {\r\n this.recentAcceptCause = null;\r\n if(recentAcceptCause == 'key') {\r\n // No need to swallow the keystroke's whitespace; we triggered the prior acceptance\r\n // FROM a space, so we've already aliased the suggestion's built-in space.\r\n returnObj.shouldSwallow = false;\r\n return;\r\n }\r\n\r\n // Standard whitespace applications from the banner, those we DO want to\r\n // swallow the first time.\r\n //\r\n // If the model doesn't insert wordbreaks, there's no space to alias, so\r\n // don't swallow the space. If it does, we consider that insertion to be\r\n // the results of the first post-accept space.\r\n returnObj.shouldSwallow = !!this.langProcessor.wordbreaksAfterSuggestions && !this.currentTarget.getTextAfterCaret();; // can be handed outside\r\n } else {\r\n returnObj.shouldSwallow = false;\r\n }\r\n }\r\n\r\n /**\r\n * Receives messages from the keyboard that the 'revert' keystroke has been entered.\r\n * Should return 'false' if the current state allows reverting a recently-applied suggestion and act accordingly.\r\n * Otherwise, return true.\r\n */\r\n private doTryRevert = (/*returnObj: {shouldSwallow: boolean}*/): void => {\r\n // Has the revert keystroke (BKSP) already been sent once since the last accept?\r\n if(this.doRevert) {\r\n // If so, clear the 'revert' option and start doing normal predictions again.\r\n this.doRevert = false;\r\n this.recentAcceptCause = null;\r\n // Otherwise, did we just accept something before the revert signal was received?\r\n } else if(this.recentAcceptCause) {\r\n this.showRevert();\r\n this.swallowPrediction = true;\r\n }\r\n\r\n // // We don't yet actually do key-based reversions.\r\n // returnObj.shouldSwallow = false;\r\n return;\r\n }\r\n\r\n /**\r\n * Function invalidateSuggestions\r\n * Scope Public\r\n * Description Clears the suggestions in the suggestion banner\r\n */\r\n private invalidateSuggestions = (source: InvalidateSourceEnum): void => {\r\n // By default, we assume that the context is the same until we notice otherwise.\r\n this.initNewContext = false;\r\n this.selected = null;\r\n\r\n if(!this.swallowPrediction || source == 'context') {\r\n this.recentAcceptCause = null;\r\n this.doRevert = false;\r\n this.recentRevert = false;\r\n\r\n if(source == 'context') {\r\n this.swallowPrediction = false;\r\n this.initNewContext = true;\r\n }\r\n }\r\n\r\n // Not checking this can result in a perceptible 'flash' of sorts due to the suggestion-update delay.\r\n if(source != 'new') {\r\n this.clearSuggestions();\r\n // this.options.forEach((option: BannerSuggestion) => {\r\n // option.update(null);\r\n // });\r\n }\r\n }\r\n\r\n private clearSuggestions() {\r\n this.updateSuggestions({\r\n suggestions: [],\r\n transcriptionID: 0\r\n });\r\n }\r\n\r\n private activateKeep(): boolean {\r\n return !this.recentAcceptCause && !this.recentRevert && !this.initNewContext;\r\n }\r\n\r\n /**\r\n * Function updateSuggestions\r\n * Scope Public\r\n * @param {Suggestion[]} suggestions Array of suggestions from the lexical model.\r\n * Description Update the displayed suggestions in the SuggestionBanner\r\n */\r\n private updateSuggestions = (prediction: ReadySuggestions): void => {\r\n let suggestions = prediction.suggestions;\r\n\r\n this._currentSuggestions = suggestions;\r\n this.selected = null;\r\n\r\n // Do we have a keep suggestion? If so, remove it from the list so that we can control its display position\r\n // and prevent it from being hidden after reversion operations.\r\n this.keepSuggestion = null;\r\n for (let s of suggestions) {\r\n if(s.tag == 'keep') {\r\n this.keepSuggestion = s as Keep;\r\n }\r\n\r\n if(s.autoAccept && !this.selected) {\r\n this.selected = s;\r\n }\r\n }\r\n\r\n if(this.keepSuggestion) {\r\n this._currentSuggestions.splice(this._currentSuggestions.indexOf(this.keepSuggestion), 1);\r\n }\r\n\r\n // If we've gotten an update request like this, it's almost always user-triggered and means the context has shifted.\r\n if(!this.swallowPrediction) {\r\n this.recentAcceptCause = null;\r\n this.doRevert = false;\r\n this.recentRevert = false;\r\n } else { // This prediction was triggered by a recent 'accept.' Now that it's fulfilled, we clear the flag.\r\n this.swallowPrediction = false;\r\n }\r\n\r\n // The rest is the same, whether from input or from \"self-updating\" after a reversion to provide new suggestions.\r\n this.sendUpdateEvent();\r\n }\r\n\r\n public sendUpdateEvent() {\r\n this.emit('update', this.currentSuggestions);\r\n }\r\n\r\n public resetContext(): Promise {\r\n const target = this.currentTarget;\r\n\r\n if(target) {\r\n // Note: should be triggered after the corresponding new-context event rule has been processed,\r\n // as that may affect the value of layerId here.\r\n return this.langProcessor.invalidateContext(target, this.getLayerId());\r\n } else {\r\n return Promise.resolve([]);\r\n }\r\n }\r\n\r\n private onModelStateChange: StateChangeHandler = (state) => {\r\n // Either way, the model has changed; either state marks the completion of such a transition.\r\n // The 'active' state displays the banner while a model loads... but its predictions are\r\n // only possible once fully 'configured'. They may appear to 'blink on' after a small delay\r\n // as a result.\r\n if(state == 'configured' || state == 'inactive') {\r\n this.resetContext();\r\n }\r\n }\r\n}", + "import { DeviceSpec } from \"@keymanapp/web-utils\";\r\n\r\n/*\r\n * This file is intended for CSS-styling constants that see use with the OSK.\r\n */\r\n\r\n/**\r\n * Defines device-level constants used for CSS styling.\r\n */\r\nexport default class StyleConstants {\r\n constructor(device: DeviceSpec) {\r\n // popupCanvasBackgroundColor\r\n if(device.OS == DeviceSpec.OperatingSystem.Android) {\r\n this.popupCanvasBackgroundColor = '#999';\r\n } else {\r\n this.popupCanvasBackgroundColor = StyleConstants.prefersDarkMode() ? '#0f1319' : '#ffffff';\r\n }\r\n }\r\n\r\n /**\r\n * Checks is a user's browser is in dark mode, if the feature is supported. Returns false otherwise.\r\n *\r\n * Thanks to https://stackoverflow.com/a/57795518 for this code.\r\n */\r\n static prefersDarkMode(): boolean {\r\n // Ensure the detector exists (otherwise, returns false)\r\n return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;\r\n }\r\n\r\n public readonly popupCanvasBackgroundColor: string;\r\n}", + "/*\r\n * Keyman is copyright (C) SIL International. MIT License.\r\n *\r\n * Detect the user's device.\r\n */\r\nimport StyleConstants from './utils/styleConstants.js';\r\nimport { DeviceSpec, Version } from \"@keymanapp/web-utils\";\r\n\r\nexport class DeviceDetector {\r\n // These correspond directly to the properties & parameters for `DeviceSpec`.\r\n touchable: boolean;\r\n OS: string;\r\n formFactor: string;\r\n browser: string;\r\n\r\n // These components aren't needed for key events. All but `version` could be a sort\r\n // of `DeviceStyle`.\r\n dyPortrait: number; // Its value is only referenced by an unused method.\r\n dyLandscape: number; // Its value is only referenced by an unused method.\r\n orientation: string|number; // Appears to be unused as well?\r\n colorScheme: 'light' | 'dark'; // Also unused?\r\n version: string; // As in, device version; only really persisted for Android.\r\n // No real sign of actual use, though.\r\n\r\n // Generates a default Device value.\r\n constructor() {\r\n this.touchable = !!('ontouchstart' in window);\r\n this.OS = '';\r\n this.formFactor='desktop';\r\n this.browser='';\r\n\r\n this.dyPortrait=0;\r\n this.dyLandscape=0;\r\n this.version='0';\r\n this.orientation=window.orientation;\r\n }\r\n\r\n /**\r\n * Get device horizontal DPI for touch devices, to set actual size of active regions\r\n * Note that the actual physical DPI may be somewhat different.\r\n *\r\n * @return {number}\r\n */\r\n getDPI(): number {\r\n var t=document.createElement('DIV') ,s=t.style,dpi=96;\r\n if(document.readyState !== 'complete') {\r\n return dpi;\r\n }\r\n\r\n t.id='calculateDPI';\r\n s.position='absolute'; s.display='block';s.visibility='hidden';\r\n s.left='10px'; s.top='10px'; s.width='1in'; s.height='10px';\r\n document.body.appendChild(t);\r\n dpi=(typeof window.devicePixelRatio == 'undefined') ? t.offsetWidth : t.offsetWidth * window.devicePixelRatio;\r\n document.body.removeChild(t);\r\n return dpi;\r\n }\r\n\r\n detect() : DeviceSpec {\r\n var possMacSpoof = false;\r\n\r\n if(navigator && navigator.userAgent) {\r\n var agent=navigator.userAgent;\r\n\r\n if(agent.indexOf('iPad') >= 0) {\r\n this.OS='iOS';\r\n this.formFactor='tablet';\r\n this.dyPortrait=this.dyLandscape=0;\r\n } else if(agent.indexOf('iPhone') >= 0) {\r\n this.OS='iOS';\r\n this.formFactor='phone';\r\n this.dyPortrait=this.dyLandscape=25;\r\n } else if(agent.indexOf('Android') >= 0) {\r\n this.OS='Android';\r\n this.formFactor='phone'; // form factor may be redefined on initialization\r\n this.dyPortrait=75;\r\n this.dyLandscape=25;\r\n try {\r\n var rx=new RegExp(\"(?:Android\\\\s+)(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)\");\r\n this.version=agent.match(rx)[1];\r\n } catch(ex) {}\r\n } else if(agent.indexOf('Linux') >= 0) {\r\n this.OS='Linux';\r\n } else if(agent.indexOf('Macintosh') >= 0) {\r\n // Starting with 13.1, \"Macintosh\" can reflect iPads (by default) or iPhones\r\n // (by user setting); a new \"Request Desktop Website\" setting for Safari will\r\n // change the user agent string to match a desktop Mac.\r\n //\r\n // Firefox uses '.' between version components, while Chrome and Safari use\r\n // '_' instead. So, we have to check for both. Yay.\r\n let regex = /Intel Mac OS X (\\d+(?:[_\\.]\\d+)+)/i;\r\n let results = regex.exec(agent);\r\n\r\n // Match result: a version string with components separated by underscores.\r\n if(!results) {\r\n console.warn(\"KMW could not properly parse the user agent string.\"\r\n + \"A suboptimal keyboard layout may result.\");\r\n this.OS='MacOSX';\r\n } else if(results.length > 1 && results[1]) {\r\n // Convert version string into a usable form.\r\n let versionString = results[1].replace('_', '.');\r\n let version = new Version(versionString);\r\n\r\n possMacSpoof = Version.MAC_POSSIBLE_IPAD_ALIAS.compareTo(version) <= 0;\r\n this.OS='MacOSX';\r\n }\r\n } else if(agent.indexOf('Windows NT') >= 0) {\r\n this.OS='Windows';\r\n if(agent.indexOf('Touch') >= 0) {\r\n this.formFactor='phone'; // will be redefined as tablet if resolution high enough\r\n }\r\n\r\n // Windows Phone and Tablet PC\r\n if(typeof (navigator).msMaxTouchPoints == 'number' && (navigator).msMaxTouchPoints > 0) {\r\n this.touchable=true;\r\n }\r\n }\r\n }\r\n\r\n // We look at the screen resolution for Android, because we can't tell from\r\n // the user agent string whether or not this is supposed to be a tablet.\r\n // It seems that there are a handful of older phones out there that report a\r\n // higher resolution than 700px*___px, but it is proving hard to test these,\r\n // and the majority have an aspect ratio <= 0.5625 anyway.\r\n // But we trust what iOS tells us for phone vs tablet.\r\n\r\n const dimMin = Math.min(screen.width,screen.height), dimMax = Math.max(screen.width,screen.height);\r\n const aspect = dimMin / dimMax;\r\n\r\n if(this.OS != 'iOS' &&\r\n this.formFactor == 'phone' &&\r\n ((dimMin >= 600 && aspect > 0.5625) || // 0.5625 -> 1920x1080 is common phone res\r\n (aspect >= 0.625)) // all reported devices with aspect >= 0.625 are tablets per https://screensiz.es/\r\n ) {\r\n this.formFactor='tablet';\r\n }\r\n\r\n // Test for potential Chrome emulation on Windows or macOS X (used only in next if-check)\r\n let possibleChromeEmulation = navigator.platform == 'Win32' || navigator.platform == 'MacIntel'\r\n\r\n // alert(sxx+'->'+device.formFactor);\r\n // Check for phony iOS devices (but don't undo for Chrome emulation used during development)\r\n if(this.OS == 'iOS' && !('ongesturestart' in window) && !possibleChromeEmulation) {\r\n this.OS='Android';\r\n }\r\n\r\n // Determine application or browser\r\n this.browser='web';\r\n if(this.OS == 'iOS' || this.OS.toLowerCase() == 'macosx') {\r\n this.browser='safari';\r\n }\r\n\r\n var bMatch=/Firefox|Chrome|OPR|Safari|Edge/;\r\n if(bMatch.test(navigator.userAgent)) {\r\n if((navigator.userAgent.indexOf('Firefox') >= 0) && ('onmozorientationchange' in screen)) {\r\n this.browser='firefox';\r\n } else if(navigator.userAgent.indexOf('OPR') >= 0) {\r\n this.browser='opera';\r\n } else if(navigator.userAgent.indexOf(' Edge/') >= 0) {\r\n // Edge is too common a word, so test for Edge/ :)\r\n // Must come before Chrome and Safari test because\r\n // Edge pretends to be both\r\n this.browser='edge';\r\n } else if(navigator.userAgent.indexOf('Chrome') >= 0) {\r\n // This test must come before Safari test because on macOS,\r\n // Chrome also reports \"Safari\"\r\n this.browser='chrome';\r\n } else if(navigator.userAgent.indexOf('Safari') >= 0) {\r\n this.browser='safari';\r\n }\r\n }\r\n\r\n if(possMacSpoof && this.browser == 'safari') {\r\n // Indistinguishable user agent string! We need a different test; fortunately, true macOS\r\n // Safari doesn't support TouchEvents. (Chrome does, though! Hence the filter above.)\r\n if(window['TouchEvent']) {\r\n this.OS='iOS';\r\n this.formFactor='tablet';\r\n this.dyPortrait=this.dyLandscape=0;\r\n\r\n // It's currently impossible to differentiate between iPhone and iPad here\r\n // except for by screen dimensions.\r\n let aspectRatio = screen.height / screen.width;\r\n if(aspectRatio < 1) {\r\n aspectRatio = 1 / aspectRatio;\r\n }\r\n\r\n // iPhones usually have a ratio of 16:9 (or 1.778) or higher, while iPads use 4:3 (or 1.333)\r\n if(aspectRatio > 1.6) {\r\n // Override - we'll treat this device as an iPhone.\r\n this.formFactor = 'phone';\r\n this.dyPortrait=this.dyLandscape=25;\r\n }\r\n }\r\n }\r\n\r\n this.colorScheme = StyleConstants.prefersDarkMode() ? 'dark' : 'light';\r\n\r\n return this.coreSpec;\r\n }\r\n\r\n /**\r\n * Returns a slimmer, web-core compatible version of this object.\r\n */\r\n public get coreSpec(): DeviceSpec {\r\n return new DeviceSpec(this.browser, this.formFactor, this.OS, this.touchable);\r\n }\r\n}\r\n", + "import { EventEmitter } from \"eventemitter3\";\r\n\r\nimport { DeviceSpec, KeyboardProperties, ManagedPromise, physicalKeyDeviceAlias, SpacebarText } from \"keyman/engine/keyboard\";\r\nimport { OutputTarget, RuleBehavior } from 'keyman/engine/js-processor';\r\nimport { PathConfiguration, PathOptionDefaults, PathOptionSpec } from \"keyman/engine/interfaces\";\r\nimport { DeviceDetector } from \"./headless/deviceDetector.js\";\r\nimport { KeyboardStub } from \"keyman/engine/keyboard-storage\";\r\n\r\ninterface EventMap {\r\n 'spacebartext': (mode: SpacebarText) => void;\r\n}\r\n\r\nexport class EngineConfiguration extends EventEmitter {\r\n // The app/webview path replaces this during init, but we expect to have something set for this\r\n // during engine construction, which occurs earlier. So no `readonly`, sadly.\r\n //\r\n // May also be manipulated by Developer's debug-host?\r\n public hostDevice: DeviceSpec;\r\n readonly sourcePath: string;\r\n readonly deferForInitialization: ManagedPromise;\r\n\r\n private _paths: PathConfiguration;\r\n public activateFirstKeyboard: boolean;\r\n private _spacebarText: SpacebarText;\r\n private _stubNamespacer?: (stub: KeyboardStub) => void;\r\n\r\n public applyCacheBusting: boolean = false;\r\n\r\n // sourcePath: see `var sPath =` in kmwbase.ts. It is not obtainable headlessly.\r\n constructor(sourcePath: string, device?: DeviceSpec) {\r\n super();\r\n\r\n if(!device) {\r\n const deviceDetector = new DeviceDetector();\r\n deviceDetector.detect();\r\n\r\n device = deviceDetector.coreSpec;\r\n }\r\n\r\n this.sourcePath = sourcePath;\r\n this.hostDevice = device;\r\n this.deferForInitialization = new ManagedPromise();\r\n }\r\n\r\n initialize(options: Required) {\r\n if(!this._paths) {\r\n this._paths = new PathConfiguration(options, this.sourcePath);\r\n } else {\r\n this._paths.updateFromOptions(options);\r\n }\r\n\r\n if(typeof options.setActiveOnRegister == 'boolean') {\r\n this.activateFirstKeyboard = options.setActiveOnRegister;\r\n } else {\r\n this.activateFirstKeyboard = true;\r\n }\r\n\r\n this._spacebarText = options.spacebarText;\r\n\r\n // Make sure this is accessible to stubs for use in generating display names!\r\n KeyboardProperties.spacebarTextMode = () => this.spacebarText;\r\n }\r\n\r\n finalizeInit() {\r\n this.deferForInitialization.resolve();\r\n }\r\n\r\n get paths() {\r\n return this._paths;\r\n }\r\n\r\n get spacebarText() {\r\n return this._spacebarText;\r\n }\r\n\r\n set spacebarText(value: SpacebarText) {\r\n if(this._spacebarText != value) {\r\n this._spacebarText = value;\r\n this.emit('spacebartext', value);\r\n }\r\n }\r\n\r\n get softDevice(): DeviceSpec {\r\n return this.hostDevice;\r\n }\r\n\r\n get hardDevice(): DeviceSpec {\r\n return physicalKeyDeviceAlias(this.hostDevice);\r\n }\r\n\r\n get stubNamespacer() {\r\n return this._stubNamespacer;\r\n }\r\n\r\n set stubNamespacer(functor: (stub: KeyboardStub) => void) {\r\n this._stubNamespacer = functor;\r\n }\r\n\r\n debugReport(): Record {\r\n return {\r\n hostDevice: this.hostDevice,\r\n initialized: this.deferForInitialization.isResolved\r\n }\r\n }\r\n\r\n /**\r\n * Facilitates implementation of additional functionality for finalized keystroke-event rules\r\n * after postKeystroke takes effect. Any behaviors defined here should be considered 'readonly' in\r\n * terms of context and should instead facilitate integration with the engine's host platform.\r\n * @param ruleBehavior The full effects of keystroke + postkeystroke rules from a processed keystroke.\r\n * @param outputTarget The engine's current source for context\r\n */\r\n onRuleFinalization(ruleBehavior: RuleBehavior, outputTarget: OutputTarget) {};\r\n}\r\n\r\nexport interface InitOptionSpec extends PathOptionSpec {\r\n /**\r\n * If set to true || \"true\" or if left undefined, the engine will automatically select the first available\r\n * keyboard for activation.\r\n *\r\n * Note that keyboards specified locally are synchronously loaded while cloud keyboards are async; as a\r\n * result, a locally-specified keyboard will generally be available \"sooner\", even if added \"later\".\r\n */\r\n setActiveOnRegister?: boolean;\r\n\r\n /**\r\n * Determines the default text shown on the spacebar. If undefined, uses `LANGUAGE_KEYBOARD`\r\n */\r\n spacebarText?: SpacebarText;\r\n}\r\n\r\nexport const InitOptionDefaults: Required = {\r\n setActiveOnRegister: true, // only needed for browser?\r\n spacebarText: SpacebarText.LANGUAGE_KEYBOARD, // useful in both, for OSK config.\r\n ...PathOptionDefaults\r\n}", + "import { EventEmitter } from 'eventemitter3';\r\nimport { ManagedPromise, type Keyboard } from 'keyman/engine/keyboard';\r\nimport { type KeyboardInterface, type OutputTarget } from 'keyman/engine/js-processor';\r\nimport { StubAndKeyboardCache, type KeyboardStub } from 'keyman/engine/keyboard-storage';\r\nimport { PredictionContext } from 'keyman/engine/interfaces';\r\nimport { EngineConfiguration } from './engineConfiguration.js';\r\n\r\ninterface EventMap {\r\n // target, then keyboard.\r\n 'targetchange': (target: OutputTarget) => boolean;\r\n\r\n /**\r\n * This event is raised whenever a keyboard change is requested.\r\n *\r\n * Note that if the keyboard has not been previously loaded, this event will be raised twice.\r\n * 1. Before the keyboard is loaded into Keyman Engine for Web.\r\n * 2. Once the keyboard is loaded, but before it is activated.\r\n * @param metadata The to-be-activated keyboard's properties\r\n * @returns\r\n */\r\n 'beforekeyboardchange': (metadata: KeyboardStub) => void;\r\n\r\n /**\r\n * This event is raised whenever an activating keyboard is being loaded into Keyman Engine for\r\n * the first time in the user's current session, which is an asynchronous operation. It is called\r\n * once the async request is initiated.\r\n * @param metadata The registered properties for the keyboard being asynchronously loaded\r\n * @param onload A Promise that resolves with `null` when loading successfully completes or\r\n * with an `error` if it fails.\r\n * @returns\r\n */\r\n 'keyboardasyncload': (metadata: KeyboardStub, onload: Promise) => void;\r\n\r\n /**\r\n * This event is raised whenever a keyboard is fully activated and set as the current active\r\n * keyboard within Keyman Engine for Web.\r\n * @param kbd\r\n * @returns\r\n */\r\n 'keyboardchange': (kbd: {keyboard: Keyboard, metadata: KeyboardStub}) => void;\r\n}\r\n\r\nexport interface ContextManagerConfiguration {\r\n /**\r\n * A function that resets any state-dependent keyboard key-state information such as\r\n * emulated modifier state and layer id. Also purges the context cache.\r\n * If an `outputTarget` is specified, it will also trigger new-context rule processing.\r\n *\r\n * Does not reset option-stores, variable-stores, etc.\r\n */\r\n readonly resetContext: (outputTarget?: OutputTarget) => void;\r\n\r\n /**\r\n * A predictive-state management object that interfaces the predictive-text banner\r\n * with the active context.\r\n */\r\n readonly predictionContext: PredictionContext;\r\n\r\n /**\r\n * The stub & keyboard curation cache holding preloaded keyboards and metadata useable\r\n * to load those not yet loaded.\r\n */\r\n readonly keyboardCache: StubAndKeyboardCache;\r\n}\r\n\r\ninterface PendingActivation {\r\n target: OutputTarget,\r\n keyboard: Promise,\r\n stub: KeyboardStub;\r\n}\r\n\r\nexport abstract class ContextManagerBase extends EventEmitter {\r\n public static readonly TIMEOUT_THRESHOLD = 10000;\r\n\r\n abstract initialize(): void;\r\n\r\n abstract get activeTarget(): OutputTarget;\r\n\r\n private _predictionContext: PredictionContext;\r\n protected keyboardCache: StubAndKeyboardCache;\r\n private _resetContext: (outputTarget?: OutputTarget) => void;\r\n\r\n private pendingActivations: PendingActivation[] = [];\r\n protected engineConfig: MainConfig;\r\n\r\n get predictionContext(): PredictionContext {\r\n return this._predictionContext;\r\n }\r\n\r\n constructor(engineConfig: MainConfig) {\r\n super();\r\n\r\n this.engineConfig = engineConfig;\r\n }\r\n\r\n configure(config: ContextManagerConfiguration) {\r\n this._resetContext = config.resetContext;\r\n this._predictionContext = config.predictionContext;\r\n this.keyboardCache = config.keyboardCache;\r\n }\r\n\r\n insertText(kbdInterface: KeyboardInterface, Ptext: string, PdeadKey: number) {\r\n // Find the correct output target to manipulate.\r\n const outputTarget = this.activeTarget;\r\n\r\n if(outputTarget != null) {\r\n if(Ptext != null) {\r\n kbdInterface.output(0, outputTarget, Ptext);\r\n }\r\n\r\n if((typeof(PdeadKey)!=='undefined') && (PdeadKey !== null)) {\r\n kbdInterface.deadkeyOutput(0, outputTarget, PdeadKey);\r\n }\r\n\r\n outputTarget.invalidateSelection();\r\n\r\n return true;\r\n }\r\n return false;\r\n }\r\n\r\n resetContext() {\r\n this._resetContext(this.activeTarget);\r\n this.predictionContext.resetContext();\r\n }\r\n\r\n abstract get activeKeyboard(): {keyboard: Keyboard, metadata: KeyboardStub};\r\n\r\n /**\r\n * Determines the 'target' currently used to determine which keyboard should be active.\r\n * When `null`, keyboard-activation operations will affect the global default; otherwise,\r\n * such operations affect only the specified `target`.\r\n *\r\n * This method exists to facilitate independent-keyboard mode operations for specific\r\n * attached elements within the app/browser target. For `app/webview`, this should\r\n * always return a consistent value - likely, `null`.\r\n */\r\n protected abstract currentKeyboardSrcTarget(): OutputTarget;\r\n\r\n /**\r\n * Ensures that newly activated keyboards are set correctly within managed context, possibly\r\n * against inactive output targets.\r\n * @param kbd\r\n * @param target\r\n */\r\n protected abstract activateKeyboardForTarget(kbd: {keyboard: Keyboard, metadata: KeyboardStub}, target: OutputTarget): void;\r\n\r\n /**\r\n * Checks the pending keyboard-activation array for an entry corresponding to the specified\r\n * OutputTarget. If found, also removes the entry for bookkeeping purposes.\r\n * @param target The specific OutputTarget affected by the pending Keyboard activation.\r\n * May be `null`, which corresponds to the global default Keyboard.\r\n * @returns `true` if pending activation is still valid, `false` otherwise.\r\n */\r\n private findAndPopActivation(target: OutputTarget): PendingActivation {\r\n // Array.findIndex requires Chrome 45+. :(\r\n let activationIndex;\r\n for(activationIndex = 0; activationIndex < this.pendingActivations.length; activationIndex++) {\r\n if(this.pendingActivations[activationIndex].target == target) {\r\n break;\r\n }\r\n }\r\n\r\n if(activationIndex == this.pendingActivations.length) {\r\n return null;\r\n }\r\n\r\n return this.pendingActivations.splice(activationIndex, 1)[0];\r\n }\r\n\r\n /**\r\n * Internally registers a pending keyboard-activation's properties, only resolving to a non-null\r\n * activation if it is still the most recent keyboard-activation request that would affect the\r\n * corresponding context.\r\n * @param kbdPromise\r\n * @param metadata\r\n * @param target\r\n * @returns\r\n */\r\n protected async deferredKeyboardActivation(\r\n kbdPromise: Promise,\r\n metadata: KeyboardStub,\r\n target: OutputTarget\r\n ): Promise {\r\n const activation: PendingActivation = {\r\n target: target,\r\n keyboard: kbdPromise,\r\n stub: metadata\r\n };\r\n\r\n // Invalidate existing requests for the specified target.\r\n this.findAndPopActivation(target);\r\n this.pendingActivations.push(activation);\r\n await kbdPromise;\r\n\r\n // The keyboard-load is complete; is the activation still desired?\r\n const activationAfterAwait = this.findAndPopActivation(target);\r\n if(activationAfterAwait == activation) {\r\n return activation;\r\n } else if(activationAfterAwait) {\r\n // Restore the popped element; it doesn't match the current activation attempt.\r\n this.pendingActivations.push(activationAfterAwait);\r\n return null;\r\n } else {\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Specifies the keyboard id and the language code to use when a 'default' keyboard\r\n * must be selected by the engine for fallback behaviors.\r\n */\r\n protected abstract getFallbackStubKey(): {\r\n id: string,\r\n langId: string\r\n };\r\n\r\n /**\r\n * Change active keyboard to keyboard selected by (internal) name and language code\r\n *\r\n * Test if selected keyboard already loaded, and simply update active stub if so.\r\n * Otherwise, insert a script to download and insert the keyboard from the repository\r\n * or user-indicated file location.\r\n *\r\n * @param keyboardId\r\n * @param languageCode\r\n * @param saveCookie\r\n * @returns\r\n */\r\n public async activateKeyboard(keyboardId: string, languageCode?: string, saveCookie?: boolean): Promise {\r\n // TODO: relocate default keyboard behavior here once we can also move core error handling for\r\n // unfound stubs here.\r\n const wasNull = !this.activeKeyboard;\r\n\r\n // If there was a previous activation attempt set and still active for the specified keyboard target,\r\n // cancel it. For exmaple, if the user selects a preloaded keyboard after having tried to select one\r\n // still async-loading, we should go with the later setting - the preloaded one.\r\n this.findAndPopActivation(this.currentKeyboardSrcTarget());\r\n\r\n const activatingKeyboard = this.prepareKeyboardForActivation(keyboardId, languageCode);\r\n\r\n const originalKeyboardTarget = this.currentKeyboardSrcTarget();\r\n\r\n const keyboard = await activatingKeyboard.keyboard;\r\n if(keyboard == null && activatingKeyboard.metadata) {\r\n // The activation was async and was cancelled - either by `beforeKeyboardChange` first-pass\r\n // cancellation or because a different keyboard was requested before completion of the async load.\r\n return false;\r\n }\r\n\r\n /*\r\n * Triggers `beforeKeyboardChange` event if the current context at the time when activation is possible\r\n * would be affected by the requested keyboard change.\r\n * - if a keyboard was asynchronously loaded for this...\r\n * - it is possible for the context (in app/browser) to have changed to a page element in\r\n * \"independent keyboard\" mode (or away from one)\r\n * - This is the \"second\" `beforeKeyboardChange` call - a loaded keyboard may now be activated.\r\n *\r\n * If the now-current context would be unaffected by the keyboard change, we do not raise the corresponding\r\n * event.\r\n */\r\n if(this.currentKeyboardSrcTarget() == originalKeyboardTarget) {\r\n this.emit('beforekeyboardchange', activatingKeyboard.metadata);\r\n }\r\n\r\n let kbdStubPair: { keyboard: Keyboard, metadata: KeyboardStub } = null;\r\n if(keyboard) {\r\n kbdStubPair = {\r\n keyboard: keyboard,\r\n metadata: activatingKeyboard.metadata\r\n };\r\n }\r\n\r\n this.activateKeyboardForTarget(kbdStubPair, originalKeyboardTarget);\r\n\r\n // Only trigger `keyboardchange` events when they will affect the active context.\r\n // (!wasNull || !!keyboard) - blocks events for `null` -> `null` transitions.\r\n // (keyman/keymanweb.com#96)\r\n if(this.currentKeyboardSrcTarget() == originalKeyboardTarget && (!wasNull || !!keyboard)) {\r\n // Will trigger KeymanEngine handler that passes keyboard to the OSK, displays it.\r\n this.emit('keyboardchange', this.activeKeyboard);\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Based on the provided keyboard id and language code, selects and (if necessary) loads the\r\n * corresponding keyboard but does not activate it.\r\n *\r\n * This acts as a helper to `activateKeyboard`, helping to centralize and DRY out the actual\r\n * activation of the requested keyboard. Note that it is a synchronous method and should stay\r\n * that way, though it should return a `Promise` for the activating keyboard.\r\n * @param keyboardId\r\n * @param languageCode\r\n * @returns\r\n */\r\n protected prepareKeyboardForActivation(\r\n keyboardId: string,\r\n languageCode?: string\r\n ): {keyboard: Promise, metadata: KeyboardStub} {\r\n // Set default language code\r\n languageCode ||= '';\r\n\r\n // Check that the saved keyboard is currently registered\r\n let requestedStub: KeyboardStub = null;\r\n if(keyboardId) {\r\n requestedStub = this.keyboardCache.getStub(keyboardId, languageCode);\r\n } else {\r\n languageCode == '';\r\n }\r\n\r\n if(!requestedStub) {\r\n if(keyboardId) {\r\n throw new Error(\"No matching stub has been registered.\");\r\n } else {\r\n return {\r\n keyboard: Promise.resolve(null),\r\n metadata: null\r\n }\r\n }\r\n }\r\n\r\n // Check if current keyboard matches requested keyboard, but not (necessarily) stub\r\n if(this.activeKeyboard?.metadata && keyboardId == this.activeKeyboard.metadata.id) {\r\n const keyboard = this.activeKeyboard.keyboard;\r\n // In this case, the keyboard is loaded; just update the stub.\r\n\r\n return {\r\n keyboard: Promise.resolve(keyboard),\r\n metadata: requestedStub\r\n };\r\n }\r\n\r\n // Determine if the keyboard was previously loaded but is not active; use the cached, pre-loaded version if so.\r\n let keyboard: Keyboard;\r\n if(keyboard = this.keyboardCache.getKeyboardForStub(requestedStub)) {\r\n return {\r\n keyboard: Promise.resolve(keyboard),\r\n metadata: requestedStub\r\n };\r\n } else {\r\n // It's async time - the keyboard is not preloaded within the cache. Use the stub's data to load it.\r\n\r\n // `beforeKeyboardChange` - first call\r\n this.emit('beforekeyboardchange', requestedStub);\r\n\r\n const defermentPromise = this.engineConfig.deferForInitialization.then(() => {\r\n // Provide a Promise for completion of the async load process.\r\n const completionPromise = new ManagedPromise();\r\n this.emit('keyboardasyncload', requestedStub, completionPromise.corePromise);\r\n\r\n let keyboardPromise = this.keyboardCache.fetchKeyboardForStub(requestedStub);\r\n let timeoutPromise = new Promise((resolve, reject) => {\r\n const timeoutMsg = `Sorry, the ${requestedStub.name} keyboard for ${requestedStub.langName} is not currently available.`;\r\n window.setTimeout(() => reject(new Error(timeoutMsg)), ContextManagerBase.TIMEOUT_THRESHOLD);\r\n });\r\n\r\n let combinedPromise = Promise.race([keyboardPromise, timeoutPromise]);\r\n\r\n // Ensure the async-load Promise completes properly.\r\n combinedPromise.then(() => {\r\n completionPromise.resolve(null);\r\n // Prevent any 'unhandled Promise rejection' events that may otherwise occur from the timeout promise.\r\n timeoutPromise.catch(() => {});\r\n });\r\n combinedPromise.catch((err) => {\r\n completionPromise.resolve(err);\r\n throw err;\r\n });\r\n\r\n return combinedPromise;\r\n });\r\n\r\n // Now the fun part: note the original call's parameters as a pending activation.\r\n let promise = this.deferredKeyboardActivation(defermentPromise, requestedStub, this.currentKeyboardSrcTarget());\r\n return {\r\n keyboard: promise.then(async (activation) => {\r\n // Is the activation we requested still pending, or was it cancelled in favor of a\r\n // different activation in some manner?\r\n if(!activation) {\r\n // If the user chose to load a different keyboard afterward that would affect the same\r\n // output target, the activation is no longer valid.\r\n return Promise.resolve(null);\r\n } else {\r\n return defermentPromise;\r\n }\r\n }),\r\n metadata: requestedStub\r\n }\r\n }\r\n }\r\n}", + "import { EventEmitter } from \"eventemitter3\";\r\nimport { Keyboard, KeyMapping, KeyEvent, Codes } from \"keyman/engine/keyboard\";\r\nimport { type RuleBehavior } from 'keyman/engine/js-processor';\r\nimport { KeyEventSourceInterface } from 'keyman/engine/osk';\r\nimport { ModifierKeyConstants } from '@keymanapp/common-types';\r\n\r\ninterface EventMap {\r\n /**\r\n * Designed to pass key events off to any consuming modules/libraries.\r\n */\r\n 'keyevent': (event: KeyEvent, callback?: (result: RuleBehavior, error?: Error) => void) => void\r\n}\r\n\r\nexport default class HardKeyboard extends EventEmitter implements KeyEventSourceInterface { }\r\n\r\nexport function processForMnemonicsAndLegacy(s: KeyEvent, activeKeyboard: Keyboard, baseLayout: string): KeyEvent {\r\n // Mnemonic handling.\r\n if(activeKeyboard && activeKeyboard.isMnemonic) {\r\n // The following will never set a code corresponding to a modifier key, so it's fine to do this,\r\n // which may change the value of Lcode, here.\r\n\r\n s.setMnemonicCode(!!(s.Lmodifiers & ModifierKeyConstants.K_SHIFTFLAG), !!(s.Lmodifiers & ModifierKeyConstants.CAPITALFLAG));\r\n }\r\n\r\n // Other minor physical-keyboard adjustments\r\n if(activeKeyboard && !activeKeyboard.isMnemonic) {\r\n // Positional Layout\r\n\r\n /* 13/03/2007 MCD: Swedish: Start mapping of keystroke to US keyboard */\r\n var Lbase = KeyMapping.languageMap[baseLayout];\r\n if(Lbase && Lbase['k'+s.Lcode]) {\r\n s.Lcode=Lbase['k'+s.Lcode];\r\n }\r\n /* 13/03/2007 MCD: Swedish: End mapping of keystroke to US keyboard */\r\n\r\n // The second conditional component (re 0x60): if CTRL or ALT is held down...\r\n // Do not remap for legacy keyboard compatibility, do not pass Go, do not collect $200.\r\n // This effectively only permits `default` and `shift` for legacy keyboards.\r\n //\r\n // Third: DO, however, track direct presses of any main modifier key. The OSK should\r\n // reflect the current modifier state even for legacy keyboards.\r\n if(!activeKeyboard.definesPositionalOrMnemonic &&\r\n !(s.Lmodifiers & Codes.modifierBitmasks.NON_LEGACY) &&\r\n !s.isModifier) {\r\n // Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic\r\n s = new KeyEvent({\r\n Lcode: KeyMapping._USKeyCodeToCharCode(s),\r\n Lmodifiers: 0,\r\n LisVirtualKey: false,\r\n vkCode: s.Lcode, // Helps to merge OSK and physical keystroke control paths.\r\n Lstates: s.Lstates,\r\n kName: '',\r\n device: s.device,\r\n isSynthetic: false\r\n });\r\n }\r\n }\r\n\r\n return s;\r\n}\r\n// Intended design:\r\n// - KeyEventKeyboard: website-integrated handler for hardware-keystroke input; interprets DOM events.\r\n// - app/web\r\n// - AppPassthroughKeyboard: WebView-hosted forwarding of hardware key events through to the Web engine.\r\n// - app/embed", + "import { Uni_IsSurrogate1, Uni_IsSurrogate2 } from '@keymanapp/common-types';\r\n\r\n/**\r\n * Returns the index for the code point divergence point between two strings, as measured in code\r\n * unit coordinates.\r\n * @param str1\r\n * @param str2\r\n * @param commonSuffix If false, asserts a common prefix to the strings. If true, asserts a common suffix.\r\n * @returns The code unit index within `str1` for the start of the code point not common to both.\r\n *\r\n * Follows the convention of (start, end) substring parameterizations having 'end' be exclusive.\r\n */\r\nexport function findCommonSubstringEndIndex(str1: string, str2: string, commonSuffix: boolean): number {\r\n /**\r\n * The maximum number of iterations to consider; exceeding this would go past a string boundary.\r\n */\r\n const maxInterval = Math.min(str1.length, str2.length);\r\n\r\n /**\r\n * The first valid index within the string.\r\n */\r\n let start: number;\r\n\r\n /**\r\n * The current index within the string under consideration as the divergence point.\r\n */\r\n let index: number;\r\n\r\n /**\r\n * The index at which to terminate the search for a divergence point.\r\n */\r\n let end: number;\r\n\r\n /**\r\n * Index shift per loop iteration.\r\n */\r\n let inc: number;\r\n\r\n /**\r\n * Difference in index for comparison between strings.\r\n * Mostly matters when assuming a common right-hand side.\r\n */\r\n let offset: number;\r\n\r\n if(commonSuffix) {\r\n start = index = str1.length - 1; // e.g. str.length == 10 => start = 9.\r\n end = index - maxInterval; // e.g. maxInterval 8, start 9 => iterate from 9 to 2, end at 1.\r\n inc = -1;\r\n offset = str2.length - str1.length;\r\n } else {\r\n start = index = 0;\r\n end = maxInterval; // last valid index: - 1. e.g. maxInterval 8 => iterate from 0 to 7, end at 8.\r\n inc = 1;\r\n offset = 0;\r\n }\r\n\r\n // Step 1: Find the index for the first code unit different between the strings.\r\n for(; index != end; index += inc) {\r\n if(str1.charAt(index) != str2.charAt(index + offset)) {\r\n break;\r\n }\r\n }\r\n\r\n // Step 2: Ensure that we're not splitting a surrogate pair.\r\n\r\n // `index` corresponds to the first char that is different _in the direction indicated by inc_.\r\n // If it's the start position, it can't split a (completed) surrogate pair.\r\n if(index != start && index != end) {\r\n // if commonLeft, high surrogate; if commonRight, low surrogate.\r\n const commonPotentialSurrogate = str1.charCodeAt(index - inc);\r\n // Opposite surrogate type from the previous variable.\r\n const divergentChar1 = str1.charCodeAt(index);\r\n const divergentChar2 = str2.charCodeAt(index + offset);\r\n\r\n const commonSurrogateChecker = commonSuffix ? Uni_IsSurrogate2 : Uni_IsSurrogate1;\r\n const divergentSurrogateChecker = commonSuffix ? Uni_IsSurrogate1 : Uni_IsSurrogate2;\r\n\r\n // If the last common character if of the direction-appropriate surrogate type (for\r\n // comprising a potential split surrogate pair representing a non-BMP char)...\r\n if(commonSurrogateChecker(commonPotentialSurrogate)) {\r\n // And one of the two divergent chars is a qualifying match - a surrogate\r\n // of the opposite type...\r\n if(divergentSurrogateChecker(divergentChar1) || divergentSurrogateChecker(divergentChar2)) {\r\n // Our current index would split a surrogate pair; decrement the index to\r\n // preserve the pair.\r\n return index - inc;\r\n }\r\n }\r\n }\r\n\r\n return index;\r\n}\r\n", + "// Defines the base Deadkey-tracking object.\r\nexport class Deadkey {\r\n p: number; // Position of deadkey\r\n d: number; // Numerical id of the deadkey\r\n o: number; // Ordinal value of the deadkey (resolves same-place conflicts)\r\n matched: number;\r\n\r\n static ordinalSeed: number = 0;\r\n\r\n constructor(pos: number, id: number) {\r\n this.p = pos;\r\n this.d = id;\r\n this.o = Deadkey.ordinalSeed++;\r\n }\r\n\r\n match(p: number, d: number): boolean {\r\n var result:boolean = (this.p == p && this.d == d);\r\n\r\n return result;\r\n }\r\n\r\n set(): void {\r\n this.matched = 1;\r\n }\r\n\r\n reset(): void {\r\n this.matched = 0;\r\n }\r\n\r\n before(other: Deadkey): boolean {\r\n return this.o < other.o;\r\n }\r\n\r\n clone(): Deadkey {\r\n let dk = new Deadkey(this.p, this.d);\r\n dk.o = this.o;\r\n\r\n return dk;\r\n }\r\n\r\n equal(other: Deadkey) {\r\n return this.d == other.d && this.p == other.d && this.o == other.o;\r\n }\r\n\r\n /**\r\n * Sorts the deadkeys in reverse order.\r\n */\r\n static sortFunc = function(a: Deadkey, b: Deadkey) {\r\n // We want descending order, so we want 'later' deadkeys first.\r\n if(a.p != b.p) {\r\n return b.p - a.p;\r\n } else {\r\n return b.o - a.o;\r\n }\r\n };\r\n}\r\n\r\n// Object-orients deadkey management.\r\nexport class DeadkeyTracker {\r\n dks: Deadkey[] = [];\r\n\r\n toSortedArray(): Deadkey[] {\r\n this.dks = this.dks.sort(Deadkey.sortFunc);\r\n return [].concat(this.dks);\r\n }\r\n\r\n clone(): DeadkeyTracker {\r\n let dkt = new DeadkeyTracker();\r\n let dks = this.toSortedArray();\r\n\r\n // Make sure to clone the deadkeys themselves - the Deadkey object is mutable.\r\n dkt.dks = [];\r\n dks.forEach(function(value: Deadkey) {\r\n dkt.dks.push(value.clone());\r\n });\r\n\r\n return dkt;\r\n }\r\n\r\n /**\r\n * Function isMatch\r\n * Scope Public\r\n * @param {number} caretPos current cursor position\r\n * @param {number} n expected offset of deadkey from cursor\r\n * @param {number} d deadkey\r\n * @return {boolean} True if deadkey found selected context matches val\r\n * Description Match deadkey at current cursor position\r\n */\r\n isMatch(caretPos: number, n: number, d: number): boolean {\r\n if(this.dks.length == 0) {\r\n return false; // I3318\r\n }\r\n\r\n var sp=caretPos;\r\n n = sp - n;\r\n for(var i = 0; i < this.dks.length; i++) {\r\n // Don't re-match an already-matched deadkey. It's possible to have two identical\r\n // entries, and they should be kept separately.\r\n if(this.dks[i].match(n, d) && !this.dks[i].matched) {\r\n this.dks[i].set();\r\n // Assumption: since we match the first possible entry in the array, we\r\n // match the entry with the lower ordinal - the 'first' deadkey in the position.\r\n return true; // I3318\r\n }\r\n }\r\n\r\n this.resetMatched(); // I3318\r\n\r\n return false;\r\n }\r\n\r\n add(dk: Deadkey) {\r\n this.dks = this.dks.concat(dk);\r\n }\r\n\r\n remove(dk: Deadkey) {\r\n var index = this.dks.indexOf(dk);\r\n this.dks.splice(index, 1);\r\n }\r\n\r\n clear() {\r\n this.dks = [];\r\n }\r\n\r\n resetMatched() {\r\n for(let dk of this.dks) {\r\n dk.reset();\r\n }\r\n }\r\n\r\n deleteMatched(): void {\r\n for(var Li = 0; Li < this.dks.length; Li++) {\r\n if(this.dks[Li].matched) {\r\n this.dks.splice(Li--, 1); // Don't forget to decrement!\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Function adjustPositions (formerly _DeadkeyAdjustPos)\r\n * Scope Private\r\n * @param {number} Lstart start position in context\r\n * @param {number} Ldelta characters to adjust by\r\n * Description Adjust saved positions of deadkeys in context\r\n */\r\n adjustPositions(Lstart: number, Ldelta: number): void {\r\n if(Ldelta == 0) {\r\n return;\r\n }\r\n\r\n for(let dk of this.dks) {\r\n if(dk.p > Lstart) {\r\n dk.p += Ldelta;\r\n }\r\n }\r\n }\r\n\r\n equal(other: DeadkeyTracker) {\r\n if(this.dks.length != other.dks.length) {\r\n return false;\r\n }\r\n\r\n const otherDks = other.dks;\r\n const matchedDks: Deadkey[] = [];\r\n\r\n for(let dk of this.dks) {\r\n const match = otherDks.find((otherDk) => dk.equal(otherDk));\r\n if(!match) {\r\n return false;\r\n }\r\n }\r\n\r\n return matchedDks.length == otherDks.length;\r\n }\r\n\r\n count(): number {\r\n return this.dks.length;\r\n }\r\n}", + "import { extendString } from \"@keymanapp/web-utils\";\r\nimport { findCommonSubstringEndIndex } from \"./stringDivergence.js\";\r\nimport { Mock } from \"./mock.js\";\r\nimport { OutputTarget as OutputTargetInterface } from 'keyman/engine/keyboard';\r\n\r\nextendString();\r\n\r\n// Defines deadkey management in a manner attachable to each element interface.\r\nimport { type KeyEvent } from 'keyman/engine/keyboard';\r\nimport { Deadkey, DeadkeyTracker } from \"./deadkeys.js\";\r\nimport { ProbabilityMass, Transform } from '@keymanapp/common-types';\r\n\r\n// Also relies on string-extensions provided by the web-utils package.\r\n\r\nexport function isEmptyTransform(transform: Transform) {\r\n if(!transform) {\r\n return true;\r\n }\r\n return transform.insert === '' && transform.deleteLeft === 0 && (transform.deleteRight ?? 0) === 0;\r\n}\r\n\r\nexport class TextTransform implements Transform {\r\n readonly insert: string;\r\n readonly deleteLeft: number;\r\n readonly deleteRight: number;\r\n readonly erasedSelection: boolean;\r\n id: number;\r\n\r\n constructor(insert: string, deleteLeft: number, deleteRight: number, erasedSelection: boolean) {\r\n this.insert = insert;\r\n this.deleteLeft = deleteLeft;\r\n this.deleteRight = deleteRight;\r\n this.erasedSelection = erasedSelection;\r\n }\r\n\r\n public static readonly nil = new TextTransform('', 0, 0, false);\r\n}\r\n\r\nexport class Transcription {\r\n readonly token: number;\r\n readonly keystroke: KeyEvent;\r\n readonly transform: TextTransform;\r\n alternates: Alternate[]; // constructed after the rest of the transcription.\r\n readonly preInput: Mock;\r\n\r\n private static tokenSeed: number = 0;\r\n\r\n constructor(keystroke: KeyEvent, transform: TextTransform, preInput: Mock, alternates?: Alternate[]/*, removedDks: Deadkey[], insertedDks: Deadkey[]*/) {\r\n let token = this.token = Transcription.tokenSeed++;\r\n\r\n this.keystroke = keystroke;\r\n this.transform = transform;\r\n this.alternates = alternates;\r\n this.preInput = preInput;\r\n\r\n this.transform.id = this.token;\r\n\r\n // Assign the ID to each alternate, as well.\r\n if(alternates) {\r\n alternates.forEach(function(alt) {\r\n alt.sample.id = token;\r\n });\r\n }\r\n }\r\n}\r\n\r\nexport type Alternate = ProbabilityMass;\r\n\r\nexport default abstract class OutputTarget implements OutputTargetInterface {\r\n private _dks: DeadkeyTracker;\r\n\r\n constructor() {\r\n this._dks = new DeadkeyTracker();\r\n }\r\n\r\n /**\r\n * Signifies that this OutputTarget has no default key processing behaviors. This should be false\r\n * for OutputTargets backed by web elements like HTMLInputElement or HTMLTextAreaElement.\r\n */\r\n get isSynthetic(): boolean {\r\n return true;\r\n }\r\n\r\n resetContext(): void {\r\n this.deadkeys().clear();\r\n }\r\n\r\n deadkeys(): DeadkeyTracker {\r\n return this._dks;\r\n }\r\n\r\n hasDeadkeyMatch(n: number, d: number): boolean {\r\n return this.deadkeys().isMatch(this.getDeadkeyCaret(), n, d);\r\n }\r\n\r\n insertDeadkeyBeforeCaret(d: number) {\r\n var dk: Deadkey = new Deadkey(this.getDeadkeyCaret(), d);\r\n this.deadkeys().add(dk);\r\n }\r\n\r\n /**\r\n * Should be called by each output target immediately before text mutation operations occur.\r\n *\r\n * Maintains solutions to old issues: I3318,I3319\r\n * @param {number} delta Use negative values if characters were deleted, positive if characters were added.\r\n */\r\n protected adjustDeadkeys(delta: number) {\r\n this.deadkeys().adjustPositions(this.getDeadkeyCaret(), delta);\r\n }\r\n\r\n /**\r\n * Needed to properly clone deadkeys for use with Mock element interfaces toward predictive text purposes.\r\n * @param {object} dks An existing set of deadkeys to deep-copy for use by this element interface.\r\n */\r\n protected setDeadkeys(dks: DeadkeyTracker) {\r\n this._dks = dks.clone();\r\n }\r\n\r\n /**\r\n * Determines the basic operations needed to reconstruct the current OutputTarget's text from the prior state specified\r\n * by another OutputTarget based on their text and caret positions.\r\n *\r\n * This is designed for use as a \"before and after\" comparison to determine the effect of a single keyboard rule at a time.\r\n * As such, it assumes that the caret is immediately after any inserted text.\r\n * @param from An output target (preferably a Mock) representing the prior state of the input/output system.\r\n */\r\n buildTransformFrom(original: OutputTarget): TextTransform {\r\n const toLeft = this.getTextBeforeCaret();\r\n const fromLeft = original.getTextBeforeCaret();\r\n\r\n const leftDivergenceIndex = findCommonSubstringEndIndex(fromLeft, toLeft, false);\r\n const deletedLeft = fromLeft.substring(leftDivergenceIndex)._kmwLength();\r\n // No need for our specialized variant here.\r\n const insertedText = toLeft.substring(leftDivergenceIndex);\r\n\r\n const toRight = this.getTextAfterCaret();\r\n const fromRight = original.getTextAfterCaret();\r\n const rightDivergenceIndex = findCommonSubstringEndIndex(fromRight, toRight, true);\r\n\r\n // Right insertions aren't supported, but right deletions will matter in some scenarios.\r\n // In particular, once we allow right-deletion for pred-text suggestions applied with the\r\n // caret mid-word..\r\n const deletedRight = fromRight.substring(0, rightDivergenceIndex + 1)._kmwLength();\r\n\r\n return new TextTransform(insertedText, deletedLeft, deletedRight, original.getSelectedText() && !this.getSelectedText());\r\n }\r\n\r\n buildTranscriptionFrom(original: OutputTarget, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription {\r\n let transform = this.buildTransformFrom(original);\r\n\r\n // If we ever decide to re-add deadkey tracking, this is the place for it.\r\n\r\n return new Transcription(keyEvent, transform, Mock.from(original, readonly), alternates);\r\n }\r\n\r\n /**\r\n * Restores the `OutputTarget` to the indicated state. Designed for use with `Transcription.preInput`.\r\n * @param original An `OutputTarget` (usually a `Mock`).\r\n */\r\n restoreTo(original: OutputTarget) {\r\n this.clearSelection();\r\n // We currently do not restore selected text; the mechanism isn't supported at present for\r\n // all output target types - especially in regard to re-selecting the text if restored.\r\n //\r\n // I believe this would mostly matter if/when reverting predictions based upon selected text.\r\n // That pattern isn't well-supported yet, though.\r\n\r\n //\r\n this.setTextBeforeCaret(original.getTextBeforeCaret());\r\n this.setTextAfterCaret(original.getTextAfterCaret());\r\n\r\n // Also, restore the deadkeys!\r\n this._dks = original._dks.clone();\r\n }\r\n\r\n apply(transform: Transform) {\r\n // Selected text should disappear on any text edit; application of a transform\r\n // certainly qualifies.\r\n this.clearSelection();\r\n\r\n if(transform.deleteRight) {\r\n this.setTextAfterCaret(this.getTextAfterCaret()._kmwSubstr(transform.deleteRight));\r\n }\r\n\r\n if(transform.deleteLeft) {\r\n this.deleteCharsBeforeCaret(transform.deleteLeft);\r\n }\r\n\r\n if(transform.insert) {\r\n this.insertTextBeforeCaret(transform.insert);\r\n }\r\n\r\n // We assume that all deadkeys are invalidated after applying a Transform, since\r\n // prediction implies we'll be completing a word, post-deadkeys.\r\n this._dks.clear();\r\n }\r\n\r\n /**\r\n * Helper to `restoreTo` - allows directly setting the 'before' context to that of another\r\n * `OutputTarget`.\r\n * @param s\r\n */\r\n protected setTextBeforeCaret(s: string): void {\r\n // This one's easy enough to provide a default implementation for.\r\n this.deleteCharsBeforeCaret(this.getTextBeforeCaret()._kmwLength());\r\n this.insertTextBeforeCaret(s);\r\n }\r\n\r\n /**\r\n * Helper to `restoreTo` - allows directly setting the 'after' context to that of another\r\n * `OutputTarget`.\r\n * @param s\r\n */\r\n protected abstract setTextAfterCaret(s: string): void;\r\n\r\n /**\r\n * Clears any selected text within the wrapper's element(s).\r\n * Silently does nothing if no such text exists.\r\n */\r\n abstract clearSelection(): void;\r\n\r\n /**\r\n * Clears any cached selection-related state values.\r\n */\r\n abstract invalidateSelection(): void;\r\n\r\n /**\r\n * Indicates whether or not the underlying element has its own selection (input, textarea)\r\n * or is part of (or possesses) the DOM's active selection. Don't confuse with isSelectionEmpty().\r\n *\r\n * TODO: rename to supportsOwnSelection\r\n */\r\n abstract hasSelection(): boolean;\r\n\r\n /**\r\n * Returns true if there is no current selection -- that is, the selection range is empty\r\n */\r\n abstract isSelectionEmpty(): boolean;\r\n\r\n /**\r\n * Returns an index corresponding to the caret's position for use with deadkeys.\r\n */\r\n abstract getDeadkeyCaret(): number;\r\n\r\n /**\r\n * Relative to the caret, gets the current context within the wrapper's element.\r\n */\r\n abstract getTextBeforeCaret(): string;\r\n\r\n /**\r\n * Gets the element's-currently selected text.\r\n */\r\n abstract getSelectedText(): string;\r\n\r\n /**\r\n * Relative to the caret (and/or active selection), gets the element's text after the caret,\r\n * excluding any actively selected text that would be immediately replaced upon text entry.\r\n */\r\n abstract getTextAfterCaret(): string;\r\n\r\n /**\r\n * Gets the element's full text, including any text that is actively selected.\r\n */\r\n abstract getText(): string;\r\n\r\n /**\r\n * Performs context deletions (from the left of the caret) as needed by the KeymanWeb engine and\r\n * corrects the location of any affected deadkeys.\r\n *\r\n * Does not delete deadkeys (b/c KMW 1 & 2 behavior maintenance).\r\n * @param dn The number of characters to delete. If negative, context will be left unchanged.\r\n */\r\n abstract deleteCharsBeforeCaret(dn: number): void;\r\n\r\n /**\r\n * Inserts text immediately before the caret's current position, moving the caret after the\r\n * newly inserted text in the process along with any affected deadkeys.\r\n *\r\n * @param s Text to insert before the caret's current position.\r\n */\r\n abstract insertTextBeforeCaret(s: string): void;\r\n\r\n /**\r\n * Allows element-specific handling for ENTER key inputs. Conceptually, this should usually\r\n * correspond to `insertTextBeforeCaret('\\n'), but actual implementation will vary greatly among\r\n * elements.\r\n */\r\n abstract handleNewlineAtCaret(): void;\r\n\r\n /**\r\n * Saves element-specific state properties prone to mutation, enabling restoration after\r\n * text-output operations.\r\n */\r\n saveProperties() {\r\n // Most element interfaces won't need anything here.\r\n }\r\n\r\n /**\r\n * Restores previously-saved element-specific state properties. Designed for use after text-output\r\n * ops to facilitate more-seamless web-dev and user interactions.\r\n */\r\n restoreProperties(){\r\n // Most element interfaces won't need anything here.\r\n }\r\n\r\n /**\r\n * Generates a synthetic event on the underlying element, signalling that its value has changed.\r\n */\r\n abstract doInputEvent(): void;\r\n}\r\n", + "import OutputTarget from './outputTarget.js';\r\n\r\nexport class Mock extends OutputTarget {\r\n text: string;\r\n\r\n selStart: number;\r\n selEnd: number;\r\n selForward: boolean = true;\r\n\r\n constructor(text?: string, caretPos?: number);\r\n constructor(text?: string, selStart?: number, selEnd?: number);\r\n constructor(text?: string, selStart?: number, selEnd?: number) {\r\n super();\r\n\r\n this.text = text ? text : \"\";\r\n var defaultLength = this.text._kmwLength();\r\n\r\n // Ensures that `caretPos == 0` is handled correctly.\r\n this.selStart = typeof selStart == \"number\" ? selStart : defaultLength;\r\n\r\n // If no selection-end is set, selection length is implied to be 0.\r\n this.selEnd = typeof selEnd == \"number\" ? selEnd : this.selStart;\r\n\r\n this.selForward = this.selEnd >= this.selStart;\r\n }\r\n\r\n // Clones the state of an existing EditableElement, creating a Mock version of its state.\r\n static from(outputTarget: OutputTarget, readonly?: boolean) {\r\n let clone: Mock;\r\n\r\n if (outputTarget instanceof Mock) {\r\n // Avoids the need to run expensive kmwstring.ts / `_kmwLength()`\r\n // calculations when deep-copying Mock instances.\r\n let priorMock = outputTarget as Mock;\r\n clone = new Mock(priorMock.text, priorMock.selStart, priorMock.selEnd);\r\n } else {\r\n const text = outputTarget.getText();\r\n const textLen = text._kmwLength();\r\n\r\n // If !hasSelection()\r\n let selectionStart: number = textLen;\r\n let selectionEnd: number = 0;\r\n\r\n if (outputTarget.hasSelection()) {\r\n let beforeText = outputTarget.getTextBeforeCaret();\r\n let afterText = outputTarget.getTextAfterCaret();\r\n selectionStart = beforeText._kmwLength();\r\n selectionEnd = textLen - afterText._kmwLength();\r\n }\r\n\r\n // readonly group or not, the returned Mock remains the same.\r\n // New-context events should act as if the caret were at the earlier-in-context\r\n // side of the selection, same as standard keyboard rules.\r\n clone = new Mock(text, selectionStart, selectionEnd);\r\n }\r\n\r\n // Also duplicate deadkey state! (Needed for fat-finger ops.)\r\n clone.setDeadkeys(outputTarget.deadkeys());\r\n\r\n return clone;\r\n }\r\n\r\n clearSelection(): void {\r\n this.text = this.getTextBeforeCaret() + this.getTextAfterCaret();\r\n this.selEnd = this.selStart;\r\n this.selForward = true;\r\n }\r\n\r\n invalidateSelection(): void {\r\n return;\r\n }\r\n\r\n isSelectionEmpty(): boolean {\r\n return this.selStart == this.selEnd;\r\n }\r\n\r\n hasSelection(): boolean {\r\n return true;\r\n }\r\n\r\n getDeadkeyCaret(): number {\r\n return this.selStart;\r\n }\r\n\r\n setSelection(start: number, end?: number) {\r\n this.selStart = start;\r\n this.selEnd = typeof end == 'number' ? end : start;\r\n\r\n this.selForward = end >= start;\r\n if (!this.selForward) {\r\n let temp = this.selStart;\r\n this.selStart = this.selEnd;\r\n this.selEnd = temp;\r\n }\r\n }\r\n\r\n getTextBeforeCaret(): string {\r\n return this.text.kmwSubstr(0, this.selStart);\r\n }\r\n\r\n getSelectedText(): string {\r\n return this.text.kmwSubstr(this.selStart, this.selEnd - this.selStart);\r\n }\r\n\r\n getTextAfterCaret(): string {\r\n return this.text.kmwSubstr(this.selEnd);\r\n }\r\n\r\n getText(): string {\r\n return this.text;\r\n }\r\n\r\n deleteCharsBeforeCaret(dn: number): void {\r\n if (dn >= 0) {\r\n if (dn > this.selStart) {\r\n dn = this.selStart;\r\n }\r\n this.adjustDeadkeys(-dn);\r\n this.text = this.text.kmwSubstr(0, this.selStart - dn) + this.text.kmwSubstr(this.selStart);\r\n this.selStart -= dn;\r\n this.selEnd -= dn;\r\n }\r\n }\r\n\r\n insertTextBeforeCaret(s: string): void {\r\n this.adjustDeadkeys(s._kmwLength());\r\n this.text = this.getTextBeforeCaret() + s + this.text.kmwSubstr(this.selStart);\r\n this.selStart += s.kmwLength();\r\n this.selEnd += s.kmwLength();\r\n }\r\n\r\n handleNewlineAtCaret(): void {\r\n this.insertTextBeforeCaret('\\n');\r\n }\r\n\r\n protected setTextAfterCaret(s: string): void {\r\n this.text = this.getTextBeforeCaret() + s;\r\n }\r\n\r\n /**\r\n * Indicates if this Mock represents an identical context to that of another Mock.\r\n * @param other\r\n * @returns\r\n */\r\n isEqual(other: Mock) {\r\n return this.text == other.text\r\n && this.selStart == other.selStart\r\n && this.selEnd == other.selEnd\r\n && this.deadkeys().equal(other.deadkeys());\r\n }\r\n\r\n doInputEvent() {\r\n // Mock isn't backed by an element, so it won't have any event listeners.\r\n }\r\n}\r\n", + "import KeyboardProcessor from \"./keyboardProcessor.js\";\r\nimport { VariableStoreDictionary } from \"keyman/engine/keyboard\";\r\nimport OutputTarget, { type Transcription } from './outputTarget.js';\r\nimport { Mock } from \"./mock.js\";\r\nimport { type VariableStore } from \"./systemStores.js\";\r\nimport { Suggestion } from '@keymanapp/common-types';\r\n\r\n/**\r\n * Represents the commands and state changes that result from a matched keyboard rule.\r\n */\r\nexport default class RuleBehavior {\r\n /**\r\n * The before-and-after Transform from matching a keyboard rule. May be `null`\r\n * if no keyboard rules were matched for the keystroke.\r\n */\r\n transcription: Transcription = null;\r\n\r\n /**\r\n * Indicates whether or not a BEEP command was issued by the matched keyboard rule.\r\n */\r\n beep?: boolean;\r\n\r\n /**\r\n * A set of changed store values triggered by the matched keyboard rule.\r\n */\r\n setStore: {[id: number]: string} = {};\r\n\r\n /**\r\n * A set of variable stores with save requests triggered by the matched keyboard rule\r\n */\r\n saveStore: {[name: string]: VariableStore} = {};\r\n\r\n /**\r\n * A set of variable stores with possible changes to be applied during finalization.\r\n */\r\n variableStores: VariableStoreDictionary = {};\r\n\r\n /**\r\n * Denotes a non-output default behavior; this should be evaluated later, against the true keystroke.\r\n */\r\n triggersDefaultCommand: boolean = false;\r\n\r\n /**\r\n * Denotes error log messages generated when attempting to generate this behavior.\r\n */\r\n errorLog?: string;\r\n\r\n /**\r\n * Denotes warning log messages generated when attempting to generate this behavior.\r\n */\r\n warningLog?: string;\r\n\r\n /**\r\n * If predictive text is active, contains a Promise returning predictive Suggestions.\r\n */\r\n predictionPromise?: Promise;\r\n\r\n /**\r\n * In reference to https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852:\r\n *\r\n * If the final group processed is a context and keystroke group (using keys),\r\n * and there is no nomatch rule, and the keystroke is not matched in the group,\r\n * the keystroke's default behavior should trigger, regardless of whether or not any\r\n * rules in prior groups matched.\r\n */\r\n triggerKeyDefault?: boolean;\r\n\r\n finalize(processor: KeyboardProcessor, outputTarget: OutputTarget, readonly: boolean) {\r\n if(!this.transcription) {\r\n throw \"Cannot finalize a RuleBehavior with no transcription.\";\r\n }\r\n\r\n if(processor.beepHandler && this.beep) {\r\n processor.beepHandler(outputTarget);\r\n }\r\n\r\n for(let storeID in this.setStore) {\r\n let sysStore = processor.keyboardInterface.systemStores[storeID];\r\n if(sysStore) {\r\n try {\r\n sysStore.set(this.setStore[storeID]);\r\n } catch (error) {\r\n if(processor.errorLogger) {\r\n processor.errorLogger(\"Rule attempted to perform illegal operation - 'platform' may not be changed.\");\r\n }\r\n }\r\n } else if(processor.warningLogger) {\r\n processor.warningLogger(\"Unknown store affected by keyboard rule: \" + storeID);\r\n }\r\n }\r\n\r\n processor.keyboardInterface.applyVariableStores(this.variableStores);\r\n\r\n if(processor.keyboardInterface.variableStoreSerializer) {\r\n for(let storeID in this.saveStore) {\r\n processor.keyboardInterface.variableStoreSerializer.saveStore(processor.activeKeyboard.id, storeID, this.saveStore[storeID]);\r\n }\r\n }\r\n\r\n if(this.triggersDefaultCommand) {\r\n let keyEvent = this.transcription.keystroke;\r\n processor.defaultRules.applyCommand(keyEvent, outputTarget);\r\n }\r\n\r\n if(processor.warningLogger && this.warningLog) {\r\n processor.warningLogger(this.warningLog);\r\n } else if(processor.errorLogger && this.errorLog) {\r\n processor.errorLogger(this.errorLog);\r\n }\r\n }\r\n\r\n /**\r\n * Merges default-related behaviors from another RuleBehavior into this one. Assumes that the current instance\r\n * \"came first\" chronologically. Both RuleBehaviors must be sourced from the same keystroke.\r\n *\r\n * Intended use: merging rule-based behavior with default key behavior during scenarios like those described\r\n * at https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852.\r\n *\r\n * This function does not attempt a \"complete\" merge for two fully-constructed RuleBehaviors! Things\r\n * WILL break for unintended uses.\r\n * @param other\r\n */\r\n mergeInDefaults(other: RuleBehavior) {\r\n let keystroke = this.transcription.keystroke;\r\n let keyFromOther = other.transcription.keystroke;\r\n if(keystroke.Lcode != keyFromOther.Lcode || keystroke.Lmodifiers != keyFromOther.Lmodifiers) {\r\n throw \"RuleBehavior default-merge not supported unless keystrokes are identical!\";\r\n }\r\n\r\n this.triggersDefaultCommand = this.triggersDefaultCommand || other.triggersDefaultCommand;\r\n\r\n let mergingMock = Mock.from(this.transcription.preInput, false);\r\n mergingMock.apply(this.transcription.transform);\r\n mergingMock.apply(other.transcription.transform);\r\n\r\n this.transcription = mergingMock.buildTranscriptionFrom(this.transcription.preInput, keystroke, false, this.transcription.alternates);\r\n }\r\n}", + "import { type KeyboardHarness } from 'keyman/engine/keyboard';\r\nimport { StoreNonCharEntry } from './kbdInterface.js';\r\n\r\nexport enum SystemStoreIDs {\r\n TSS_LAYER = 33,\r\n TSS_PLATFORM = 31,\r\n TSS_NEWLAYER = 42,\r\n TSS_OLDLAYER = 43\r\n}\r\n\r\n/*\r\n* Type alias definitions to reflect the parameters of the fullContextMatch() callback (KMW 10+).\r\n* No constructors or methods since keyboards will not utilize the same backing prototype, and\r\n* property names are shorthanded to promote minification.\r\n*/\r\ntype PlainKeyboardStore = string;\r\n\r\nexport type KeyboardStoreElement = (string | StoreNonCharEntry);\r\nexport type ComplexKeyboardStore = KeyboardStoreElement[];\r\n\r\nexport type KeyboardStore = PlainKeyboardStore | ComplexKeyboardStore;\r\n\r\nexport type VariableStore = { [name: string]: string };\r\n\r\nexport interface VariableStoreSerializer {\r\n loadStore(keyboardID: string, storeName: string): VariableStore;\r\n saveStore(keyboardID: string, storeName: string, storeMap: VariableStore): void;\r\n}\r\n\r\n/**\r\n * Defines common behaviors associated with system stores.\r\n */\r\nexport abstract class SystemStore {\r\n public readonly id: number;\r\n\r\n constructor(id: number) {\r\n this.id = id;\r\n }\r\n\r\n abstract matches(value: string): boolean;\r\n\r\n set(value: string): void {\r\n throw new Error(\"System store with ID \" + this.id + \" may not be directly set.\");\r\n }\r\n}\r\n\r\n/**\r\n * A handler designed to receive feedback whenever a system store's value is changed.\r\n * @param source The system store being mutated, before the value change occurs.\r\n * @param newValue The new value being set\r\n * @returns `false` / `undefined` to allow the change, `true` to block the change.\r\n */\r\nexport type SystemStoreMutationHandler = (source: MutableSystemStore, newValue: string) => boolean;\r\n\r\nexport class MutableSystemStore extends SystemStore {\r\n private _value: string;\r\n handler?: SystemStoreMutationHandler = null;\r\n\r\n constructor(id: number, defaultValue: string) {\r\n super(id);\r\n this._value = defaultValue;\r\n }\r\n\r\n get value() {\r\n return this._value;\r\n }\r\n\r\n matches(value: string) {\r\n return this._value == value;\r\n }\r\n\r\n set(value: string) {\r\n // Even if things stay the same, we should still signal this.\r\n // It's important for tracking if a rule directly set the layer\r\n // versus if it passively remained.\r\n if(this.handler) {\r\n if(this.handler(this, value)) {\r\n return;\r\n }\r\n }\r\n\r\n this._value = value;\r\n }\r\n}\r\n\r\n/**\r\n * Handles checks against the current platform.\r\n */\r\nexport class PlatformSystemStore extends SystemStore {\r\n private readonly kbdInterface: KeyboardHarness;\r\n\r\n constructor(keyboardInterface: KeyboardHarness) {\r\n super(SystemStoreIDs.TSS_PLATFORM);\r\n\r\n this.kbdInterface = keyboardInterface;\r\n }\r\n\r\n matches(value: string) {\r\n var i,constraint,constraints=value.split(' ');\r\n let device = this.kbdInterface.activeDevice;\r\n\r\n for(i=0; i void;\r\n\r\n /**\r\n * Function registerKeyboard KR\r\n * Scope Public\r\n * @param {Object} Pk Keyboard object\r\n * Description Registers a keyboard with KeymanWeb once its script has fully loaded.\r\n *\r\n * In web-core, this also activates the keyboard; in other modules, this method\r\n * may be replaced with other implementations.\r\n */\r\n registerKeyboard(Pk: any): void {\r\n // NOTE: This implementation is web-core specific and is intentionally replaced, whole-sale,\r\n // by DOM-aware code.\r\n let keyboard = new Keyboard(Pk);\r\n this.loadedKeyboard = keyboard;\r\n }\r\n\r\n /**\r\n * Get *cached or uncached* keyboard context for a specified range, relative to caret\r\n *\r\n * @param {number} n Number of characters to move back from caret\r\n * @param {number} ln Number of characters to return\r\n * @param {Object} Pelem Element to work with (must be currently focused element)\r\n * @return {string} Context string\r\n *\r\n * Example [abcdef|ghi] as INPUT, with the caret position marked by |:\r\n * KC(2,1,Pelem) == \"e\"\r\n * KC(3,3,Pelem) == \"def\"\r\n * KC(10,10,Pelem) == \"abcdef\" i.e. return as much as possible of the requested string\r\n */\r\n\r\n context(n: number, ln: number, outputTarget: OutputTarget): string {\r\n var v = this.cachedContext.get(n, ln);\r\n if(v !== null) {\r\n return v;\r\n }\r\n\r\n var r = this.KC_(n, ln, outputTarget);\r\n this.cachedContext.set(n, ln, r);\r\n return r;\r\n }\r\n\r\n /**\r\n * Get (uncached) keyboard context for a specified range, relative to caret\r\n *\r\n * @param {number} n Number of characters to move back from caret\r\n * @param {number} ln Number of characters to return\r\n * @param {Object} Pelem Element to work with (must be currently focused element)\r\n * @return {string} Context string\r\n *\r\n * Example [abcdef|ghi] as INPUT, with the caret position marked by |:\r\n * KC(2,1,Pelem) == \"e\"\r\n * KC(3,3,Pelem) == \"def\"\r\n * KC(10,10,Pelem) == \"XXXXabcdef\" i.e. return as much as possible of the requested string, where X = \\uFFFE\r\n */\r\n private KC_(n: number, ln: number, outputTarget: OutputTarget): string {\r\n var tempContext = '';\r\n\r\n // If we have a selection, we have an empty context\r\n tempContext = outputTarget.isSelectionEmpty() ? outputTarget.getTextBeforeCaret() : \"\";\r\n\r\n if(tempContext._kmwLength() < n) {\r\n tempContext = Array(n-tempContext._kmwLength()+1).join(\"\\uFFFE\") + tempContext;\r\n }\r\n\r\n return tempContext._kmwSubstr(-n)._kmwSubstr(0,ln);\r\n }\r\n\r\n /**\r\n * Function nul KN\r\n * Scope Public\r\n * @param {number} n Length of context to check\r\n * @param {Object} Ptarg Element to work with (must be currently focused element)\r\n * @return {boolean} True if length of context is less than or equal to n\r\n * Description Test length of context, return true if the length of the context is less than or equal to n\r\n *\r\n * Example [abc|def] as INPUT, with the caret position marked by |:\r\n * KN(3,Pelem) == TRUE\r\n * KN(2,Pelem) == FALSE\r\n * KN(4,Pelem) == TRUE\r\n */\r\n nul(n: number, outputTarget: OutputTarget): boolean {\r\n var cx=this.context(n+1, 1, outputTarget);\r\n\r\n // With #31, the result will be a replacement character if context is empty.\r\n return cx === \"\\uFFFE\";\r\n }\r\n\r\n /**\r\n * Function contextMatch KCM\r\n * Scope Public\r\n * @param {number} n Number of characters to move back from caret\r\n * @param {Object} Ptarg Focused element\r\n * @param {string} val String to match\r\n * @param {number} ln Number of characters to return\r\n * @return {boolean} True if selected context matches val\r\n * Description Test keyboard context for match\r\n */\r\n contextMatch(n: number, outputTarget: OutputTarget, val: string, ln: number): boolean {\r\n var cx=this.context(n, ln, outputTarget);\r\n if(cx === val) {\r\n return true; // I3318\r\n }\r\n outputTarget.deadkeys().resetMatched(); // I3318\r\n return false;\r\n }\r\n\r\n /**\r\n * Builds the *cached or uncached* keyboard context for a specified range, relative to caret\r\n *\r\n * @param {number} n Number of characters to move back from caret\r\n * @param {number} ln Number of characters to return\r\n * @param {Object} Pelem Element to work with (must be currently focused element)\r\n * @return {Array} Context array (of strings and numbers)\r\n */\r\n private _BuildExtendedContext(n: number, ln: number, outputTarget: OutputTarget): CachedExEntry {\r\n var cache: CachedExEntry = this.cachedContextEx.get(n, ln);\r\n if(cache !== null) {\r\n return cache;\r\n } else {\r\n // By far the easiest way to correctly build what we want is to start from the right and work to what we need.\r\n // We may have done it for a similar cursor position before.\r\n cache = this.cachedContextEx.get(n, n);\r\n if(cache === null) {\r\n // First, let's make sure we have a cloned, sorted copy of the deadkey array.\r\n let unmatchedDeadkeys = outputTarget.deadkeys().toSortedArray(); // Is reverse-order sorted for us already.\r\n\r\n // Time to build from scratch!\r\n var index = 0;\r\n cache = { valContext: [], deadContext: []};\r\n while(cache.valContext.length < n) {\r\n // As adapted from `deadkeyMatch`.\r\n var sp = outputTarget.getDeadkeyCaret();\r\n var deadPos = sp - index;\r\n if(unmatchedDeadkeys.length > 0 && unmatchedDeadkeys[0].p > deadPos) {\r\n // We have deadkeys at the right-hand side of the caret! They don't belong in the context, so pop 'em off.\r\n unmatchedDeadkeys.splice(0, 1);\r\n continue;\r\n } else if(unmatchedDeadkeys.length > 0 && unmatchedDeadkeys[0].p == deadPos) {\r\n // Take the deadkey.\r\n cache.deadContext[n-cache.valContext.length-1] = unmatchedDeadkeys[0];\r\n cache.valContext = ([unmatchedDeadkeys[0].d] as (string|number)[]).concat(cache.valContext);\r\n unmatchedDeadkeys.splice(0, 1);\r\n } else {\r\n // Take the character. We get \"\\ufffe\" if it doesn't exist.\r\n var kc = this.context(++index, 1, outputTarget);\r\n cache.valContext = ([kc] as (string|number)[]).concat(cache.valContext);\r\n }\r\n }\r\n this.cachedContextEx.set(n, n, cache);\r\n }\r\n\r\n // Now that we have the cache...\r\n var subCache = cache;\r\n subCache.valContext = subCache.valContext.slice(0, ln);\r\n for(var i=0; i < subCache.valContext.length; i++) {\r\n if(subCache.valContext[i] == '\\ufffe') {\r\n subCache.valContext.splice(0, 1);\r\n subCache.deadContext.splice(0, 1);\r\n }\r\n }\r\n\r\n if(subCache.valContext.length == 0) {\r\n subCache.valContext = ['\\ufffe'];\r\n subCache.deadContext = [];\r\n }\r\n\r\n this.cachedContextEx.set(n, ln, subCache);\r\n\r\n return subCache;\r\n }\r\n }\r\n\r\n /**\r\n * Function fullContextMatch KFCM\r\n * Scope Private\r\n * @param {number} n Number of characters to move back from caret\r\n * @param {Object} Ptarg Focused element\r\n * @param {Array} rule An array of ContextEntries to match.\r\n * @return {boolean} True if the fully-specified rule context matches the current KMW state.\r\n *\r\n * A KMW 10+ function designed to bring KMW closer to Keyman Desktop functionality,\r\n * near-directly modeling (externally) the compiled form of Desktop rules' context section.\r\n */\r\n fullContextMatch(n: number, outputTarget: OutputTarget, rule: ContextEntry[]): boolean {\r\n // Stage one: build the context index map.\r\n var fullContext = this._BuildExtendedContext(n, rule.length, outputTarget);\r\n this.ruleContextEx = this.cachedContextEx.clone();\r\n var context = fullContext.valContext;\r\n var deadContext = fullContext.deadContext;\r\n\r\n var mismatch = false;\r\n\r\n // This symbol internally indicates lack of context in a position. (See KC_)\r\n const NUL_CONTEXT = \"\\uFFFE\";\r\n\r\n var assertNever = function(x: never): never {\r\n // Could be accessed by improperly handwritten calls to `fullContextMatch`.\r\n throw new Error(\"Unexpected object in fullContextMatch specification: \" + x);\r\n }\r\n\r\n // Stage two: time to match against the rule specified.\r\n for(var i=0; i < rule.length; i++) {\r\n if(typeof rule[i] == 'string') {\r\n var str = rule[i] as string;\r\n if(str !== context[i]) {\r\n mismatch = true;\r\n break;\r\n }\r\n } else {\r\n // TypeScript needs a cast to this intermediate type to do its discriminated union magic.\r\n var r = rule[i] as ContextNonCharEntry;\r\n switch(r.t) {\r\n case 'd':\r\n // We still need to set a flag here;\r\n if(r['d'] !== context[i]) {\r\n mismatch = true;\r\n } else {\r\n deadContext[i].set();\r\n }\r\n break;\r\n case 'a':\r\n var lookup: KeyboardStoreElement;\r\n\r\n if(typeof context[i] == 'string') {\r\n lookup = context[i] as string;\r\n } else {\r\n lookup = {'t': 'd', 'd': context[i] as number};\r\n }\r\n\r\n var result = this.any(i, lookup, r.a);\r\n\r\n if(!r.n) { // If it's a standard 'any'...\r\n if(!result) {\r\n mismatch = true;\r\n } else if(deadContext[i] !== undefined) {\r\n // It's a deadkey match, so indicate that.\r\n deadContext[i].set();\r\n }\r\n // 'n' for 'notany'.\r\n // - if `result === true`, `any` would match: this should thus fail.\r\n // - if `context[i] === NUL_CONTEXT`, `notany` should not match.\r\n } else if(r.n && (result || context[i] === NUL_CONTEXT)) {\r\n mismatch = true;\r\n }\r\n break;\r\n case 'i':\r\n // The context will never hold a 'beep.'\r\n var ch = this._Index(r.i, r.o) as string | RuleDeadkey;\r\n\r\n if(ch !== undefined && (typeof(ch) == 'string' ? ch : ch.d) !== context[i]) {\r\n mismatch = true;\r\n } else if(deadContext[i] !== undefined) {\r\n deadContext[i].set();\r\n }\r\n break;\r\n case 'c':\r\n if(context[r.c - 1] !== context[i]) {\r\n mismatch = true;\r\n } else if(deadContext[i] !== undefined) {\r\n deadContext[i].set();\r\n }\r\n break;\r\n case 'n':\r\n // \\uFFFE is the internal 'no context here sentinel'.\r\n if(context[i] != NUL_CONTEXT) {\r\n mismatch = true;\r\n }\r\n break;\r\n default:\r\n assertNever(r);\r\n }\r\n }\r\n }\r\n\r\n if(mismatch) {\r\n // Reset the matched 'any' indices, if any.\r\n outputTarget.deadkeys().resetMatched();\r\n this._AnyIndices = [];\r\n }\r\n\r\n return !mismatch;\r\n }\r\n\r\n /**\r\n * Function KIK\r\n * Scope Public\r\n * @param {Object} e keystroke event\r\n * @return {boolean} true if keypress event\r\n * Description Test if event as a keypress event\r\n */\r\n isKeypress(e: KeyEvent): boolean {\r\n if(this.activeKeyboard.isMnemonic) { // I1380 - support KIK for positional layouts\r\n return !e.LisVirtualKey; // will now return true for U_xxxx keys, but not for T_xxxx keys\r\n } else {\r\n return KeyMapping._USKeyCodeToCharCode(e) ? true : false; // I1380 - support KIK for positional layouts\r\n }\r\n }\r\n\r\n /**\r\n * Maps a KeyEvent's modifiers to their appropriate value for key-rule evaluation\r\n * based on the rule's specified target modifier set.\r\n *\r\n * Mostly used to correct chiral OSK-keys targeting non-chiral rules.\r\n * @param e The source KeyEvent\r\n * @returns\r\n */\r\n private static matchModifiersToRuleChirality(eventModifiers: number, targetModifierMask: number): number {\r\n const CHIRAL_ALT = ModifierKeyConstants.LALTFLAG | ModifierKeyConstants.RALTFLAG;\r\n const CHIRAL_CTRL = ModifierKeyConstants.LCTRLFLAG | ModifierKeyConstants.RCTRLFLAG;\r\n\r\n let modifiers = eventModifiers;\r\n\r\n // If the target rule does not use chiral alt...\r\n if(!(targetModifierMask & CHIRAL_ALT)) {\r\n const altIntersection = modifiers & CHIRAL_ALT;\r\n\r\n if(altIntersection) {\r\n // Undo the chiral part and replace with non-chiral.\r\n modifiers ^= altIntersection | ModifierKeyConstants.K_ALTFLAG;\r\n }\r\n }\r\n\r\n // If the target rule does not use chiral ctrl...\r\n if(!(targetModifierMask & CHIRAL_CTRL)) {\r\n const ctrlIntersection = modifiers & CHIRAL_CTRL;\r\n\r\n if(ctrlIntersection) {\r\n // Undo the chiral part and replace with non-chiral.\r\n modifiers ^= ctrlIntersection | ModifierKeyConstants.K_CTRLFLAG;\r\n }\r\n }\r\n\r\n return modifiers;\r\n }\r\n\r\n /**\r\n * Function keyMatch KKM\r\n * Scope Public\r\n * @param {Object} e keystroke event\r\n * @param {number} Lruleshift\r\n * @param {number} Lrulekey\r\n * @return {boolean} True if key matches rule\r\n * Description Test keystroke with modifiers against rule\r\n */\r\n keyMatch(e: KeyEvent, Lruleshift:number, Lrulekey:number): boolean {\r\n var retVal = false; // I3318\r\n var keyCode = (e.Lcode == 173 ? 189 : e.Lcode); //I3555 (Firefox hyphen issue)\r\n\r\n let bitmask = this.activeKeyboard.modifierBitmask;\r\n var modifierBitmask = bitmask & Codes.modifierBitmasks[\"ALL\"];\r\n var stateBitmask = bitmask & Codes.stateBitmasks[\"ALL\"];\r\n\r\n const eventModifiers = KeyboardInterface.matchModifiersToRuleChirality(e.Lmodifiers, Lruleshift);\r\n\r\n if(e.vkCode > 255) {\r\n keyCode = e.vkCode; // added to support extended (touch-hold) keys for mnemonic layouts\r\n }\r\n\r\n if(e.LisVirtualKey || keyCode > 255) {\r\n if((Lruleshift & 0x4000) == 0x4000 || (keyCode > 255)) { // added keyCode test to support extended keys\r\n retVal = ((Lrulekey == keyCode) && ((Lruleshift & modifierBitmask) == eventModifiers)); //I3318, I3555\r\n retVal = retVal && this.stateMatch(e, Lruleshift & stateBitmask);\r\n }\r\n } else if((Lruleshift & 0x4000) == 0) {\r\n retVal = (keyCode == Lrulekey); // I3318, I3555\r\n }\r\n if(!retVal) {\r\n this.activeTargetOutput.deadkeys().resetMatched(); // I3318\r\n }\r\n return retVal; // I3318\r\n };\r\n\r\n /**\r\n * Function stateMatch KSM\r\n * Scope Public\r\n * @param {Object} e keystroke event\r\n * @param {number} Lstate\r\n * Description Test keystroke against state key rules\r\n */\r\n stateMatch(e: KeyEvent, Lstate: number) {\r\n return ((Lstate & e.Lstates) == Lstate);\r\n }\r\n\r\n /**\r\n * Function keyInformation KKI\r\n * Scope Public\r\n * @param {Object} e\r\n * @return {Object} Object with event's virtual key flag, key code, and modifiers\r\n * Description Get object with extended key event information\r\n */\r\n keyInformation(e: KeyEvent): KeyInformation {\r\n var ei = new KeyInformation();\r\n ei['vk'] = e.LisVirtualKey;\r\n ei['code'] = e.Lcode;\r\n ei['modifiers'] = e.Lmodifiers;\r\n return ei;\r\n };\r\n\r\n /**\r\n * Function deadkeyMatch KDM\r\n * Scope Public\r\n * @param {number} n offset from current cursor position\r\n * @param {Object} Ptarg target element\r\n * @param {number} d deadkey\r\n * @return {boolean} True if deadkey found selected context matches val\r\n * Description Match deadkey at current cursor position\r\n */\r\n deadkeyMatch(n: number, outputTarget: OutputTarget, d: number): boolean {\r\n return outputTarget.hasDeadkeyMatch(n, d);\r\n }\r\n\r\n /**\r\n * Function beep KB\r\n * Scope Public\r\n * @param {Object} Pelem element to flash\r\n * Description Flash body as substitute for audible beep; notify embedded device to vibrate\r\n */\r\n beep(outputTarget: OutputTarget): void {\r\n this.resetContextCache();\r\n\r\n // Denote as part of the matched rule's behavior.\r\n this.ruleBehavior.beep = true;\r\n }\r\n\r\n _ExplodeStore(store: KeyboardStore): ComplexKeyboardStore {\r\n if(typeof(store) == 'string') {\r\n let cachedStores = this.activeKeyboard.explodedStores;\r\n\r\n // Is the result cached?\r\n if(cachedStores[store]) {\r\n return cachedStores[store];\r\n }\r\n\r\n // Nope, so let's build its cache.\r\n var result: ComplexKeyboardStore = [];\r\n for(var i=0; i < store._kmwLength(); i++) {\r\n result.push(store._kmwCharAt(i));\r\n }\r\n\r\n // Cache the result for later!\r\n cachedStores[store] = result;\r\n return result;\r\n } else {\r\n return store;\r\n }\r\n }\r\n\r\n /**\r\n * Function any KA\r\n * Scope Public\r\n * @param {number} n character position (index)\r\n * @param {string} ch character to find in string\r\n * @param {string} s 'any' string\r\n * @return {boolean} True if character found in 'any' string, sets index accordingly\r\n * Description Test for character matching\r\n */\r\n any(n: number, ch: KeyboardStoreElement, s: KeyboardStore): boolean {\r\n if(ch == '') {\r\n return false;\r\n }\r\n\r\n s = this._ExplodeStore(s);\r\n var Lix = -1;\r\n for(var i=0; i < s.length; i++) {\r\n const entry = s[i];\r\n if(typeof(entry) == 'string') {\r\n if(s[i] == ch) {\r\n Lix = i;\r\n break;\r\n }\r\n // @ts-ignore // Needs to test against .t for automatic inference, but it's not actually there.\r\n } else if(entry.d === (ch as RuleDeadkey).d) {\r\n Lix = i;\r\n break;\r\n }\r\n }\r\n this._AnyIndices[n] = Lix;\r\n return Lix >= 0;\r\n }\r\n\r\n /**\r\n * Function _Index\r\n * Scope Public\r\n * @param {string} Ps string\r\n * @param {number} Pn index\r\n * Description Returns the character from a store string according to the offset in the index array\r\n */\r\n _Index(Ps: KeyboardStore, Pn: number): KeyboardStoreElement {\r\n Ps = this._ExplodeStore(Ps);\r\n\r\n if(this._AnyIndices[Pn-1] < Ps.length) { //I3319\r\n return Ps[this._AnyIndices[Pn-1]];\r\n } else {\r\n /* Should not be possible for a compiled keyboard, but may arise\r\n * during the development of handwritten keyboards.\r\n */\r\n console.warn(\"Unmatched contextual index() statement detected in rule with index \" + Pn + \"!\");\r\n return \"\";\r\n }\r\n }\r\n\r\n /**\r\n * Function indexOutput KIO\r\n * Scope Public\r\n * @param {number} Pdn no of character to overwrite (delete)\r\n * @param {string} Ps string\r\n * @param {number} Pn index\r\n * @param {Object} Pelem element to output to\r\n * Description Output a character selected from the string according to the offset in the index array\r\n */\r\n indexOutput(Pdn: number, Ps: KeyboardStore, Pn: number, outputTarget: OutputTarget): void {\r\n this.resetContextCache();\r\n\r\n var assertNever = function(x: never): never {\r\n // Could be accessed by improperly handwritten calls to `fullContextMatch`.\r\n throw new Error(\"Unexpected object in fullContextMatch specification: \" + x);\r\n }\r\n\r\n var indexChar = this._Index(Ps, Pn);\r\n if(indexChar !== \"\") {\r\n if(typeof indexChar == 'string' ) {\r\n this.output(Pdn, outputTarget, indexChar); //I3319\r\n } else if(indexChar.t) {\r\n switch(indexChar.t) {\r\n case 'b': // Beep commands may appear within stores.\r\n this.beep(outputTarget);\r\n break;\r\n case 'd':\r\n this.deadkeyOutput(Pdn, outputTarget, indexChar.d);\r\n break;\r\n default:\r\n assertNever(indexChar);\r\n }\r\n } else { // For keyboards developed during 10.0's alpha phase - t:'d' was assumed.\r\n this.deadkeyOutput(Pdn, outputTarget, (indexChar as any).d);\r\n }\r\n }\r\n }\r\n\r\n\r\n /**\r\n * Function deleteContext KDC\r\n * Scope Public\r\n * @param {number} dn number of context entries to overwrite\r\n * @param {Object} Pelem element to output to\r\n * @param {string} s string to output\r\n * Description Keyboard output\r\n */\r\n deleteContext(dn: number, outputTarget: OutputTarget): void {\r\n var context: CachedExEntry;\r\n\r\n // We want to control exactly which deadkeys get removed.\r\n if(dn > 0) {\r\n context = this._BuildExtendedContext(dn, dn, outputTarget);\r\n let nulCount = 0;\r\n\r\n for(var i=0; i < context.valContext.length; i++) {\r\n var dk = context.deadContext[i];\r\n\r\n if(dk) {\r\n // Remove deadkey in context.\r\n outputTarget.deadkeys().remove(dk);\r\n\r\n // Reduce our reported context size.\r\n dn--;\r\n } else if(context.valContext[i] == \"\\uFFFE\") {\r\n // Count any `nul` sentinels that would contribute to our deletion count.\r\n nulCount++;\r\n }\r\n }\r\n\r\n // Prevent attempts to delete nul sentinels, as they don't exist in the actual context.\r\n // (Addresses regression from KMW v 12.0 paired with Developer bug through same version)\r\n let contextLength = context.valContext.length - nulCount;\r\n if(dn > contextLength) {\r\n dn = contextLength;\r\n }\r\n }\r\n\r\n // If a matched deadkey hasn't been deleted, we don't WANT to delete it.\r\n outputTarget.deadkeys().resetMatched();\r\n\r\n // Why reinvent the wheel? Delete the remaining characters by 'inserting a blank string'.\r\n this.output(dn, outputTarget, '');\r\n }\r\n\r\n /**\r\n * Function output KO\r\n * Scope Public\r\n * @param {number} dn number of characters to overwrite\r\n * @param {Object} Pelem element to output to\r\n * @param {string} s string to output\r\n * Description Keyboard output\r\n */\r\n output(dn: number, outputTarget: OutputTarget, s:string): void {\r\n this.resetContextCache();\r\n\r\n outputTarget.saveProperties();\r\n outputTarget.clearSelection();\r\n outputTarget.deadkeys().deleteMatched(); // I3318\r\n if(dn >= 0) {\r\n // Automatically manages affected deadkey positions. Does not delete deadkeys b/c legacy behavior support.\r\n outputTarget.deleteCharsBeforeCaret(dn);\r\n }\r\n // Automatically manages affected deadkey positions.\r\n outputTarget.insertTextBeforeCaret(s);\r\n outputTarget.restoreProperties();\r\n }\r\n\r\n /**\r\n * `contextExOutput` function emits the character or object at `contextOffset` from the\r\n * current matched rule's context. Introduced in Keyman 14.0, in order to resolve a\r\n * gap between desktop and web core functionality for context(n) matching on notany().\r\n * See #917 for additional detail.\r\n * @alias KCXO\r\n * @public\r\n * @param {number} Pdn number of characters to delete left of cursor\r\n * @param {OutputTarget} outputTarget target to output to\r\n * @param {number} contextLength length of current rule context to retrieve\r\n * @param {number} contextOffset offset from start of current rule context, 1-based\r\n */\r\n contextExOutput(Pdn: number, outputTarget: OutputTarget, contextLength: number, contextOffset: number): void {\r\n this.resetContextCache();\r\n\r\n if(Pdn >= 0) {\r\n this.output(Pdn, outputTarget, \"\");\r\n }\r\n\r\n const context = this.ruleContextEx.get(contextLength, contextLength);\r\n const dk = context.deadContext[contextOffset-1], vc = context.valContext[contextOffset-1];\r\n if(dk) {\r\n outputTarget.insertDeadkeyBeforeCaret(dk.d);\r\n } else if(typeof vc == 'string') {\r\n this.output(-1, outputTarget, vc);\r\n } else {\r\n throw new Error(\"contextExOutput: should never be a numeric valContext with no corresponding deadContext\");\r\n }\r\n }\r\n\r\n /**\r\n * Function deadkeyOutput KDO\r\n * Scope Public\r\n * @param {number} Pdn no of character to overwrite (delete)\r\n * @param {Object} Pelem element to output to\r\n * @param {number} Pd deadkey id\r\n * Description Record a deadkey at current cursor position, deleting Pdn characters first\r\n */\r\n deadkeyOutput(Pdn: number, outputTarget: OutputTarget, Pd: number): void {\r\n this.resetContextCache();\r\n\r\n if(Pdn >= 0) {\r\n this.output(Pdn, outputTarget,\"\"); //I3318 corrected to >=\r\n }\r\n\r\n outputTarget.insertDeadkeyBeforeCaret(Pd);\r\n // _DebugDeadKeys(Pelem, 'KDeadKeyOutput: dn='+Pdn+'; deadKey='+Pd);\r\n }\r\n\r\n /**\r\n * KIFS compares the content of a system store with a string value\r\n *\r\n * @param {number} systemId ID of the system store to test (only TSS_LAYER currently supported)\r\n * @param {string} strValue String value to compare to\r\n * @param {Object} Pelem Currently active element (may be needed by future tests)\r\n * @return {boolean} True if the test succeeds\r\n */\r\n ifStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean {\r\n var result=true;\r\n let store = this.systemStores[systemId];\r\n if(store) {\r\n result = store.matches(strValue);\r\n }\r\n return result; //Moved from previous line, now supports layer selection, Build 350\r\n }\r\n\r\n /**\r\n * KSETS sets the value of a system store to a string\r\n *\r\n * @param {number} systemId ID of the system store to set (only TSS_LAYER currently supported)\r\n * @param {string} strValue String to set as the system store content\r\n * @param {Object} Pelem Currently active element (may be needed in future tests)\r\n * @return {boolean} True if command succeeds\r\n * (i.e. for TSS_LAYER, if the layer is successfully selected)\r\n *\r\n * Note that option/variable stores are instead set within keyboard script code, as they only\r\n * affect keyboard behavior.\r\n */\r\n setStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean {\r\n this.resetContextCache();\r\n // Unique case: we only allow set(&layer) ops from keyboard rules triggered by touch OSKs.\r\n if(systemId == SystemStoreIDs.TSS_LAYER && this.activeDevice.touchable) {\r\n // Denote the changed store as part of the matched rule's behavior.\r\n this.ruleBehavior.setStore[systemId] = strValue;\r\n return true;\r\n }\r\n return false;\r\n }\r\n\r\n /**\r\n * Load an option store value from a cookie or default value\r\n *\r\n * @param {string} kbdName keyboard internal name\r\n * @param {string} storeName store (option) name, embedded in cookie name\r\n * @param {string} dfltValue default value\r\n * @return {string} current or default option value\r\n *\r\n * This will only ever be called when the keyboard is loaded, as it is used by keyboards\r\n * to initialize a store value on the keyboard's script object.\r\n */\r\n loadStore(kbdName: string, storeName:string, dfltValue:string): string {\r\n this.resetContextCache();\r\n if(this.variableStoreSerializer) {\r\n let cValue = this.variableStoreSerializer.loadStore(kbdName, storeName);\r\n return cValue[storeName] || dfltValue;\r\n } else {\r\n return dfltValue;\r\n }\r\n }\r\n\r\n /**\r\n * Save an option store value to a cookie\r\n *\r\n * @param {string} storeName store (option) name, embedded in cookie name\r\n * @param {string} optValue option value to save\r\n * @return {boolean} true if save successful\r\n *\r\n * Note that a keyboard will freely manipulate the value of its variable stores on the\r\n * script object within its own code. This function's use is merely to _persist_ that\r\n * value across sessions, providing a custom user default for later uses of the keyboard.\r\n */\r\n saveStore(storeName:string, optValue:string): boolean {\r\n this.resetContextCache();\r\n var kbd=this.activeKeyboard;\r\n if(!kbd || typeof kbd.id == 'undefined' || kbd.id == '') {\r\n return false;\r\n }\r\n\r\n // And the lookup under that entry looks for the value under the store name, again.\r\n let valueObj: VariableStore = {};\r\n valueObj[storeName] = optValue;\r\n\r\n // Null-check in case of invocation during unit-test\r\n if(this.ruleBehavior) {\r\n this.ruleBehavior.saveStore[storeName] = valueObj;\r\n } else {\r\n // We're in a unit-test environment, directly invoking this method from outside of a keyboard.\r\n // In this case, we should immediately commit the change.\r\n this.variableStoreSerializer.saveStore(this.activeKeyboard.id, storeName, valueObj);\r\n }\r\n return true;\r\n }\r\n\r\n resetContextCache(): void {\r\n this.cachedContext.reset();\r\n this.cachedContextEx.reset();\r\n }\r\n\r\n defaultBackspace(outputTarget: OutputTarget) {\r\n if(outputTarget.isSelectionEmpty()) {\r\n // Delete the character left of the caret\r\n this.output(1, outputTarget, \"\");\r\n } else {\r\n // Delete just the selection\r\n this.output(0, outputTarget, \"\");\r\n }\r\n }\r\n\r\n /**\r\n * Function processNewContextEvent\r\n * Scope Private\r\n * @param {Object} outputTarget The target receiving input\r\n * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard.\r\n * Description Calls the keyboard's `begin newContext` group\r\n * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin NewContext`\r\n */\r\n processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior {\r\n if(!this.activeKeyboard) {\r\n throw \"No active keyboard for keystroke processing!\";\r\n }\r\n return this.process(this.activeKeyboard.processNewContextEvent.bind(this.activeKeyboard), outputTarget, keystroke, true);\r\n }\r\n\r\n /**\r\n * Function processPostKeystroke\r\n * Scope Private\r\n * @param {Object} outputTarget The target receiving input\r\n * @param {Object} keystroke The input keystroke with relevant properties to be mapped by the keyboard.\r\n * Description Calls the keyboard's `begin postKeystroke` group\r\n * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin PostKeystroke`\r\n */\r\n processPostKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior {\r\n if(!this.activeKeyboard) {\r\n throw \"No active keyboard for keystroke processing!\";\r\n }\r\n return this.process(this.activeKeyboard.processPostKeystroke.bind(this.activeKeyboard), outputTarget, keystroke, true);\r\n }\r\n\r\n /**\r\n * Function processKeystroke\r\n * Scope Private\r\n * @param {Object} outputTarget The target receiving input\r\n * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard.\r\n * Description Encapsulates calls to keyboard input processing.\r\n * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin Unicode`\r\n */\r\n processKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior {\r\n if(!this.activeKeyboard) {\r\n throw \"No active keyboard for keystroke processing!\";\r\n }\r\n return this.process(this.activeKeyboard.process.bind(this.activeKeyboard), outputTarget, keystroke, false);\r\n }\r\n\r\n private process(callee: (outputTarget: OutputTarget, keystroke: KeyEvent) => boolean, outputTarget: OutputTarget, keystroke: KeyEvent, readonly: boolean): RuleBehavior {\r\n // Clear internal state tracking data from prior keystrokes.\r\n if(!outputTarget) {\r\n throw \"No target specified for keyboard output!\";\r\n } else if(!this.activeKeyboard) {\r\n throw \"No active keyboard for keystroke processing!\";\r\n } else if(!callee) {\r\n throw \"No callee for keystroke processing!\";\r\n }\r\n\r\n outputTarget.invalidateSelection();\r\n\r\n outputTarget.deadkeys().resetMatched(); // I3318\r\n this.resetContextCache();\r\n\r\n // Capture the initial state of the OutputTarget before any rules are matched.\r\n let preInput = Mock.from(outputTarget, true);\r\n\r\n // Capture the initial state of any variable stores\r\n const cachedVariableStores = this.activeKeyboard.variableStores;\r\n\r\n // Establishes the results object, allowing corresponding commands to set values here as appropriate.\r\n this.ruleBehavior = new RuleBehavior();\r\n\r\n // Ensure the settings are in place so that KIFS/ifState activates and deactivates\r\n // the appropriate rule(s) for the modeled device.\r\n this.activeDevice = keystroke.device;\r\n\r\n // Calls the start-group of the active keyboard.\r\n this.activeTargetOutput = outputTarget;\r\n var matched = callee(outputTarget, keystroke);\r\n this.activeTargetOutput = null;\r\n\r\n // Finalize the rule's results.\r\n this.ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(preInput, keystroke, readonly);\r\n\r\n // We always backup the changes to variable stores to the RuleBehavior, to\r\n // be applied during finalization, then restore them to the cached initial\r\n // values to avoid side-effects with predictive text mocks.\r\n this.ruleBehavior.variableStores = this.activeKeyboard.variableStores;\r\n this.activeKeyboard.variableStores = cachedVariableStores;\r\n\r\n // `matched` refers to whether or not the FINAL rule (from any group) matched, rather than\r\n // whether or not ANY rule matched. If the final rule doesn't match, we trigger the key's\r\n // default behavior (if appropriate).\r\n //\r\n // See https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852\r\n this.ruleBehavior.triggerKeyDefault = !matched;\r\n\r\n // Clear our result-tracking variable to prevent any possible pollution for future processing.\r\n let behavior = this.ruleBehavior;\r\n this.ruleBehavior = null;\r\n\r\n return behavior;\r\n }\r\n\r\n /**\r\n * Applies the dictionary of variable store values to the active keyboard\r\n *\r\n * Has no effect on keyboards compiled with 14.0 or earlier; system store\r\n * names are not exposed unless compiled with Developer 15.0 or later.\r\n *\r\n * @param stores A dictionary of stores which should be found in the\r\n * keyboard\r\n */\r\n applyVariableStores(stores: VariableStoreDictionary): void {\r\n this.activeKeyboard.variableStores = stores;\r\n }\r\n\r\n /**\r\n * Publishes the KeyboardInterface's shorthand API names. As this assigns the current functions\r\n * held by the longform versions, note that this should be called after replacing any of them via\r\n * JS method extension.\r\n *\r\n * DOM-aware KeymanWeb should call this after its domKbdInterface.ts code is loaded, as it replaces\r\n * a few. (This is currently done within its kmwapi.ts.)\r\n */\r\n static __publishShorthandAPI() {\r\n // Keyboard callbacks\r\n let prototype = this.prototype;\r\n\r\n var exportKBCallback = function(miniName: string, longName: keyof KeyboardInterface) {\r\n if(prototype[longName]) {\r\n // @ts-ignore\r\n prototype[miniName] = prototype[longName];\r\n }\r\n }\r\n\r\n exportKBCallback('KSF', 'saveFocus');\r\n // @ts-ignore // is defined at a higher level\r\n exportKBCallback('KBR', 'beepReset');\r\n exportKBCallback('KT', 'insertText');\r\n exportKBCallback('KR', 'registerKeyboard');\r\n // @ts-ignore // is defined at a higher level\r\n exportKBCallback('KRS', 'registerStub');\r\n exportKBCallback('KC', 'context');\r\n exportKBCallback('KN', 'nul');\r\n exportKBCallback('KCM', 'contextMatch');\r\n exportKBCallback('KFCM', 'fullContextMatch');\r\n exportKBCallback('KIK', 'isKeypress');\r\n exportKBCallback('KKM', 'keyMatch');\r\n exportKBCallback('KSM', 'stateMatch');\r\n exportKBCallback('KKI', 'keyInformation');\r\n exportKBCallback('KDM', 'deadkeyMatch');\r\n exportKBCallback('KB', 'beep');\r\n exportKBCallback('KA', 'any');\r\n exportKBCallback('KDC', 'deleteContext');\r\n exportKBCallback('KO', 'output');\r\n exportKBCallback('KDO', 'deadkeyOutput');\r\n exportKBCallback('KCXO', 'contextExOutput');\r\n exportKBCallback('KIO', 'indexOutput');\r\n exportKBCallback('KIFS', 'ifStore');\r\n exportKBCallback('KSETS', 'setStore');\r\n exportKBCallback('KLOAD', 'loadStore');\r\n exportKBCallback('KSAVE', 'saveStore');\r\n }\r\n}\r\n\r\n(function() {\r\n // This will be the only call within the keyboard module.\r\n KeyboardInterface.__publishShorthandAPI();\r\n}());\r\n", + "/*\r\n * Keyman is copyright (C) SIL International. MIT License.\r\n *\r\n * Implementation of the JavaScript keyboard processor\r\n */\r\n\r\n// #region Big ol' list of imports\r\n\r\nimport { EventEmitter } from 'eventemitter3';\r\nimport { ModifierKeyConstants } from '@keymanapp/common-types';\r\nimport {\r\n Codes, type Keyboard, MinimalKeymanGlobal, KeyEvent, Layouts,\r\n DefaultRules, EmulationKeystrokes\r\n} from \"keyman/engine/keyboard\";\r\nimport { Mock } from \"./mock.js\";\r\nimport type OutputTarget from \"./outputTarget.js\";\r\nimport RuleBehavior from \"./ruleBehavior.js\";\r\nimport KeyboardInterface from './kbdInterface.js';\r\nimport { DeviceSpec, globalObject } from \"@keymanapp/web-utils\";\r\nimport { type MutableSystemStore, SystemStoreIDs } from \"./systemStores.js\";\r\n\r\n// #endregion\r\n\r\n// Also relies on @keymanapp/web-utils, which is included via tsconfig.json.\r\n\r\nexport type BeepHandler = (outputTarget: OutputTarget) => void;\r\nexport type LogMessageHandler = (str: string) => void;\r\n\r\nexport interface ProcessorInitOptions {\r\n baseLayout?: string;\r\n keyboardInterface?: KeyboardInterface;\r\n defaultOutputRules?: DefaultRules; // Takes the class def object, not an instance thereof.\r\n}\r\n\r\ninterface EventMap {\r\n statekeychange: (stateKeys: typeof KeyboardProcessor.prototype.stateKeys) => void;\r\n}\r\n\r\nexport default class KeyboardProcessor extends EventEmitter {\r\n public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = {\r\n baseLayout: 'us',\r\n defaultOutputRules: new DefaultRules()\r\n };\r\n\r\n // Tracks the simulated value for supported state keys, allowing the OSK to mirror a physical keyboard for them.\r\n // Using the exact keyCode name from the Codes definitions will allow for certain optimizations elsewhere in the code.\r\n stateKeys = {\r\n \"K_CAPS\":false,\r\n \"K_NUMLOCK\":false,\r\n \"K_SCROLL\":false\r\n };\r\n\r\n // Tracks the most recent modifier state information in order to quickly detect changes\r\n // in keyboard state not otherwise captured by the hosting page in the browser.\r\n // Needed for AltGr simulation.\r\n modStateFlags: number = 0;\r\n\r\n keyboardInterface: KeyboardInterface;\r\n\r\n /**\r\n * Indicates the device (platform) to be used for non-keystroke events,\r\n * such as those sent to `begin postkeystroke` and `begin newcontext`\r\n * entry points.\r\n */\r\n contextDevice: DeviceSpec;\r\n\r\n baseLayout: string;\r\n\r\n defaultRules: DefaultRules;\r\n\r\n // Callbacks for various feedback types\r\n beepHandler?: BeepHandler;\r\n warningLogger?: LogMessageHandler;\r\n errorLogger?: LogMessageHandler;\r\n\r\n constructor(device: DeviceSpec, options?: ProcessorInitOptions) {\r\n super();\r\n\r\n if(!options) {\r\n options = KeyboardProcessor.DEFAULT_OPTIONS;\r\n }\r\n\r\n this.contextDevice = device;\r\n\r\n this.baseLayout = options.baseLayout || KeyboardProcessor.DEFAULT_OPTIONS.baseLayout;\r\n this.keyboardInterface = options.keyboardInterface || new KeyboardInterface(globalObject(), MinimalKeymanGlobal);\r\n this.defaultRules = options.defaultOutputRules || KeyboardProcessor.DEFAULT_OPTIONS.defaultOutputRules;\r\n }\r\n\r\n public get activeKeyboard(): Keyboard {\r\n return this.keyboardInterface.activeKeyboard;\r\n }\r\n\r\n public set activeKeyboard(keyboard: Keyboard) {\r\n this.keyboardInterface.activeKeyboard = keyboard;\r\n\r\n // All old deadkeys and keyboard-specific cache should immediately be invalidated\r\n // on a keyboard change.\r\n this.resetContext();\r\n }\r\n\r\n get layerStore(): MutableSystemStore {\r\n return this.keyboardInterface.systemStores[SystemStoreIDs.TSS_LAYER] as MutableSystemStore;\r\n }\r\n\r\n public get newLayerStore(): MutableSystemStore {\r\n return this.keyboardInterface.systemStores[SystemStoreIDs.TSS_NEWLAYER] as MutableSystemStore;\r\n }\r\n\r\n public get oldLayerStore(): MutableSystemStore {\r\n return this.keyboardInterface.systemStores[SystemStoreIDs.TSS_OLDLAYER] as MutableSystemStore;\r\n }\r\n\r\n public get layerId(): string {\r\n return this.layerStore.value;\r\n }\r\n\r\n // Note: will trigger an 'event' callback designed to notify the OSK of layer changes.\r\n public set layerId(value: string) {\r\n this.layerStore.set(value);\r\n }\r\n\r\n /**\r\n * Get the default RuleBehavior for the specified key, attempting to mimic standard browser defaults\r\n * where and when appropriate.\r\n *\r\n * @param {object} Lkc The pre-analyzed KeyEvent object\r\n * @param {boolean} outputTarget The OutputTarget receiving the KeyEvent\r\n * @return {string}\r\n */\r\n defaultRuleBehavior(Lkc: KeyEvent, outputTarget: OutputTarget, readonly: boolean): RuleBehavior {\r\n let preInput = Mock.from(outputTarget, readonly);\r\n let ruleBehavior = new RuleBehavior();\r\n\r\n let matched = false;\r\n var char = '';\r\n var special: EmulationKeystrokes;\r\n if(Lkc.isSynthetic || outputTarget.isSynthetic) {\r\n matched = true; // All the conditions below result in matches until the final else, which restores the expected default\r\n // if no match occurs.\r\n\r\n if(this.defaultRules.isCommand(Lkc)) {\r\n // Note this in the rule behavior, return successfully. We'll consider applying it later.\r\n ruleBehavior.triggersDefaultCommand = true;\r\n\r\n // We'd rather let the browser handle these keys, but we're using emulated keystrokes, forcing KMW\r\n // to emulate default behavior here.\r\n } else if((special = this.defaultRules.forSpecialEmulation(Lkc)) != null) {\r\n switch(special) {\r\n case EmulationKeystrokes.Backspace:\r\n this.keyboardInterface.defaultBackspace(outputTarget);\r\n break;\r\n case EmulationKeystrokes.Enter:\r\n outputTarget.handleNewlineAtCaret();\r\n break;\r\n // case '\\u007f': // K_DEL\r\n // // For (possible) future implementation.\r\n // // Would recommend (conceptually) equaling K_RIGHT + K_BKSP, the former of which would technically be a 'command'.\r\n default:\r\n // In case we extend the allowed set, but forget to implement its handling case above.\r\n ruleBehavior.errorLog = \"Unexpected 'special emulation' character (\\\\u\" + (special as String).kmwCharCodeAt(0).toString(16) + \") went unhandled!\";\r\n }\r\n } else {\r\n // Back to the standard default, pending normal matching.\r\n matched = false;\r\n }\r\n }\r\n\r\n let isMnemonic = this.activeKeyboard && this.activeKeyboard.isMnemonic;\r\n\r\n if(!matched) {\r\n if((char = this.defaultRules.forAny(Lkc, isMnemonic, ruleBehavior)) != null) {\r\n special = this.defaultRules.forSpecialEmulation(Lkc)\r\n if(special == EmulationKeystrokes.Backspace) {\r\n // A browser's default backspace may fail to delete both parts of an SMP character.\r\n this.keyboardInterface.defaultBackspace(outputTarget);\r\n } else if(special || this.defaultRules.isCommand(Lkc)) { // Filters out 'commands' like TAB.\r\n // We only do the \"for special emulation\" cases under the condition above... aside from backspace\r\n // Let the browser handle those.\r\n return null;\r\n } else {\r\n this.keyboardInterface.output(0, outputTarget, char);\r\n }\r\n } else {\r\n // No match, no default RuleBehavior.\r\n return null;\r\n }\r\n }\r\n\r\n // Shortcut things immediately if there were issues generating this rule behavior.\r\n if(ruleBehavior.errorLog) {\r\n return ruleBehavior;\r\n }\r\n\r\n let transcription = outputTarget.buildTranscriptionFrom(preInput, Lkc, readonly);\r\n ruleBehavior.transcription = transcription;\r\n\r\n return ruleBehavior;\r\n }\r\n\r\n processNewContextEvent(device: DeviceSpec, outputTarget: OutputTarget): RuleBehavior {\r\n return this.activeKeyboard ?\r\n this.keyboardInterface.processNewContextEvent(outputTarget, this.activeKeyboard.constructNullKeyEvent(device, this.stateKeys)) :\r\n null;\r\n }\r\n\r\n processPostKeystroke(device: DeviceSpec, outputTarget: OutputTarget): RuleBehavior {\r\n return this.activeKeyboard ?\r\n this.keyboardInterface.processPostKeystroke(outputTarget, this.activeKeyboard.constructNullKeyEvent(device, this.stateKeys)) :\r\n null;\r\n }\r\n\r\n processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior {\r\n var matchBehavior: RuleBehavior;\r\n\r\n // Before keyboard rules apply, check if the left-context is empty.\r\n const nothingDeletable = outputTarget.getTextBeforeCaret().kmwLength() == 0 && outputTarget.isSelectionEmpty();\r\n\r\n // Pass this key code and state to the keyboard program\r\n if(this.activeKeyboard && keyEvent.Lcode != 0) {\r\n matchBehavior = this.keyboardInterface.processKeystroke(outputTarget, keyEvent);\r\n }\r\n\r\n // Final conditional component - if someone actually makes a keyboard rule that blocks output\r\n // of K_BKSP with an empty left-context or does other really weird things... it's on them.\r\n //\r\n // We don't expect such rules to appear, but trying to override them would likely result in odd\r\n // behavior in cases where such rules actually would appear. (Though, _that_ should be caught\r\n // in the keyboard-review process and heavily discouraged, so... yeah.)\r\n if(nothingDeletable && keyEvent.Lcode == Codes.keyCodes.K_BKSP && matchBehavior.triggerKeyDefault) {\r\n matchBehavior = this.defaultRuleBehavior(keyEvent, outputTarget, false);\r\n matchBehavior.triggerKeyDefault = true;\r\n // Force a single `deleteLeft`.\r\n // @ts-ignore // force value override, because deleteLeft is marked readonly.\r\n matchBehavior.transcription.transform.deleteLeft = 1;\r\n } else if(!matchBehavior || matchBehavior.triggerKeyDefault) {\r\n // Restore the virtual key code if a mnemonic keyboard is being used\r\n // If no vkCode value was stored, maintain the original Lcode value.\r\n keyEvent.Lcode=keyEvent.vkCode || keyEvent.Lcode;\r\n\r\n // Handle unmapped keys, including special keys\r\n // The following is physical layout dependent, so should be avoided if possible. All keys should be mapped.\r\n this.keyboardInterface.activeTargetOutput = outputTarget;\r\n\r\n // Match against the 'default keyboard' - rules to mimic the default string output when typing in a browser.\r\n // Many keyboards rely upon these 'implied rules'.\r\n let defaultBehavior = this.defaultRuleBehavior(keyEvent, outputTarget, false);\r\n if(defaultBehavior) {\r\n if(!matchBehavior) {\r\n matchBehavior = defaultBehavior;\r\n } else {\r\n matchBehavior.mergeInDefaults(defaultBehavior);\r\n }\r\n matchBehavior.triggerKeyDefault = false; // We've triggered it successfully.\r\n } // If null, we must rely on something else (like the browser, in DOM-aware code) to fulfill the default.\r\n\r\n this.keyboardInterface.activeTargetOutput = null;\r\n }\r\n\r\n return matchBehavior;\r\n }\r\n\r\n /**\r\n * Function _UpdateVKShift\r\n * Scope Private\r\n * @param {Object} e OSK event\r\n * @return {boolean} Always true\r\n * Description Updates the current shift state within KMW, updating the OSK's visualization thereof.\r\n */\r\n _UpdateVKShift(e: KeyEvent): boolean {\r\n let keyShiftState=0;\r\n\r\n const lockNames = ['CAPS', 'NUM_LOCK', 'SCROLL_LOCK'] as const;\r\n const lockKeys = ['K_CAPS', 'K_NUMLOCK', 'K_SCROLL'] as const;\r\n const lockModifiers = [ModifierKeyConstants.CAPITALFLAG, ModifierKeyConstants.NUMLOCKFLAG, ModifierKeyConstants.SCROLLFLAG] as const;\r\n\r\n\r\n if(!this.activeKeyboard) {\r\n return true;\r\n }\r\n\r\n if(e) {\r\n // read shift states from Pevent\r\n keyShiftState = e.Lmodifiers;\r\n\r\n // Are we simulating AltGr? If it's a simulation and not real, time to un-simulate for the OSK.\r\n if(this.activeKeyboard.isChiral && (this.activeKeyboard.emulatesAltGr) &&\r\n (this.modStateFlags & Codes.modifierBitmasks['ALT_GR_SIM']) == Codes.modifierBitmasks['ALT_GR_SIM']) {\r\n keyShiftState |= Codes.modifierBitmasks['ALT_GR_SIM'];\r\n keyShiftState &= ~ModifierKeyConstants.RALTFLAG;\r\n }\r\n\r\n // Set stateKeys where corresponding value is passed in e.Lstates\r\n let stateMutation = false;\r\n for(let i=0; i < lockNames.length; i++) {\r\n if(e.Lstates & Codes.stateBitmasks[lockNames[i]]) {\r\n this.stateKeys[lockKeys[i]] = !!(e.Lstates & lockModifiers[i]);\r\n stateMutation = true;\r\n }\r\n }\r\n\r\n if(stateMutation) {\r\n this.emit('statekeychange', this.stateKeys);\r\n }\r\n }\r\n\r\n this.updateStates();\r\n\r\n if(this.activeKeyboard.isMnemonic && this.stateKeys['K_CAPS']) {\r\n // Modifier keypresses doesn't trigger mnemonic manipulation of modifier state.\r\n // Only an output key does; active use of Caps will also flip the SHIFT flag.\r\n if(!e || !e.isModifier) {\r\n // Mnemonic keystrokes manipulate the SHIFT property based on CAPS state.\r\n // We need to unflip them when tracking the OSK layer.\r\n keyShiftState ^= ModifierKeyConstants.K_SHIFTFLAG;\r\n }\r\n }\r\n\r\n this.layerId = this.getLayerId(keyShiftState);\r\n return true;\r\n }\r\n\r\n private updateStates(): void {\r\n const lockKeys = ['K_CAPS', 'K_NUMLOCK', 'K_SCROLL'] as const;\r\n const lockModifiers = [ModifierKeyConstants.CAPITALFLAG, ModifierKeyConstants.NUMLOCKFLAG, ModifierKeyConstants.SCROLLFLAG] as const;\r\n const noLockModifers = [ModifierKeyConstants.NOTCAPITALFLAG, ModifierKeyConstants.NOTNUMLOCKFLAG, ModifierKeyConstants.NOTSCROLLFLAG] as const;\r\n\r\n\r\n\r\n for(let i=0; i < lockKeys.length; i++) {\r\n const key = lockKeys[i];\r\n const flag = this.stateKeys[key];\r\n\r\n // Ensures that the current mod-state info properly matches the currently-simulated\r\n // state key states.\r\n if(flag) {\r\n this.modStateFlags |= lockModifiers[i];\r\n this.modStateFlags &= ~noLockModifers[i];\r\n } else {\r\n this.modStateFlags &= ~lockModifiers[i];\r\n this.modStateFlags |= noLockModifers[i];\r\n }\r\n }\r\n }\r\n\r\n getLayerId(modifier: number): string {\r\n return Layouts.getLayerId(modifier);\r\n }\r\n\r\n /**\r\n * Select the OSK's next keyboard layer based upon layer switching keys as a default\r\n * The next layer will be determined from the key name unless otherwise specifed\r\n *\r\n * @param {string} keyName key identifier\r\n * @return {boolean} return true if keyboard layer changed\r\n */\r\n selectLayer(keyEvent: KeyEvent): boolean {\r\n let keyName = keyEvent.kName;\r\n var nextLayer = keyEvent.kNextLayer;\r\n var isChiral = this.activeKeyboard && this.activeKeyboard.isChiral;\r\n\r\n // Layer must be identified by name, not number (27/08/2015)\r\n if(typeof nextLayer == 'number') {\r\n nextLayer = this.getLayerId(nextLayer * 0x10);\r\n }\r\n\r\n // Identify next layer, if required by key\r\n if(!nextLayer) {\r\n switch(keyName) {\r\n case 'K_LSHIFT':\r\n case 'K_RSHIFT':\r\n case 'K_SHIFT':\r\n nextLayer = 'shift';\r\n break;\r\n case 'K_LCONTROL':\r\n case 'K_LCTRL':\r\n if(isChiral) {\r\n nextLayer = 'leftctrl';\r\n break;\r\n }\r\n case 'K_RCONTROL':\r\n case 'K_RCTRL':\r\n if(isChiral) {\r\n nextLayer = 'rightctrl';\r\n break;\r\n }\r\n case 'K_CTRL':\r\n nextLayer = 'ctrl';\r\n break;\r\n case 'K_LMENU':\r\n case 'K_LALT':\r\n if(isChiral) {\r\n nextLayer = 'leftalt';\r\n break;\r\n }\r\n case 'K_RMENU':\r\n case 'K_RALT':\r\n if(isChiral) {\r\n nextLayer = 'rightalt';\r\n break;\r\n }\r\n case 'K_ALT':\r\n nextLayer = 'alt';\r\n break;\r\n case 'K_ALTGR':\r\n if(isChiral) {\r\n nextLayer = 'leftctrl-rightalt';\r\n } else {\r\n nextLayer = 'ctrl-alt';\r\n }\r\n break;\r\n case 'K_CURRENCIES':\r\n case 'K_NUMERALS':\r\n case 'K_SHIFTED':\r\n case 'K_UPPER':\r\n case 'K_LOWER':\r\n case 'K_SYMBOLS':\r\n nextLayer = 'default';\r\n break;\r\n }\r\n }\r\n\r\n // If no key corresponding to a layer transition is pressed, maintain the current layer.\r\n if(!nextLayer) {\r\n return false;\r\n }\r\n\r\n // Change layer and refresh OSK\r\n this.updateLayer(keyEvent, nextLayer);\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Sets the new layer id, allowing for toggling shift/ctrl/alt while preserving the remainder\r\n * of the modifiers represented by the current layer id (where applicable)\r\n *\r\n * @param {string} id layer id (e.g. ctrlshift)\r\n */\r\n updateLayer(keyEvent: KeyEvent, id: string) {\r\n let activeLayer = this.layerId;\r\n var s = activeLayer;\r\n\r\n // Do not change layer unless needed (27/08/2015)\r\n if(id == activeLayer && keyEvent.device.formFactor != DeviceSpec.FormFactor.Desktop) {\r\n return;\r\n }\r\n\r\n var idx=id;\r\n var i;\r\n\r\n if(keyEvent.device.formFactor == DeviceSpec.FormFactor.Desktop) {\r\n // Need to test if target layer is a standard layer (based on the plain 'default')\r\n var replacements= ['leftctrl', 'rightctrl', 'ctrl', 'leftalt', 'rightalt', 'alt', 'shift'];\r\n\r\n for(i=0; i < replacements.length; i++) {\r\n // Don't forget to remove the kebab-case hyphens!\r\n idx=idx.replace(replacements[i] + '-', '');\r\n idx=idx.replace(replacements[i],'');\r\n }\r\n\r\n // If we are presently on the default layer, drop the 'default' and go straight to the shifted mode.\r\n // If on a common symbolic layer, drop out of symbolic mode and go straight to the shifted mode.\r\n if(activeLayer == 'default' || activeLayer == 'numeric' || activeLayer == 'symbol' || activeLayer == 'currency' || idx != '') {\r\n s = id;\r\n }\r\n // Otherwise, we are based upon a layer that accepts modifier variations.\r\n // Modify the layer according to the current state and key pressed.\r\n //\r\n // TODO: Consider: should this ever be allowed for a base layer other than 'default'? If not,\r\n // if(idx == '') with accompanying if-else structural shift would be a far better test here.\r\n else {\r\n // Save our current modifier state.\r\n var modifier=Codes.getModifierState(s);\r\n\r\n // Strip down to the base modifiable layer.\r\n for(i=0; i < replacements.length; i++) {\r\n // Don't forget to remove the kebab-case hyphens!\r\n s=s.replace(replacements[i] + '-', '');\r\n s=s.replace(replacements[i],'');\r\n }\r\n\r\n // Toggle the modifier represented by our input argument.\r\n switch(id) {\r\n case 'shift':\r\n modifier ^= ModifierKeyConstants.K_SHIFTFLAG;\r\n break;\r\n case 'leftctrl':\r\n modifier ^= ModifierKeyConstants.LCTRLFLAG;\r\n break;\r\n case 'rightctrl':\r\n modifier ^= ModifierKeyConstants.RCTRLFLAG;\r\n break;\r\n case 'ctrl':\r\n modifier ^= ModifierKeyConstants.K_CTRLFLAG;\r\n break;\r\n case 'leftalt':\r\n modifier ^= ModifierKeyConstants.LALTFLAG;\r\n break;\r\n case 'rightalt':\r\n modifier ^= ModifierKeyConstants.RALTFLAG;\r\n break;\r\n case 'alt':\r\n modifier ^= ModifierKeyConstants.K_ALTFLAG;\r\n break;\r\n default:\r\n s = id;\r\n }\r\n\r\n // Combine our base modifiable layer and attach the new modifier variation info to obtain our destination layer.\r\n if(s != 'default') {\r\n if(s == '') {\r\n s = this.getLayerId(modifier);\r\n } else {\r\n s = this.getLayerId(modifier) + '-' + s;\r\n }\r\n }\r\n }\r\n\r\n if(s == '') {\r\n s = 'default';\r\n }\r\n } else {\r\n // Mobile form-factor. Either the layout is specified by a keyboard developer with direct layer name references\r\n // or all layers are accessed via subkey of a single layer-shifting key - no need for modifier-combining logic.\r\n s = id;\r\n }\r\n\r\n let layout = this.activeKeyboard.layout(keyEvent.device.formFactor);\r\n if(layout.getLayer(s)) {\r\n this.layerId = s;\r\n } else {\r\n this.layerId = 'default';\r\n }\r\n\r\n let baseModifierState = Codes.getModifierState(this.layerId);\r\n this.modStateFlags = baseModifierState | keyEvent.Lstates;\r\n }\r\n\r\n // Returns true if the key event is a modifier press, allowing keyPress to return selectively\r\n // in those cases.\r\n doModifierPress(Levent: KeyEvent, outputTarget: OutputTarget, isKeyDown: boolean): boolean {\r\n if(!this.activeKeyboard) {\r\n return false;\r\n }\r\n\r\n if(Levent.isModifier) {\r\n this.activeKeyboard.notify(Levent.Lcode, outputTarget, isKeyDown ? 1 : 0);\r\n // For eventual integration - we bypass an OSK update for physical keystrokes when in touch mode.\r\n if(!Levent.device.touchable) {\r\n return this._UpdateVKShift(Levent); // I2187\r\n } else {\r\n return true;\r\n }\r\n }\r\n\r\n if(Levent.LmodifierChange) {\r\n this.activeKeyboard.notify(0, outputTarget, 1);\r\n if(!Levent.device.touchable) {\r\n this._UpdateVKShift(Levent);\r\n }\r\n }\r\n\r\n // No modifier keypresses detected.\r\n return false;\r\n }\r\n\r\n /**\r\n * Tell the currently active keyboard that a new context has been selected,\r\n * e.g. by focus change, selection change, keyboard change, etc.\r\n *\r\n * @param {Object} outputTarget The OutputTarget that has focus\r\n * @returns {Object} A RuleBehavior object describing the cumulative effects of\r\n * all matched keyboard rules\r\n */\r\n performNewContextEvent(outputTarget: OutputTarget): RuleBehavior {\r\n const ruleBehavior = this.processNewContextEvent(this.contextDevice, outputTarget);\r\n\r\n if(ruleBehavior) {\r\n ruleBehavior.finalize(this, outputTarget, true);\r\n }\r\n return ruleBehavior;\r\n }\r\n\r\n resetContext(target?: OutputTarget) {\r\n this.layerId = 'default';\r\n\r\n // Make sure all deadkeys for the context get cleared properly.\r\n target?.resetContext();\r\n this.keyboardInterface.resetContextCache();\r\n\r\n // May be null if it's a keyboard swap.\r\n // Performed before _UpdateVKShift since the op may modify the displayed layer\r\n // Also updates the layer for predictions.\r\n if(target) {\r\n this.performNewContextEvent(target);\r\n }\r\n\r\n if(!this.contextDevice.touchable) {\r\n this._UpdateVKShift(null);\r\n }\r\n };\r\n\r\n setNumericLayer(device: DeviceSpec) {\r\n if (this.activeKeyboard) {\r\n let layout = this.activeKeyboard.layout(device.formFactor);\r\n if(layout.getLayer('numeric')) {\r\n this.layerId = 'numeric';\r\n }\r\n }\r\n };\r\n}\r\n", + "import { Keyboard, KeyboardLoaderBase as KeyboardLoader } from \"keyman/engine/keyboard\";\r\nimport { EventEmitter } from \"eventemitter3\";\r\n\r\nimport KeyboardStub from \"./keyboardStub.js\";\r\n\r\nconst KEYBOARD_PREFIX = \"Keyboard_\";\r\n\r\nfunction prefixed(text: string) {\r\n if(!text.startsWith(KEYBOARD_PREFIX)) {\r\n return KEYBOARD_PREFIX + text;\r\n } else {\r\n return text;\r\n }\r\n}\r\n\r\nexport {prefixed as toPrefixedKeyboardId};\r\n\r\nfunction withoutPrefix(text: string) {\r\n if(text.startsWith(KEYBOARD_PREFIX)) {\r\n return text.substring(KEYBOARD_PREFIX.length);\r\n } else {\r\n return text;\r\n }\r\n}\r\n\r\nexport {withoutPrefix as toUnprefixedKeyboardId};\r\n\r\ninterface EventMap {\r\n /**\r\n * Indicates that the specified stub has just been registered within the cache.\r\n *\r\n * Note for future hook: establish a listener for this event during engine init\r\n * to denote the first added stub to facilitate auto-activation of the first\r\n * keyboard to be registered.\r\n */\r\n stubadded: (stub: KeyboardStub) => void;\r\n\r\n /**\r\n * Indicates that the specified Keyboard has just been added to the cache.\r\n */\r\n keyboardadded: (keyboard: Keyboard) => void;\r\n}\r\n\r\nexport default class StubAndKeyboardCache extends EventEmitter {\r\n private stubSetTable: Record> = {};\r\n private keyboardTable: Record> = {};\r\n\r\n private readonly keyboardLoader: KeyboardLoader;\r\n\r\n constructor(keyboardLoader?: KeyboardLoader) {\r\n super();\r\n this.keyboardLoader = keyboardLoader;\r\n }\r\n\r\n getKeyboardForStub(stub: KeyboardStub): Keyboard {\r\n return stub ? this.getKeyboard(stub.KI) : null;\r\n }\r\n\r\n getKeyboard(keyboardID: string): Keyboard {\r\n if(!keyboardID) {\r\n return null;\r\n }\r\n const entry = this.keyboardTable[prefixed(keyboardID)];\r\n\r\n // Unit testing may 'trip up' in the DOM, as bundled versions of a class from one bundled\r\n // module will fail against an `instanceof` expecting the version bundled in a second.\r\n //\r\n // Thus, we filter based on `Promise`, which needs no module.\r\n return entry instanceof Promise ? null : entry;\r\n }\r\n\r\n get defaultStub(): KeyboardStub {\r\n /* See the following two StackOverflow links:\r\n * - https://stackoverflow.com/a/23202095\r\n * - https://stackoverflow.com/a/5525820\r\n *\r\n * Also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Object/values#description\r\n *\r\n * As keyboard IDs are never purely numeric, any sufficiently-recent browser will\r\n * maintain the order in which stubs were added to this cache.\r\n *\r\n * Note that if a keyboard is removed, its matching stubs are also removed, so the next most-recent\r\n * property will take precedence.\r\n *\r\n * Might possibly fail to return the oldest registered stub for the oldest of supported browsers\r\n * (i.e, Android 5.0), but will work for anything decently recent. Even then... we still supply\r\n * _a_ keyboard. Just not in a way that will seem deterministic/controllable to site designers.\r\n *\r\n * Warning: Object.values and Object.entries require Chrome for Android 54, which is higher than\r\n * API 21's base. Object.keys only requires Chrome for Android 18, so is safe.\r\n */\r\n\r\n // An `Object.keys`-based helper function. Gets the first entry of Object.values for the object.\r\n // Can be written with stronger type safety... if we get very explicit with generics during calls.\r\n // That'd be more verbose than desired here.\r\n function getFirstValue(obj: any) {\r\n const keys = Object.keys(obj);\r\n if(keys.length == 0) {\r\n return undefined;\r\n } else {\r\n return obj[keys[0]];\r\n }\r\n };\r\n\r\n const stubTable = getFirstValue(this.stubSetTable) as Record;\r\n if(!stubTable) {\r\n return undefined;\r\n } else {\r\n // First value = first registered stub for that first keyboard.\r\n // Does not consider later-added stubs, but neither does removeKeyboard - removal is \"all or nothing\".\r\n return getFirstValue(stubTable) as KeyboardStub;\r\n }\r\n }\r\n\r\n addKeyboard(keyboard: Keyboard) {\r\n const keyboardID = prefixed(keyboard.id);\r\n this.keyboardTable[keyboardID] = keyboard;\r\n\r\n this.emit('keyboardadded', keyboard);\r\n }\r\n\r\n fetchKeyboardForStub(stub: KeyboardStub) : Promise {\r\n return this.fetchKeyboard(stub.KI);\r\n }\r\n\r\n isFetchingKeyboard(keyboardID: string): boolean {\r\n if(!keyboardID) {\r\n throw new Error(\"Keyboard ID must be specified\");\r\n }\r\n\r\n keyboardID = prefixed(keyboardID);\r\n\r\n const cachedEntry = this.keyboardTable[keyboardID];\r\n return cachedEntry instanceof Promise;\r\n }\r\n\r\n fetchKeyboard(keyboardID: string): Promise {\r\n if(!keyboardID) {\r\n throw new Error(\"Keyboard ID must be specified\");\r\n }\r\n\r\n if(!this.keyboardLoader) {\r\n throw new Error(\"Cannot load keyboards; this cache was configured without a loader\");\r\n }\r\n\r\n keyboardID = prefixed(keyboardID);\r\n\r\n const cachedEntry = this.keyboardTable[keyboardID];\r\n if(cachedEntry instanceof Keyboard) {\r\n return Promise.resolve(cachedEntry);\r\n } else if(cachedEntry instanceof Promise) {\r\n return cachedEntry;\r\n }\r\n\r\n const stub = this.getStub(keyboardID, null);\r\n if(!stub) {\r\n throw new Error(`No stub for ${withoutPrefix(keyboardID)} has been registered`);\r\n }\r\n\r\n if(!stub.filename) {\r\n throw new Error(`The registered stub for ${withoutPrefix(keyboardID)} lacks a path to the main keyboard file`);\r\n }\r\n\r\n const promise = this.keyboardLoader.loadKeyboardFromStub(stub);\r\n this.keyboardTable[keyboardID] = promise;\r\n\r\n promise.then((kbd) => {\r\n // Overrides the built-in ID in case of keyboard namespacing.\r\n kbd.scriptObject[\"KI\"] = keyboardID;\r\n this.addKeyboard(kbd);\r\n }).catch((err) => {\r\n delete this.keyboardTable[keyboardID];\r\n throw err;\r\n })\r\n\r\n return promise;\r\n }\r\n\r\n addStub(stub: KeyboardStub) {\r\n const keyboardID = prefixed(stub.KI);\r\n const stubTable = this.stubSetTable[keyboardID] = this.stubSetTable[keyboardID] ?? {};\r\n stubTable[stub.KLC] = stub;\r\n\r\n this.emit('stubadded', stub);\r\n }\r\n\r\n findMatchingStub(stub: KeyboardStub) {\r\n return this.getStub(stub.KI, stub.KLC);\r\n }\r\n\r\n getStub(keyboardID: string, languageID: string): KeyboardStub;\r\n getStub(keyboard: Keyboard, languageID?: string): KeyboardStub;\r\n getStub(arg0: string | Keyboard, arg1?: string): KeyboardStub {\r\n let keyboardID: string;\r\n let languageID = arg1 || '---';\r\n\r\n if(arg0 instanceof Keyboard) {\r\n keyboardID = arg0.id;\r\n } else {\r\n keyboardID = arg0;\r\n }\r\n\r\n if(keyboardID) {\r\n keyboardID = prefixed(keyboardID);\r\n }\r\n\r\n const stubTable = this.stubSetTable[keyboardID] ?? {};\r\n\r\n if(languageID != '---') {\r\n return stubTable[languageID];\r\n } else {\r\n const keys = Object.keys(stubTable);\r\n if(keys.length == 0) {\r\n return null;\r\n } else {\r\n return stubTable[keys[0]];\r\n }\r\n };\r\n }\r\n\r\n /**\r\n * Removes all metadata (stubs) associated with a specific keyboard from the cache, optionally\r\n * removing the cached keyboard as well.\r\n * @param keyboard Either the keyboard ID or `Keyboard` instance\r\n * @param purge If `true`, will also purge the `Keyboard` instance itself from the cache.\r\n * If `false`, only forgets the metadata (stubs).\r\n */\r\n forgetKeyboard(keyboard: string | Keyboard, purge: boolean = false) {\r\n let id: string = (keyboard instanceof Keyboard) ? keyboard.id : prefixed(keyboard);\r\n\r\n if(this.stubSetTable[id]) {\r\n delete this.stubSetTable[id];\r\n }\r\n\r\n if(purge && this.keyboardTable[id]) {\r\n delete this.keyboardTable[id];\r\n }\r\n }\r\n\r\n getStubList(): KeyboardStub[] {\r\n let arr: KeyboardStub[] = [];\r\n\r\n const kbdIds = Object.keys(this.stubSetTable);\r\n for(let kbdId of kbdIds) {\r\n let row = this.stubSetTable[kbdId];\r\n const langIds = Object.keys(row);\r\n for(let langId of langIds) {\r\n arr.push(row[langId]);\r\n }\r\n }\r\n\r\n return arr;\r\n }\r\n}", + "import {\r\n type KeyboardAPIPropertySpec as APISimpleKeyboard,\r\n type KeyboardAPIPropertyMultilangSpec as APICompoundKeyboard,\r\n KeyboardProperties,\r\n type LanguageAPIPropertySpec,\r\n} from 'keyman/engine/keyboard';\r\nimport { toPrefixedKeyboardId as prefixed } from './stubAndKeyboardCache.js';\r\n\r\n\r\n// Language regions as defined by cloud server\r\nexport const REGIONS = ['World','Africa','Asia','Europe','South America','North America','Oceania','Central America','Middle East'];\r\nexport const REGION_CODES = ['un','af','as','eu','sa','na','oc','ca','me'];\r\n\r\nexport type KeyboardAPISpec = (APISimpleKeyboard | APICompoundKeyboard) & {\r\n displayName?: string;\r\n filename: string\r\n};\r\n\r\nexport interface RawKeyboardStub extends KeyboardStub {};\r\n\r\n/*\r\n * Get keyboard path (relative or absolute)\r\n * KeymanWeb 2 revised keyboard location specification:\r\n * (a) absolute URL (includes ':') - load from specified URL\r\n * (b) relative URL (starts with /, ./, ../) - load with respect to current page\r\n * (c) filename only (anything else) - prepend keyboards option to URL\r\n * (e.g. default keyboards option will be set by Cloud)\r\n *\r\n * So, to fully interpret the following regex, it detects the following patterns (at minimum):\r\n * ../file (but not .../file)\r\n * ./file\r\n * /file\r\n * http:// (on the colon)\r\n * hello:world (on the colon) - that one miiiight be less intentional, though. Would 'fall\r\n * over' on attempted use anyway, since it's not a valid path.\r\n *\r\n * Alternative clearer version - '^(\\.{0,2}/)|(:)'?\r\n * Unless backslashes should be able to replace dots?\r\n */\r\nconst REGEX_FOR_PRECONFIGURED_PATH=RegExp('^(([\\\\.]/)|([\\\\.][\\\\.]/)|(/))|(:)');\r\n\r\nfunction configureFilePathing(path: string, configurationBasePath: string) {\r\n configurationBasePath = configurationBasePath || '';\r\n if(path && !REGEX_FOR_PRECONFIGURED_PATH.test(path)) {\r\n return configurationBasePath + path;\r\n } else {\r\n return path;\r\n }\r\n}\r\n\r\nexport default class KeyboardStub extends KeyboardProperties {\r\n KR: string;\r\n KRC: string;\r\n KF: string;\r\n\r\n KP?: string;\r\n\r\n // For the first flavor of constructor, note that Developer relies on KMW's path config to complete the paths...\r\n // even though supplying an 'internal'-style stub.\r\n public constructor(rawStub: RawKeyboardStub, keyboardBaseUri?: string, fontBaseUri?: string);\r\n public constructor(apiSpec: APISimpleKeyboard & { filename: string }, keyboardBaseUri?: string, fontBaseUri?: string);\r\n public constructor(kbdId: string, lngId: string);\r\n constructor(arg0: string | RawKeyboardStub | (APISimpleKeyboard & { filename: string }), arg1?: string, arg2?: string) {\r\n if(typeof arg0 !== 'string') {\r\n if(arg0.id !== undefined) {\r\n let apiSpec = arg0 as APISimpleKeyboard & { filename: string };\r\n apiSpec.id = prefixed(apiSpec.id);\r\n super(apiSpec, arg2);\r\n this.KF = configureFilePathing(apiSpec.filename, arg1);\r\n this.mapRegion(apiSpec.languages);\r\n } else {\r\n let rawStub = arg0 as RawKeyboardStub;\r\n rawStub.KI = prefixed(rawStub.KI);\r\n super(rawStub, arg2);\r\n\r\n this.KF = configureFilePathing(rawStub.KF, arg1);\r\n this.KP = rawStub.KP;\r\n this.KR = rawStub.KR;\r\n this.KRC = rawStub.KRC;\r\n\r\n\r\n return;\r\n }\r\n } else {\r\n super(prefixed(arg0), arg1);\r\n }\r\n }\r\n\r\n private mapRegion(language: LanguageAPIPropertySpec) {\r\n // Accept region as number (from Cloud server), code, or name\r\n const region=language.region;\r\n let rIndex=0;\r\n if(typeof(region) == 'number') {\r\n if(region < 1 || region > 9) {\r\n rIndex = 0;\r\n } else {\r\n rIndex = region-1;\r\n }\r\n } else if(typeof(region) == 'string') {\r\n let list = (region.length == 2 ? REGION_CODES : REGIONS);\r\n for(let i=0; i {\r\n // The deprecated `language` is assigned to satisfy TS type-checking.\r\n const intermediate = {...arg, languages: language, language: undefined as LanguageAPIPropertySpec};\r\n const stub: KeyboardStub = new KeyboardStub(intermediate, keyboardBaseUri, fontBaseUri);\r\n\r\n stubs.push(stub);\r\n })\r\n\r\n return stubs;\r\n }\r\n\r\n public merge(stub: KeyboardStub) {\r\n this.KL ||= stub.KL;\r\n this.KR ||= stub.KR;\r\n this.KRC ||= stub.KRC;\r\n this.KN ||= stub.KN;\r\n this.KF ||= stub.KF;\r\n this.KFont ||= stub.KFont;\r\n this.KOskFont ||= stub.KOskFont;\r\n\r\n if(stub._displayName) {\r\n this._displayName ||= stub._displayName;\r\n }\r\n }\r\n\r\n public validateForCustomKeyboard(): Error {\r\n if(super.validateForCustomKeyboard() || !this.KF || !this.KR) {\r\n return new Error('To use a custom keyboard, you must specify file name, keyboard id, keyboard name, language, language code, and region.');\r\n } else {\r\n return null;\r\n }\r\n }\r\n}\r\n\r\n// Information about a keyboard that fails to get added\r\nexport interface ErrorStub {\r\n keyboard?: {\r\n id: string;\r\n name: string;\r\n },\r\n language?: {\r\n id?: string;\r\n name?: string;\r\n }\r\n\r\n error: Error;\r\n}\r\n\r\nexport function mergeAndResolveStubPromises(keyboardStubs: (KeyboardStub|ErrorStub)[], errorStubs: ErrorStub[]) :\r\n Promise<(KeyboardStub|ErrorStub)[]> {\r\n if (errorStubs.length == 0) {\r\n return Promise.resolve(keyboardStubs);\r\n } if (keyboardStubs.length == 0) {\r\n return Promise.reject(errorStubs);\r\n } else {\r\n // Merge this with errorStubs\r\n let result: (KeyboardStub|ErrorStub)[] = keyboardStubs;\r\n return Promise.resolve(result.concat(errorStubs));\r\n }\r\n}", + "import { EventEmitter } from 'eventemitter3';\r\n\r\nimport { PathConfiguration } from 'keyman/engine/interfaces';\r\n\r\nimport { default as KeyboardStub, ErrorStub, KeyboardAPISpec, mergeAndResolveStubPromises } from '../keyboardStub.js';\r\nimport { LanguageAPIPropertySpec, ManagedPromise, Version } from 'keyman/engine/keyboard';\r\nimport CloudRequesterInterface from './requesterInterface.js';\r\n\r\n// For when the API call straight-up times out.\r\nexport const CLOUD_TIMEOUT_ERR = \"The Cloud API request timed out.\";\r\n// Currently cannot distinguish between \"no matching keyboard\" and other script-load errors.\r\nexport const CLOUD_MALFORMED_OBJECT_ERR = \"Could not find a keyboard with that ID.\";\r\n// Represents unspecified errors that occur when registering the results of a successful API call.\r\nexport const CLOUD_STUB_REGISTRATION_ERR = \"The Cloud API failed to find an appropriate keyboard.\";\r\n// Represents custom, specified KMW errors that occur when registering the results of a successful API call.\r\nexport const CLOUD_REGISTRATION_ERR = \"Error occurred while registering keyboards: \";\r\n\r\nexport const MISSING_KEYBOARD = function(kbdid: string) {\r\n return kbdid + ' keyboard not found.';\r\n}\r\n\r\ntype CloudQueryOptions = {\r\n context: 'keyboard' | 'language';\r\n keyboardid?: string,\r\n keyboardBaseUri?: string,\r\n fontBaseUri?: string\r\n}\r\n\r\ntype CloudKeyboardQueryResult = {\r\n /**\r\n * A 1D array is returned for keyboard-id based queries: `addKeyboards('sil_cameroon_qwerty')`\r\n * returns a single array with one entry.\r\n *\r\n * A 2D array is returned for language-code based keyboard queries: `addKeyboards('@fr,@en')`\r\n * returns two arrays of keyboards, one per language code.\r\n * - First index = fr\r\n * - Second = en\r\n */\r\n keyboard: KeyboardAPISpec | KeyboardAPISpec[] | KeyboardAPISpec[][],\r\n options: { context: 'keyboard' } & CloudQueryOptions,\r\n error?: string,\r\n timerid: string\r\n};\r\n\r\ntype CloudLanguagesQueryResult = {\r\n languages: LanguageAPIPropertySpec[],\r\n options: { context: 'language' } & CloudQueryOptions,\r\n error?: string,\r\n keyboardid?: string,\r\n timerid: string\r\n}\r\n\r\nexport type CloudQueryResult = CloudKeyboardQueryResult | CloudLanguagesQueryResult;\r\n\r\ninterface EventMap {\r\n 'unboundregister': (registration: ReturnType) => void\r\n}\r\n\r\nexport default class CloudQueryEngine extends EventEmitter {\r\n private cloudResolutionPromises: Map> = new Map();\r\n\r\n private _languageListPromise: ManagedPromise;\r\n private languageFetchStarted: boolean = false;\r\n\r\n private requestEngine: CloudRequesterInterface;\r\n private pathConfig: PathConfiguration;\r\n\r\n constructor(requestEngine: CloudRequesterInterface, pathConfig: PathConfiguration) {\r\n super();\r\n\r\n this.requestEngine = requestEngine;\r\n this.pathConfig = pathConfig;\r\n\r\n this._languageListPromise = new ManagedPromise;\r\n }\r\n\r\n public get languageListPromise(): Promise {\r\n if(!this.languageFetchStarted) {\r\n this.languageFetchStarted = true;\r\n\r\n this.keymanCloudRequest('', true).catch((error) => {\r\n // If promise is not error, then...\r\n this.languageFetchStarted = false;\r\n\r\n // We should allow retries.\r\n this._languageListPromise.reject(error);\r\n this._languageListPromise = new ManagedPromise;\r\n });\r\n }\r\n\r\n return this._languageListPromise.corePromise;\r\n }\r\n\r\n /**\r\n * Request keyboard metadata from the Keyman Cloud keyboard metadata server\r\n *\r\n * @param {string} cmd command string\r\n * @param {boolean?} byLanguage if true, context=languages, else context=keyboards\r\n * @returns {Promise<(KeyboardStub[]>} Promise of added keyboard stubs\r\n **/\r\n keymanCloudRequest(cmd: string, byLanguage: true): Promise;\r\n keymanCloudRequest(cmd: string, byLanguage: false): Promise;\r\n keymanCloudRequest(cmd: string, byLanguage?: boolean): Promise | Promise {\r\n // Some basic support toward #5044, but definitely not a full solution toward it.\r\n // Wraps the cloud API keyboard-stub request in a Promise, allowing response on network\r\n // and/or parser errors. Also detects when `register` returns due to an error case that\r\n // does not throw errors. (There are a few such \"empty\" `return` statements there.)\r\n const URL='https://api.keyman.com/cloud/4.0/'\r\n + ((arguments.length > 1) && byLanguage ? 'languages' : 'keyboards');\r\n\r\n\r\n const queryConfig = '?jsonp=keyman.register&languageidtype=bcp47&version='+Version.CURRENT.toString();\r\n\r\n const query = URL + queryConfig + cmd;\r\n\r\n let { promise, queryId } = this.requestEngine.request(query);\r\n this.cloudResolutionPromises.set(queryId, promise);\r\n\r\n promise.finally(() => {\r\n this.cloudResolutionPromises.delete(queryId);\r\n });\r\n\r\n return promise.corePromise as any;\r\n }\r\n\r\n /**\r\n * Call back from cloud for adding keyboard metadata\r\n *\r\n * This function should be published to scripts returned from the cloud;\r\n * they will expect to call it as `keyman.register`.\r\n *\r\n * @param {Object} x metadata object\r\n **/\r\n registerFromCloud = (x: CloudQueryResult) => {\r\n const promiseid = Number.parseInt(x.timerid);\r\n\r\n let result: KeyboardStub[] | LanguageAPIPropertySpec[] | Error;\r\n try {\r\n result = this._registerCore(x);\r\n } catch(err) {\r\n result = new Error(CLOUD_REGISTRATION_ERR + err);\r\n }\r\n\r\n if(!promiseid) {\r\n this.emit('unboundregister', result);\r\n return;\r\n } else {\r\n const promise = this.cloudResolutionPromises.get(promiseid);\r\n\r\n if(!promise) {\r\n this.emit('unboundregister', result);\r\n return;\r\n } else {\r\n try {\r\n if(result instanceof Error) {\r\n promise.reject(result as Error);\r\n } else {\r\n promise.resolve(result as any);\r\n }\r\n } finally {\r\n this.cloudResolutionPromises.delete(promiseid);\r\n }\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Call back from cloud for adding keyboard metadata\r\n *\r\n * @param {Object} queryResult metadata object\r\n **/\r\n private _registerCore(queryResult: CloudQueryResult): KeyboardStub[] | LanguageAPIPropertySpec[] | Error { // TODO (#5044): should return heterogenous type; allow array of stubs.\r\n const options: CloudQueryOptions = queryResult.options;\r\n\r\n // Font path defined by cloud entry\r\n let fontPath=options['fontBaseUri'];\r\n\r\n // or overridden locally, in page source\r\n if(this.pathConfig.fonts != '') {\r\n fontPath=this.pathConfig.fonts;\r\n }\r\n else {\r\n // If there's no preconfigured option for font paths, uses the cloud's returned `fontPath` in its place.\r\n this.pathConfig.updateFontPath(fontPath);\r\n }\r\n\r\n // Indicate if unable to register keyboard\r\n if(typeof(queryResult.error) == 'string') {\r\n // Currently unreachable (24 May 2021 - API returns a 404; returned 'script' does not call register)\r\n var badName='';\r\n if(typeof(queryResult.options.keyboardid) == 'string') {\r\n let keyboardId = queryResult.options.keyboardid;\r\n badName = keyboardId.substring(0,1).toUpperCase() + keyboardId.substring(1);\r\n }\r\n\r\n return new Error(MISSING_KEYBOARD(badName));\r\n }\r\n\r\n // Ignore callback unless the context is defined\r\n if(typeof(options) == 'undefined' || typeof(options['context']) == 'undefined') {\r\n return new Error(CLOUD_MALFORMED_OBJECT_ERR);\r\n }\r\n\r\n // Register each keyboard for the specified language codes\r\n let stubs: KeyboardStub[] = [];\r\n\r\n if(options.context == 'keyboard') {\r\n let i, kp=(queryResult as CloudKeyboardQueryResult).keyboard;\r\n // Process array of keyboard definitions\r\n if(Array.isArray(kp)) {\r\n for(i=0; i stub.KLC == lgCode)];\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Build 362: addKeyboardArray() link to Cloud. One or more arguments may be used\r\n *\r\n * @param x keyboard name string or keyboard metadata JSON object\r\n * @returns resolved or rejected promise with merged array of stubs.\r\n */\r\n async fetchCloudStubs(cloudList: string[]): Promise<(KeyboardStub|ErrorStub)[]> {\r\n // // Ensure keymanweb is initialized before continuing to add keyboards\r\n // if(!this.keymanweb.initialized) {\r\n // await this.deferment;\r\n // }\r\n\r\n if(cloudList.length == 0) {\r\n return Promise.resolve([]);\r\n }\r\n\r\n // Update the keyboard metadata list from keyman.com - build the command\r\n let cmd='&keyboardid=';\r\n let comma = '';\r\n for(let i=0; i {\r\n // Internal, undocumented use-case of `keyman.register`: precached keyboard loading\r\n // Other uses may trigger errors, especially if there's a type-structure mismatch.\r\n // Those errors should not be handled here; let them surface.\r\n if(Array.isArray(registration)) {\r\n registration.forEach((entry) => {\r\n if(entry instanceof KeyboardStub) {\r\n this.cache.addStub(entry);\r\n }\r\n });\r\n }\r\n });\r\n }\r\n\r\n addKeyboardArray(x: (string|RawKeyboardMetadata)[]): Promise<(KeyboardStub | ErrorStub)[]> {\r\n let completeStubs: KeyboardStub[] = [];\r\n let incompleteStubs: KeyboardStub[] = [];\r\n let stubs: KeyboardStub[] = [];\r\n let identifiers: string[] = [];\r\n let errorStubs: ErrorStub[] = [];\r\n\r\n // #region Parameter preprocessing: is incoming data already 'complete', or do we need to fetch the 'complete' version?\r\n\r\n for(let entry of x) {\r\n if(typeof entry == 'string') {\r\n if(entry.length > 0) {\r\n identifiers.push(entry);\r\n }\r\n } else { // is some sort of object.\r\n // @ts-ignore\r\n if(entry['KI'] || entry['KL'] || entry['KLC'] || entry['KFont'] || entry['KOskFont']) {\r\n stubs.push(new KeyboardStub(entry as RawKeyboardStub));\r\n } else {\r\n let apiSpecEntry = entry as KeyboardAPISpec;\r\n if(typeof(apiSpecEntry.language) != \"undefined\") {\r\n console.warn(\"The 'language' property for keyboard stubs has been deprecated. Please use the 'languages' property instead.\");\r\n }\r\n apiSpecEntry.languages ||= apiSpecEntry.language;\r\n\r\n if(typeof apiSpecEntry.languages === 'undefined') {\r\n let msg = 'To use keyboard \\'' + apiSpecEntry.id + '\\', you must specify languages.';\r\n errorStubs.push(convertToErrorStub(apiSpecEntry, msg));\r\n } else if(Array.isArray(apiSpecEntry.languages)) {\r\n let splitStubs = KeyboardStub.toStubs(apiSpecEntry, this.pathConfig.keyboards, this.pathConfig.fonts);\r\n for(let stub of splitStubs) {\r\n if(stub instanceof KeyboardStub) {\r\n stubs.push(stub);\r\n } else {\r\n errorStubs.push(stub);\r\n }\r\n }\r\n } else { // is a single-language entry.\r\n const singleLangEntry = apiSpecEntry as KeyboardAPIPropertySpec & { filename: string };\r\n stubs.push(new KeyboardStub(singleLangEntry, this.pathConfig.keyboards, this.pathConfig.fonts));\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Next pass: determine which stubs are fully-formed and which are not.\r\n for(let stub of stubs) {\r\n if(stub.KF) {\r\n let err = stub.validateForCustomKeyboard();\r\n if(err) {\r\n errorStubs.push(convertToErrorStub(stub, err));\r\n } else {\r\n completeStubs.push(stub); // completes are directly added (if possible without error)\r\n }\r\n } else {\r\n incompleteStubs.push(stub); // incompletes are only used to build a query & are not merged back later.\r\n }\r\n }\r\n\r\n // #endregion\r\n\r\n // After that, request stubs that aren't fully-formed / are just requested by string.\r\n // Verify that we have at least a keyboard ID or a language code.\r\n let cloudList: CloudRequestEntry[] = [];\r\n for(let incomplete of incompleteStubs) {\r\n if(!incomplete.KI && !incomplete.KLC) {\r\n errorStubs.push(convertToErrorStub(incomplete, \"Cannot fetch keyboard information without a keyboard ID or language code.\"));\r\n continue;\r\n }\r\n\r\n // Requests not of string form never specify a specific version.\r\n // If an 'incomplete stub', we may have prefixed the keyboard ID - undo that!\r\n const querySpec = toQuerySpecs(unprefixed(incomplete.id), incomplete.langId);\r\n if(isUniqueRequest(this.cache, cloudList, querySpec)) {\r\n cloudList.push(querySpec);\r\n }\r\n }\r\n\r\n // Double-check the incoming string-based queries, too!\r\n for(let identifier of identifiers) {\r\n const pList=identifier.split('@');\r\n let lList=[''];\r\n if(pList[0].toLowerCase() == 'english') {\r\n pList[0] = 'us';\r\n }\r\n\r\n if(pList.length > 1) {\r\n lList=pList[1].split(',');\r\n }\r\n\r\n for(let j=0; j 0 && lList[j] == '') { // for j==0 => '', no language was specified.\r\n continue;\r\n }\r\n\r\n const querySpec = toQuerySpecs(pList[0], lList[j], pList[2]);\r\n if(isUniqueRequest(this.cache, cloudList, querySpec)) {\r\n cloudList.push(querySpec);\r\n }\r\n }\r\n }\r\n\r\n // Go ahead and register 'complete' stubs.\r\n completeStubs.forEach((stub) => this.cache.addStub(stub));\r\n\r\n // After that, note that we do merge non-fully-formed stubs with returned stubs that match?\r\n let resultPromise = this.cloudQueryEngine.fetchCloudStubs(cloudList.map((spec) => spec.toString()));\r\n return resultPromise.then((queryResults) => {\r\n for(let result of queryResults) {\r\n if(result instanceof KeyboardStub) {\r\n // Register the newly-complete stub.\r\n this.cache.addStub(result);\r\n completeStubs.push(result);\r\n } else {\r\n errorStubs.push(result);\r\n }\r\n }\r\n\r\n return [].concat(errorStubs).concat(completeStubs);\r\n });\r\n }\r\n\r\n async addLanguageKeyboards(languages: string[]): Promise<(ErrorStub | KeyboardStub)[]> {\r\n // Covers the 'add a keyboard by language name' angle.\r\n let errorStubs: ErrorStub[] = [];\r\n let fetchedLanguageList: LanguageAPIPropertySpec[] = [];\r\n\r\n try {\r\n fetchedLanguageList = await this.cloudQueryEngine.languageListPromise;\r\n } catch (error) {\r\n console.error(error);\r\n errorStubs.push({error: error});\r\n return errorStubs;\r\n }\r\n\r\n // Defer registering keyboards by language until the language list has been loaded\r\n const languageList = fetchedLanguageList;\r\n\r\n // Identify and register each keyboard by language name\r\n let cmd = '';\r\n for(let i=0; i {\r\n console.error(err);\r\n let stub: ErrorStub = {error: err};\r\n errorStubs.push(stub);\r\n return Promise.reject(errorStubs);\r\n });\r\n }\r\n\r\n async fetchCloudCatalog() {\r\n try {\r\n const stubs = await this.cloudQueryEngine.keymanCloudRequest('', false);\r\n stubs.forEach((stub) => this.cache.addStub(stub));\r\n return stubs;\r\n } catch(error) {\r\n return Promise.reject([{error: error}]);\r\n }\r\n }\r\n\r\n /**\r\n * Display warning if language name unavailable to add keyboard\r\n * @param {string} languageName\r\n * @returns string of Error message\r\n */\r\n private alertLanguageUnavailable(languageName: string): string {\r\n let msg = 'No keyboards are available for '+ languageName + '. '\r\n +'Does it have another language name?';\r\n\r\n // TODO: hooks for internal alerts!\r\n // this.keymanweb.util.internalAlert(msg);\r\n return msg;\r\n }\r\n}", + "import { ModelSpec } from 'keyman/engine/interfaces';\r\n\r\nexport default class ModelManager {\r\n // Tracks registered models by ID.\r\n private registeredModels: {[id: string]: ModelSpec} = {};\r\n\r\n // Allows for easy model lookup by language code; useful when switching keyboards.\r\n languageModelMap: {[language:string]: ModelSpec} = {};\r\n\r\n modelForLanguage(lgCode: string) {\r\n return this.languageModelMap[lgCode];\r\n }\r\n\r\n // Accessible publicly as keyman.modelCache.register(model: ModelSpec)\r\n register(model: ModelSpec): void {\r\n // Forcibly lowercase the model ID before proceeding.\r\n model.id = model.id.toLowerCase();\r\n\r\n if(JSON.stringify(model) == JSON.stringify(this.registeredModels[model.id])) {\r\n // We are already registered, let's not go through and re-register\r\n // because we'll already have the correct model active\r\n return;\r\n }\r\n this.registeredModels[model.id] = model;\r\n\r\n // Register the model for each targeted language code variant.\r\n let mm = this;\r\n model.languages.forEach(function(code: string) {\r\n // Prevent null / undefined codes; they're invalid.\r\n if(!code) {\r\n console.warn(\"Null / undefined language codes are not permitted for registration.\");\r\n return;\r\n }\r\n\r\n mm.languageModelMap[code] = model;\r\n });\r\n }\r\n\r\n unregister(modelId: string): ModelSpec {\r\n let model: ModelSpec;\r\n\r\n modelId = modelId.toLowerCase();\r\n\r\n // Remove the model from the id-lookup associative array.\r\n if(this.registeredModels[modelId]) {\r\n model = this.registeredModels[modelId];\r\n delete this.registeredModels[modelId];\r\n } else {\r\n return null;\r\n }\r\n\r\n // Ensure the model is deregistered for each targeted language code variant.\r\n let mm = this;\r\n model.languages.forEach(function(code: string) {\r\n if(mm.languageModelMap[code].id == modelId) {\r\n delete mm.languageModelMap[code];\r\n }\r\n });\r\n\r\n return model;\r\n }\r\n\r\n isRegistered(model: ModelSpec): boolean {\r\n return !! this.registeredModels[model.id.toLowerCase()];\r\n }\r\n}\r\n", + "/**\r\n * Function arrayFromNodeList\r\n * Scope Public\r\n * @param {Object} nl a node list, as returned from getElementsBy_____ methods.\r\n * Description Transforms a node list into an array. *\r\n * @return {Array}\r\n */\r\nexport function arrayFromNodeList(nl: NodeList|HTMLCollectionOf): HTMLElement[] {\r\n let res: (HTMLElement)[] = [];\r\n for(let i=0; i < nl.length; i++) {\r\n // Typing says we could get Node instances; it's up to use to use this method responsibly.\r\n res.push(nl[i] as HTMLElement);\r\n }\r\n return res;\r\n}", + "// Found a bit of magic formatting that allows dynamic return typing for a specified element tag!\r\nexport default function createUnselectableElement(nodeName:E) {\r\n const e = document.createElement(nodeName);\r\n\r\n e.style.userSelect=\"none\";\r\n e.style.webkitUserSelect = \"none\";\r\n return e;\r\n}", + "import { DeviceSpec, ManagedPromise } from '@keymanapp/web-utils';\r\nimport { type InternalKeyboardFont as KeyboardFont } from 'keyman/engine/keyboard';\r\n\r\ntype FontFamilyStyleMap = {[family: string]: HTMLStyleElement};\r\n\r\nexport class StylesheetManager {\r\n private fontStyleDefinitions: { [os: string]: FontFamilyStyleMap} = {};\r\n private linkedSheets: {\r\n sheet: HTMLStyleElement,\r\n load: ManagedPromise\r\n }[] = [];\r\n private fontPromises: Promise[] = [];\r\n private doCacheBusting: boolean;\r\n\r\n public readonly linkNode: Node;\r\n\r\n public get sheets(): readonly HTMLStyleElement[] {\r\n return this.linkedSheets.map((entry) => entry.sheet);\r\n }\r\n\r\n public constructor(linkNode?: Node, doCacheBusting?: boolean) {\r\n if(!linkNode) {\r\n let _ElemHead=document.getElementsByTagName('HEAD');\r\n if(_ElemHead.length > 0) {\r\n linkNode = _ElemHead[0];\r\n } else {\r\n linkNode = document.body; // Won't work on [old?] Chrome, ah well\r\n }\r\n }\r\n this.linkNode = linkNode;\r\n this.doCacheBusting = doCacheBusting || false;\r\n }\r\n\r\n linkStylesheet(sheet: HTMLStyleElement | HTMLLinkElement) {\r\n if(!(sheet instanceof HTMLLinkElement) && !sheet.innerHTML) {\r\n return;\r\n }\r\n\r\n const promise = new ManagedPromise();\r\n if(sheet instanceof HTMLLinkElement) {\r\n sheet.onload = () => promise.resolve();\r\n } else {\r\n // If it's an inline sheet, it's essentially already loaded.\r\n // The microtask delay this induces (for type compat) also\r\n // gives the browser time to apply the inlined-style.\r\n promise.resolve();\r\n }\r\n\r\n this.linkedSheets.push({\r\n sheet: sheet,\r\n load: promise\r\n });\r\n this.linkNode.appendChild(sheet);\r\n }\r\n\r\n /**\r\n * Provides a `Promise` that resolves when all currently-linked stylesheets have loaded.\r\n * Any change to the set of linked sheets after the initial call will be ignored.\r\n */\r\n async allLoadedPromise(): Promise {\r\n const allPromises = this.linkedSheets.map((entry) => entry.load.corePromise);\r\n if(Promise.allSettled) {\r\n // allSettled - Chrome 76 / Safari 13\r\n // Delays for settling (either then OR catch) for ALL promises.\r\n await Promise.allSettled(allPromises)\r\n } else {\r\n // all - Chrome 32\r\n // If an error happens, .all instantly resolves regardless of state of\r\n // other Promises.\r\n await Promise.all(allPromises);\r\n }\r\n }\r\n\r\n /**\r\n * Build a stylesheet with a font-face CSS descriptor for the embedded font appropriate\r\n * for the browser being used\r\n *\r\n * @param {Object} fd keymanweb font descriptor (internal format; should be preprocessed)\r\n * @param {string} fontPathRoot Should correspond to `this.keyman.options['fonts']`\r\n **/\r\n addStyleSheetForFont(fd: KeyboardFont, fontPathRoot: string, os?: DeviceSpec.OperatingSystem) {\r\n // Test if a valid font descriptor\r\n if(!fd) {\r\n return null;\r\n }\r\n\r\n if(typeof(fd.files) == 'undefined') {\r\n return null;\r\n }\r\n\r\n const fontKey = fd.family;\r\n let source: string;\r\n\r\n let i, ttf='', woff='', fList=[], data='';\r\n\r\n // TODO: 22 Aug 2014: check that font path passed from cloud is actually used!\r\n\r\n if(!os) {\r\n os = DeviceSpec.OperatingSystem.Other; // as a fallback option.\r\n }\r\n\r\n // Do not add a new font-face style sheet if already added for this font\r\n const fontStyleMap = this.fontStyleDefinitions[os] = this.fontStyleDefinitions[os] || {};\r\n\r\n if(fontStyleMap[fontKey]) {\r\n const sheet = fontStyleMap[fontKey];\r\n\r\n if(!sheet.parentNode) {\r\n this.linkStylesheet(sheet);\r\n }\r\n return null;\r\n }\r\n\r\n if(typeof(fd.files) == 'string') {\r\n fList[0]=fd.files;\r\n } else {\r\n fList=fd.files;\r\n }\r\n\r\n for(i=0;i 0) ttf=fList[i];\r\n if(fList[i].toLowerCase().indexOf('.ttf') > 0) ttf=fList[i];\r\n if(fList[i].toLowerCase().indexOf('.woff') > 0) woff=fList[i];\r\n }\r\n\r\n // Font path qualified to support page-relative fonts (build 347)\r\n if(ttf != '' && (ttf.indexOf('/') < 0)) {\r\n ttf = fontPathRoot+ttf;\r\n }\r\n\r\n if(woff != '' && (woff.indexOf('/') < 0)) {\r\n woff = fontPathRoot+woff;\r\n }\r\n\r\n // Build the font-face definition according to the browser being used\r\n var s='@font-face {\\nfont-family:'\r\n + fd.family + ';\\nfont-style:normal;\\nfont-weight:normal;\\n';\r\n\r\n // Build the font source string according to the browser,\r\n // but return without adding the style sheet if the required font type is unavailable\r\n\r\n // Modern browsers: use WOFF, TTF and fallback finally to SVG. Don't provide EOT\r\n if(data) {\r\n // For inline-defined fonts:\r\n const formatStartIndex = 'data:font/'.length;\r\n const format = data.substring(formatStartIndex, data.indexOf(';', formatStartIndex));\r\n s +=`src:url('${data}'), format('${format}');`;\r\n } else if(os == DeviceSpec.OperatingSystem.iOS) {\r\n if(ttf != '') {\r\n if(this.doCacheBusting) {\r\n ttf = this.cacheBust(ttf);\r\n }\r\n source = \"url('\"+encodeURI(ttf)+\"') format('truetype')\";\r\n }\r\n } else {\r\n if(woff != '') {\r\n source = \"url('\"+encodeURI(woff)+\"') format('woff')\";\r\n }\r\n\r\n if(ttf != '') {\r\n source = \"url('\"+encodeURI(ttf)+\"') format('truetype')\";\r\n }\r\n }\r\n\r\n if(!source) {\r\n return null;\r\n }\r\n\r\n s += 'src:'+source+';';\r\n\r\n s=s+'\\n}\\n';\r\n\r\n const sheet = createStyleSheet(s);\r\n fontStyleMap[fontKey] = sheet;\r\n\r\n /* https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API\r\n * Compat: Chrome 35... _just_ on the unupdated-Android 5.0 threshold.\r\n *\r\n * Note: this could probably wholesale-replace the stylesheet!\r\n * Would need: `document.fonts.add(fontFace)` - does not have to wait for the load() Promise.\r\n *\r\n * For now, we're using this solely to detect when the font has been succesfully loaded.\r\n */\r\n const fontFace = new FontFace(fd.family, source);\r\n\r\n let loadPromise = fontFace.load();\r\n const clearPromise = () => {\r\n this.fontPromises = this.fontPromises.filter((entry) => entry != loadPromise);\r\n }\r\n this.fontPromises.push(loadPromise.then(clearPromise).catch(clearPromise));\r\n\r\n this.linkStylesheet(sheet);\r\n\r\n return sheet;\r\n }\r\n\r\n private cacheBust(uri: string) {\r\n // Our WebView version directly sets the keyboard path, and it may replace the file\r\n // after KMW has loaded. We need cache-busting to prevent the new version from\r\n // being ignored.\r\n return uri + \"?v=\" + (new Date()).getTime(); /*cache buster*/\r\n }\r\n\r\n /**\r\n * Add a reference to an external stylesheet file\r\n *\r\n * @param {string} href path to stylesheet file\r\n */\r\n linkExternalSheet(href: string, force?: boolean): HTMLStyleElement {\r\n try {\r\n if(!force && document.querySelector(\"link[href=\"+JSON.stringify(href)+\"]\") != null) {\r\n // We've already linked this stylesheet, don't do it again\r\n return null;\r\n }\r\n } catch(e) {\r\n // We've built an invalid href, somehow?\r\n return null;\r\n }\r\n\r\n const linkElement=document.createElement('link');\r\n linkElement.type='text/css';\r\n linkElement.rel='stylesheet';\r\n linkElement.href=href;\r\n\r\n this.linkStylesheet(linkElement);\r\n return linkElement;\r\n }\r\n\r\n public unlink(stylesheet: HTMLStyleElement) {\r\n const index = this.linkedSheets.findIndex((entry) => entry.sheet == stylesheet);\r\n if(index > -1) {\r\n const tuple = this.linkedSheets.splice(index, 1);\r\n // Ensure we don't leave `await`s that were waiting on the stylesheet hanging.\r\n tuple[0].load.resolve();\r\n stylesheet.parentNode.removeChild(stylesheet);\r\n return true;\r\n }\r\n\r\n return false;\r\n }\r\n\r\n public unlinkAll() {\r\n for(let tuple of this.linkedSheets) {\r\n const sheet = tuple.sheet;\r\n if(sheet.parentNode) {\r\n sheet.parentNode.removeChild(sheet);\r\n }\r\n // Clear out any lingering `await`s.\r\n tuple.load.resolve();\r\n }\r\n\r\n this.linkedSheets.splice(0, this.linkedSheets.length);\r\n }\r\n}\r\n\r\n/**\r\n * Add a stylesheet to a page programmatically, for use by the OSK, the UI or the page creator\r\n *\r\n * @param {string} s style string\r\n * @return {Object} returns the object reference\r\n **/\r\nexport function createStyleSheet(styleString: string): HTMLStyleElement {\r\n var _ElemStyle: HTMLStyleElement = document.createElement<'style'>('style');\r\n\r\n _ElemStyle.type = 'text/css';\r\n _ElemStyle.appendChild(document.createTextNode(styleString));\r\n\r\n return _ElemStyle;\r\n}", + "/**\r\n * Get orientation of tablet or phone display\r\n *\r\n * @return {boolean}\r\n */\r\nexport default function landscapeView(): boolean\t{ // new for I3363 (Build 301)\r\n var orientation: number;\r\n\r\n // Assume portrait mode if orientation undefined\r\n if(typeof window.orientation != 'undefined') { // Used by iOS Safari\r\n // Else landscape for +/-90, portrait for 0, +/-180\r\n orientation = window.orientation as number;\r\n } else if(typeof window.screen.orientation != 'undefined') { // Used by Firefox, Chrome\r\n orientation = window.screen.orientation.angle;\r\n }\r\n\r\n if(orientation !== undefined) {\r\n return (Math.abs(orientation/90) == 1);\r\n } else {\r\n return false;\r\n }\r\n}", + "type DecodedCookieFieldValue = string | number | boolean;\r\n\r\ntype FilteredRecordEncoder = (value: DecodedCookieFieldValue, key: string) => string;\r\ntype FilteredRecordDecoder = (value: string, key: string) => DecodedCookieFieldValue;\r\n\r\nexport default class CookieSerializer> {\r\n readonly name: string;\r\n\r\n constructor(name: string) {\r\n this.name = name;\r\n }\r\n\r\n load(decoder?: FilteredRecordDecoder): Type {\r\n return this.loadCookie(this.name, decoder || ((val: string) => val as DecodedCookieFieldValue)) as Type;\r\n }\r\n\r\n save(cookie: Type, encoder?: FilteredRecordEncoder) {\r\n this.saveCookie(this.name, cookie, encoder || ((val: DecodedCookieFieldValue) => val as string));\r\n }\r\n\r\n /**\r\n * Document cookie parsing for use by kernel, OSK, UI etc.\r\n *\r\n * @return {Object} array of names and strings\r\n */\r\n private _loadRawCookies(): Record {\r\n let v: Record = {};\r\n if(typeof(document.cookie) != 'undefined' && document.cookie != '') {\r\n let c = document.cookie.split(/;\\s*/);\r\n for(let i = 0; i < c.length; i++) {\r\n let d = c[i].split('=');\r\n if(d.length == 2) {\r\n v[d[0]] = d[1];\r\n }\r\n }\r\n }\r\n\r\n return v;\r\n }\r\n\r\n /**\r\n * Document cookie parsing for use by kernel, OSK, UI etc.\r\n *\r\n * @param {string} cookieName cookie name\r\n * @return {Object} array of variables and values\r\n */\r\n private loadCookie(cookieName: string, decoder: FilteredRecordDecoder): Record {\r\n let cookie: Record = {};\r\n let allCookies = this._loadRawCookies();\r\n const encodedCookie = allCookies[cookieName];\r\n\r\n if(encodedCookie) {\r\n let rawDecode = decodeURIComponent(encodedCookie).split(';');\r\n for(let i=0; i 1) {\r\n const [key, value] = record;\r\n // key, value\r\n cookie[key] = decoder(value, key);\r\n } else {\r\n // key, \r\n cookie[record[0]] = '';\r\n }\r\n }\r\n }\r\n return cookie;\r\n }\r\n\r\n /**\r\n * Standard cookie saving for use by kernel, OSK, UI etc.\r\n *\r\n * @param {string} cookieName name of cookie\r\n * @param {Object} cookieValueMap object with array of named arguments and values\r\n */\r\n private saveCookie(cookieName: string, cookieValueMap: Record, encoder: FilteredRecordEncoder) {\r\n let serialization='';\r\n for(let key in cookieValueMap) {\r\n serialization += key + '=' + encoder(cookieValueMap[key], key) + \";\";\r\n }\r\n\r\n let d = new Date(new Date().valueOf() + 1000 * 60 * 60 * 24 * 30).toUTCString();\r\n let cookieConfig = ' path=/; expires=' + d; //Fri, 31 Dec 2099 23:59:59 GMT;';\r\n document.cookie = `${cookieName}=${encodeURIComponent(serialization)}; ${cookieConfig}`;\r\n }\r\n}", + "/**\r\n * Function getAbsoluteX\r\n * Scope Public\r\n * @param {Object} Pobj HTML element\r\n * @return {number}\r\n * Description Returns x-coordinate of Pobj element absolute position with respect to page\r\n */\r\nexport function getAbsoluteX(Pobj: HTMLElement): number { // I1476 - Handle SELECT overlapping END\r\n var Lobj: HTMLElement\r\n\r\n if(!Pobj) {\r\n return 0;\r\n }\r\n\r\n var Lcurleft = Pobj.offsetLeft ? Pobj.offsetLeft : 0;\r\n Lobj = Pobj; \t// I2404 - Support for IFRAMEs\r\n\r\n if (Lobj.offsetParent) {\r\n while (Lobj.offsetParent) {\r\n Lobj = Lobj.offsetParent as HTMLElement;\r\n Lcurleft += Lobj.offsetLeft;\r\n }\r\n\r\n // On mobile devices, the OSK uses 'fixed' - this requires some extra offset work to handle.\r\n let Ldoc = Lobj.ownerDocument;\r\n if(Lobj.style.position == 'fixed' && Ldoc && Ldoc.scrollingElement) {\r\n Lcurleft += Ldoc.scrollingElement.scrollLeft;\r\n }\r\n }\r\n // Correct position if element is within a frame (but not if the controller is in document within that frame)\r\n // We used to reference a KMW state variable `this.keyman._MasterDocument`, but it was only ever set to `window.document`.\r\n if(Lobj && Lobj.ownerDocument && (Pobj.ownerDocument != window.document)) {\r\n var Ldoc=Lobj.ownerDocument; // I2404 - Support for IFRAMEs\r\n\r\n if(Ldoc && Ldoc.defaultView && Ldoc.defaultView.frameElement) {\r\n return Lcurleft + getAbsoluteX(Ldoc.defaultView.frameElement) - Ldoc.documentElement.scrollLeft;\r\n }\r\n }\r\n return Lcurleft;\r\n}\r\n\r\n/**\r\n * Function getAbsoluteY\r\n * Scope Public\r\n * @param {Object} Pobj HTML element\r\n * @return {number}\r\n * Description Returns y-coordinate of Pobj element absolute position with respect to page\r\n */\r\nexport function getAbsoluteY(Pobj: HTMLElement): number {\r\n var Lobj: HTMLElement\r\n\r\n if(!Pobj) {\r\n return 0;\r\n }\r\n var Lcurtop = Pobj.offsetTop ? Pobj.offsetTop : 0;\r\n Lobj = Pobj; // I2404 - Support for IFRAMEs\r\n\r\n if (Lobj.ownerDocument && Lobj instanceof Lobj.ownerDocument.defaultView.HTMLElement) {\r\n while (Lobj.offsetParent) {\r\n Lobj = Lobj.offsetParent as HTMLElement;\r\n Lcurtop += Lobj.offsetTop;\r\n }\r\n\r\n // On mobile devices, the OSK uses 'fixed' - this requires some extra offset work to handle.\r\n let Ldoc = Lobj.ownerDocument;\r\n if(Lobj.style.position == 'fixed' && Ldoc && Ldoc.scrollingElement) {\r\n Lcurtop += Ldoc.scrollingElement.scrollTop;\r\n }\r\n }\r\n\r\n // Correct position if element is within a frame (but not if the controller is in document within that frame)\r\n // We used to reference a KMW state variable `this.keyman._MasterDocument`, but it was only ever set to `window.document`.\r\n if(Lobj && Lobj.ownerDocument && (Pobj.ownerDocument != window.document)) {\r\n var Ldoc=Lobj.ownerDocument; // I2404 - Support for IFRAMEs\r\n\r\n if(Ldoc && Ldoc.defaultView && Ldoc.defaultView.frameElement) {\r\n return Lcurtop + getAbsoluteY(Ldoc.defaultView.frameElement);\r\n }\r\n }\r\n return Lcurtop;\r\n}", + "import { VariableStore, VariableStoreSerializer } from 'keyman/engine/js-processor';\r\nimport { CookieSerializer } from \"keyman/engine/dom-utils\";\r\n\r\n// While there's little reason we couldn't store all of a keyboard's store values within\r\n// the same cookie... that's not what we had implemented in the last pre-es-module version\r\n// of KeymanWeb. We're keeping this transformation _very_ straightforward.\r\n//\r\n// Also of note: there's nothing we can do to allow TS to provide type-checking of\r\n// dynamic property names; they'd have to be known at compile time to facilitate\r\n// strict type checking.\r\nclass VarStoreSerializer extends CookieSerializer {\r\n constructor(keyboardID: string, storeName: string) {\r\n super(`KeymanWeb_${keyboardID}_Option_${storeName}`);\r\n }\r\n\r\n load() {\r\n return super.load(decodeURIComponent);\r\n }\r\n\r\n save(storeMap: VariableStore) {\r\n super.save(storeMap, encodeURIComponent);\r\n }\r\n}\r\n\r\nexport class VariableStoreCookieSerializer implements VariableStoreSerializer {\r\n loadStore(keyboardID: string, storeName: string): VariableStore {\r\n const storeCookieSerializer = new VarStoreSerializer(keyboardID, storeName);\r\n return storeCookieSerializer.load();\r\n }\r\n\r\n saveStore(keyboardID: string, storeName: string, storeMap: VariableStore) {\r\n const storeCookieSerializer = new VarStoreSerializer(keyboardID, storeName);\r\n storeCookieSerializer.save(storeMap);\r\n }\r\n}", + "import { KeyboardObject } from \"keyman/engine/keyboard\";\r\nimport { KeyboardInterface as KeyboardInterfaceBase } from 'keyman/engine/js-processor';\r\nimport { KeyboardStub, RawKeyboardStub, toUnprefixedKeyboardId as unprefixed } from 'keyman/engine/keyboard-storage';\r\n\r\nimport { ContextManagerBase } from './contextManagerBase.js';\r\nimport { VariableStoreCookieSerializer } from \"./variableStoreCookieSerializer.js\";\r\nimport KeymanEngine from \"./keymanEngine.js\";\r\nimport { EngineConfiguration } from \"./engineConfiguration.js\";\r\n\r\nexport default class KeyboardInterface> extends KeyboardInterfaceBase {\r\n protected readonly engine: KeymanEngine;\r\n private stubNamespacer?: (stub: RawKeyboardStub) => void;\r\n\r\n constructor(\r\n _jsGlobal: any,\r\n engine: KeymanEngine,\r\n stubNamespacer?: (stub: RawKeyboardStub) => void\r\n ) {\r\n super(_jsGlobal, engine, new VariableStoreCookieSerializer());\r\n this.engine = engine;\r\n this.stubNamespacer = stubNamespacer;\r\n }\r\n\r\n // Preserves a keyboard's ID, even if namespaced, via script tag tagging.\r\n preserveID(Pk: any /** a `Keyboard`'s `scriptObject` entry */) {\r\n var trueID;\r\n\r\n // Find the currently-executing script tag; KR is called directly from each keyboard's definition script.\r\n if(document.currentScript) {\r\n trueID = document.currentScript.id;\r\n } else {\r\n var scripts = document.getElementsByTagName('script');\r\n var currentScript = scripts[scripts.length-1];\r\n\r\n trueID = currentScript.id;\r\n }\r\n\r\n // Final check that the script tag is valid and appropriate for the loading keyboard.\r\n if(!trueID) {\r\n return;\r\n } else if(trueID.indexOf(unprefixed(Pk['KI'])) != -1) {\r\n Pk['KI'] = trueID; // Take the script's version of the ID, which may include package namespacing.\r\n } else {\r\n console.error(\"Error when registering keyboard: current SCRIPT tag's ID does not match!\");\r\n }\r\n }\r\n\r\n registerKeyboard(Pk: KeyboardObject): void {\r\n // Among other things, sets Pk as a newly-active Keyboard.\r\n super.registerKeyboard(Pk);\r\n const registeredKeyboard = this.loadedKeyboard;\r\n\r\n this.preserveID(Pk);\r\n\r\n this.engine.config.deferForInitialization.then(() => {\r\n if(!this.engine.keyboardRequisitioner.cache.isFetchingKeyboard(registeredKeyboard.id)) {\r\n // Deliberate keyboard pre-loading via direct script-tag link on the page.\r\n // Just load the keyboard and reset the harness's keyboard-receiver field.\r\n this.engine.keyboardRequisitioner.cache.addKeyboard(registeredKeyboard);\r\n this.loadedKeyboard = null;\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Add the basic keyboard parameters (keyboard stub) to the array of keyboard stubs\r\n * If no language code is specified in a keyboard it cannot be registered,\r\n * and a keyboard stub must be registered before the keyboard is loaded\r\n * for the keyboard to be usable.\r\n *\r\n * @param {Object} Pstub Keyboard stub object\r\n * @return {?number} 1 if already registered, else null\r\n */\r\n registerStub(Pstub: RawKeyboardStub): number {\r\n if(this.stubNamespacer) {\r\n this.stubNamespacer(Pstub);\r\n }\r\n\r\n // This is where app-hosted KeymanWeb receives pre-formed stubs.\r\n // They're specified in the \"internal\" format (KI, KN, KLC...)\r\n // (SHIFT-CTRL-F @ repo-level for the mobile apps: `setKeymanLanguage`)\r\n // Keyman Developer may also use this method directly for its test-host page.\r\n //\r\n // It may also be used by documented legacy API:\r\n // https://help.keyman.com/DEVELOPER/ENGINE/WEB/2.0/guide/examples/manual-control\r\n // (See: referenced laokeys_load.js)\r\n //\r\n // The mobile apps typically have fully-preconfigured paths, but Developer's\r\n // test-host page does not.\r\n\r\n const buildStub = () => {\r\n const pathConfig = this.engine.config.paths;\r\n return new KeyboardStub(Pstub, pathConfig.keyboards, pathConfig.fonts);\r\n };\r\n\r\n if(!this.engine.config.deferForInitialization.isResolved) {\r\n // pathConfig is not ready until KMW initializes, which prevents proper stub-building.\r\n this.engine.config.deferForInitialization.then(() => this.engine.keyboardRequisitioner.cache.addStub(buildStub()));\r\n } else {\r\n const stub = buildStub();\r\n\r\n if(this.engine.keyboardRequisitioner?.cache.findMatchingStub(stub)) {\r\n return 1;\r\n }\r\n this.engine.keyboardRequisitioner.cache.addStub(stub);\r\n }\r\n\r\n return null;\r\n }\r\n\r\n insertText = (Ptext: string, PdeadKey:number): void => {\r\n this.resetContextCache();\r\n // As this function isn't provided a handle to an active outputTarget, we rely on\r\n // the context manager to resolve said issue.\r\n this.engine.contextManager.insertText(this, Ptext, PdeadKey);\r\n }\r\n\r\n // Short-hand name: necessary to do it this way due to assignment style.\r\n KT = this.insertText;\r\n}\r\n\r\n(function() {\r\n KeyboardInterface.__publishShorthandAPI();\r\n}());", + "// Enables DOM types, but just for this one module.\r\n\r\n///\r\n\r\nimport { Keyboard, KeyboardHarness, KeyboardLoaderBase, KeyboardLoadErrorBuilder, MinimalKeymanGlobal } from 'keyman/engine/keyboard';\r\n\r\nimport { ManagedPromise } from '@keymanapp/web-utils';\r\n\r\nexport class DOMKeyboardLoader extends KeyboardLoaderBase {\r\n public readonly element: HTMLIFrameElement;\r\n private readonly performCacheBusting: boolean;\r\n\r\n constructor()\r\n constructor(harness: KeyboardHarness);\r\n constructor(harness: KeyboardHarness, cacheBust?: boolean)\r\n constructor(harness?: KeyboardHarness, cacheBust?: boolean) {\r\n if(harness && harness._jsGlobal != window) {\r\n // Copy the String typing over; preserve string extensions!\r\n harness._jsGlobal['String'] = window['String'];\r\n }\r\n\r\n if(!harness) {\r\n super(new KeyboardHarness(window, MinimalKeymanGlobal));\r\n } else {\r\n super(harness);\r\n }\r\n\r\n this.performCacheBusting = cacheBust || false;\r\n }\r\n\r\n protected loadKeyboardInternal(\r\n uri: string,\r\n errorBuilder: KeyboardLoadErrorBuilder,\r\n id?: string\r\n ): Promise {\r\n const promise = new ManagedPromise();\r\n\r\n if(this.performCacheBusting) {\r\n uri = this.cacheBust(uri);\r\n }\r\n\r\n try {\r\n const document = this.harness._jsGlobal.document;\r\n const script = document.createElement('script');\r\n if(id) {\r\n script.id = id;\r\n }\r\n document.head.appendChild(script);\r\n script.onerror = (err: any) => {\r\n promise.reject(errorBuilder.missingError(err));\r\n }\r\n script.onload = () => {\r\n if(this.harness.loadedKeyboard) {\r\n const keyboard = this.harness.loadedKeyboard;\r\n this.harness.loadedKeyboard = null;\r\n promise.resolve(keyboard);\r\n } else {\r\n promise.reject(errorBuilder.scriptError());\r\n }\r\n }\r\n\r\n // On the oldest mobile devices we support, Promise.finally may not actually exist.\r\n // Fortunately... it's not that hard of an issue to work around.\r\n // Note: es6-shim doesn't polyfill Promise.finally!\r\n promise.then(() => {\r\n // It is safe to remove the script once it has been run (https://stackoverflow.com/a/37393041)\r\n script.remove();\r\n }).catch(() => {\r\n script.remove();\r\n });\r\n\r\n // Now that EVERYTHING ELSE is ready, establish the link to the keyboard's script.\r\n script.src = uri;\r\n } catch (err) {\r\n return Promise.reject(err);\r\n }\r\n\r\n return promise.corePromise;\r\n }\r\n\r\n private cacheBust(uri: string) {\r\n // Our WebView version directly sets the keyboard path, and it may replace the file\r\n // after KMW has loaded. We need cache-busting to prevent the new version from\r\n // being ignored.\r\n return uri + \"?v=\" + (new Date()).getTime(); /*cache buster*/\r\n }\r\n}", + "import { CasingForm, Configuration, Context } from '@keymanapp/common-types';\r\nimport { Mock } from \"keyman/engine/js-processor\";\r\n\r\nexport default class ContextWindow implements Context {\r\n // Used to limit the range of context replicated for use of keyboard rules within\r\n // the engine, as used for fat-finger prep / `Alternate` generation.\r\n public static readonly ENGINE_RULE_WINDOW: Configuration = {\r\n leftContextCodePoints: 64,\r\n rightContextCodePoints: 32\r\n };\r\n\r\n left: string;\r\n right?: string;\r\n\r\n startOfBuffer: boolean;\r\n endOfBuffer: boolean;\r\n\r\n casingForm?: CasingForm;\r\n\r\n constructor(mock: Mock, config: Configuration, layerId: string) {\r\n this.left = mock.getTextBeforeCaret();\r\n this.startOfBuffer = this.left._kmwLength() <= config.leftContextCodePoints;\r\n if(!this.startOfBuffer) {\r\n // Our custom substring version will return the last n characters if param #1 is given -n.\r\n this.left = this.left._kmwSubstr(-config.leftContextCodePoints);\r\n }\r\n\r\n this.right = mock.getTextAfterCaret();\r\n this.endOfBuffer = this.right._kmwLength() <= config.rightContextCodePoints;\r\n if(!this.endOfBuffer) {\r\n this.right = this.right._kmwSubstr(0, config.rightContextCodePoints);\r\n }\r\n\r\n this.casingForm =\r\n layerId == 'shift' ? 'initial' :\r\n layerId == 'caps' ? 'upper' :\r\n null;\r\n }\r\n\r\n public toMock(): Mock {\r\n let caretPos = this.left._kmwLength();\r\n\r\n return new Mock(this.left + (this.right || \"\"), caretPos);\r\n }\r\n}", + "/*\r\n * Copyright (c) 2018 National Research Council Canada (author: Eddie A. Santos)\r\n * Copyright (c) 2018 SIL International\r\n *\r\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\r\n * this software and associated documentation files (the \"Software\"), to deal in\r\n * the Software without restriction, including without limitation the rights to\r\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\r\n * the Software, and to permit persons to whom the Software is furnished to do so,\r\n * subject to the following conditions:\r\n *\r\n * The above copyright notice and this permission notice shall be included in all\r\n * copies or substantial portions of the Software.\r\n *\r\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\r\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\r\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\r\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\r\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\r\n */\r\n\r\n/// \r\n\r\nimport { Token } from '@keymanapp/lm-message-types';\r\n\r\ntype Resolve = (value?: T | PromiseLike) => void;\r\ntype Reject = (reason?: any) => void;\r\ninterface PromiseCallbacks {\r\n resolve: Resolve;\r\n reject: Reject;\r\n}\r\n\r\n\r\n/**\r\n * Associate tokens with promises.\r\n *\r\n * First, .make() a promise -- associate a token with resolve/reject callbacks.\r\n *\r\n * You can either .keep() a promise -- resolve() and forget it;\r\n * Or you may also .break() a promise -- reject() and forget it.\r\n *\r\n * is the type of resolved value (value yielded successfully by promise).\r\n */\r\nexport default class PromiseStore {\r\n // IE11 offers partial support for new Map().\r\n // Assume only .get(), .set(), .has(), .delete(), and .size work.\r\n // See: http://kangax.github.io/compat-table/es6/#test-Map\r\n private _promises: Map>;\r\n constructor() {\r\n this._promises = new Map();\r\n }\r\n /**\r\n * How many promises are currently being tracked?\r\n */\r\n get length(): number {\r\n return this._promises.size;\r\n }\r\n /**\r\n * Associate a token with its respective resolve and reject callbacks.\r\n */\r\n make(token: Token, resolve: Resolve, reject: Reject): void {\r\n if (this._promises.has(token)) {\r\n return reject(`Existing request with token ${token}`);\r\n }\r\n this._promises.set(token, { reject, resolve });\r\n }\r\n /**\r\n * Resolve the promise associated with a token (with a value!).\r\n * Once the promise is resolved, the token is removed..\r\n */\r\n keep(token: Token, value: T) {\r\n let callbacks = this._promises.get(token);\r\n if (!callbacks) {\r\n throw new Error(`No promise associated with token: ${token}`);\r\n }\r\n let accept = callbacks.resolve;\r\n this._promises.delete(token);\r\n return accept(value);\r\n }\r\n /**\r\n * Instantly reject and forget a promise associated with the token.\r\n */\r\n break(token: Token, reason?: any): void {\r\n let callbacks = this._promises.get(token);\r\n if (!callbacks) {\r\n throw new Error(`No promise associated with token: ${token}`);\r\n }\r\n this._promises.delete(token);\r\n callbacks.reject(reason);\r\n }\r\n}", + "/*\r\n * Copyright (c) 2018 National Research Council Canada (author: Eddie A. Santos)\r\n * Copyright (c) 2018 SIL International\r\n *\r\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\r\n * this software and associated documentation files (the \"Software\"), to deal in\r\n * the Software without restriction, including without limitation the rights to\r\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\r\n * the Software, and to permit persons to whom the Software is furnished to do so,\r\n * subject to the following conditions:\r\n *\r\n * The above copyright notice and this permission notice shall be included in all\r\n * copies or substantial portions of the Software.\r\n *\r\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\r\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\r\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\r\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\r\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\r\n */\r\n\r\nimport { Capabilities, Configuration, Context, Distribution, Reversion, Suggestion, Transform, USVString } from '@keymanapp/common-types';\r\nimport PromiseStore from \"./promise-store.js\";\r\nimport { OutgoingMessage } from '@keymanapp/lm-message-types';\r\n\r\n/// \r\n\r\n/**\r\n * Top-level interface to the Language Modelling layer, or \"LMLayer\" for short.\r\n *\r\n * The Language Modelling layer provides a way for keyboards to offer prediction and\r\n * correction functionalities. The LMLayer proper runs within a Web Worker, however,\r\n * this class is intended to run in the main thread, and automatically spawn a Web\r\n * Worker, capable of offering predictions.\r\n *\r\n * Since the Worker runs in a different thread, the public methods of this class are\r\n * asynchronous. Methods of note include:\r\n *\r\n * - #loadModel() -- loads a specified model file\r\n * - #predict() -- ask the LMLayer to offer suggestions (predictions or corrections) for\r\n * the input event\r\n * - #unloadModel() -- unloads the LMLayer's currently loaded model, preparing it to\r\n * receive (load) a new model\r\n *\r\n * The top-level LMLayer will automatically starts up its own Web Worker.\r\n */\r\n\r\nexport default class LMLayer {\r\n /**\r\n * The underlying worker instance. By default, this is the LMLayerWorker.\r\n */\r\n private _worker: Worker;\r\n /** Call this when the LMLayer has sent us the 'ready' message! */\r\n private _declareLMLayerReady: (conf: Configuration) => void;\r\n private _predictPromises: PromiseStore;\r\n private _wordbreakPromises: PromiseStore;\r\n private _acceptPromises: PromiseStore;\r\n private _revertPromises: PromiseStore;\r\n private _nextToken: number;\r\n // @ts-ignore // currently unused & unreferenced.\r\n private capabilities: Capabilities;\r\n\r\n /**\r\n * Construct the top-level LMLayer interface. This also starts the underlying Worker.\r\n *\r\n * @param uri URI of the underlying LMLayer worker code. This will usually be a blob:\r\n * or file: URI. If uri is not provided, this will start the default Worker.\r\n */\r\n constructor(capabilities: Capabilities, worker: Worker, testMode?: boolean) {\r\n // Either use the given worker, or instantiate the default worker.\r\n this._worker = worker;\r\n this._worker.onmessage = this.onMessage.bind(this)\r\n this._declareLMLayerReady = null;\r\n this._predictPromises = new PromiseStore();\r\n this._wordbreakPromises = new PromiseStore();\r\n this._acceptPromises = new PromiseStore();\r\n this._revertPromises = new PromiseStore();\r\n this._nextToken = Number.MIN_SAFE_INTEGER;\r\n\r\n this.sendConfig(capabilities, !!testMode);\r\n }\r\n\r\n /**\r\n * Initializes the LMLayer worker with the host platform's capability set.\r\n *\r\n * @param capabilities The host platform's capability spec - a model cannot assume access to more context\r\n * than specified by this parameter.\r\n */\r\n private sendConfig(capabilities: Capabilities, testMode: boolean) {\r\n this._worker.postMessage({\r\n message: 'config',\r\n capabilities: capabilities,\r\n testMode: testMode\r\n });\r\n }\r\n\r\n /**\r\n * Initializes the LMLayer worker with a path to the desired model file.\r\n */\r\n loadModel(modelSource: string, loadType: 'file' | 'raw' = 'file'): Promise {\r\n return new Promise((resolve, _reject) => {\r\n // Sets up so the promise is resolved in the onMessage() callback, when it receives\r\n // the 'ready' message.\r\n this._declareLMLayerReady = resolve;\r\n\r\n let modelSourceSpec: any = {\r\n type: loadType\r\n };\r\n\r\n if(loadType == 'file') {\r\n modelSourceSpec.file = modelSource;\r\n } else {\r\n modelSourceSpec.code = modelSource;\r\n }\r\n\r\n this._worker.postMessage({\r\n message: 'load',\r\n source: modelSourceSpec\r\n });\r\n });\r\n }\r\n\r\n /**\r\n * Unloads the previously-active model from memory, resetting the LMLayer to prep\r\n * for transition to use of a new model.\r\n */\r\n public unloadModel() {\r\n this._worker.postMessage({\r\n message: 'unload'\r\n });\r\n }\r\n\r\n predict(transform: Transform | Distribution, context: Context): Promise {\r\n let token = this._nextToken++;\r\n return new Promise((resolve, reject) => {\r\n this._predictPromises.make(token, resolve, reject);\r\n this._worker.postMessage({\r\n message: 'predict',\r\n token: token,\r\n transform: transform,\r\n context: context,\r\n });\r\n });\r\n }\r\n\r\n wordbreak(context: Context): Promise {\r\n let token = this._nextToken++;\r\n return new Promise((resolve, reject) => {\r\n this._wordbreakPromises.make(token, resolve, reject);\r\n this._worker.postMessage({\r\n message: 'wordbreak',\r\n token: token,\r\n context: context\r\n })\r\n });\r\n }\r\n\r\n acceptSuggestion(suggestion: Suggestion, context: Context, postTransform: Transform): Promise {\r\n let token = this._nextToken++;\r\n return new Promise((resolve, reject) => {\r\n this._acceptPromises.make(token, resolve, reject);\r\n this._worker.postMessage({\r\n message: 'accept',\r\n token: token,\r\n suggestion: suggestion,\r\n context: context,\r\n postTransform: postTransform\r\n });\r\n });\r\n }\r\n\r\n revertSuggestion(reversion: Reversion, context: Context): Promise {\r\n let token = this._nextToken++;\r\n return new Promise((resolve, reject) => {\r\n this._revertPromises.make(token, resolve, reject);\r\n this._worker.postMessage({\r\n message: 'revert',\r\n token: token,\r\n reversion: reversion,\r\n context: context\r\n })\r\n });\r\n }\r\n\r\n resetContext(context: Context) {\r\n this._worker.postMessage({\r\n message: 'reset-context',\r\n context: context\r\n });\r\n }\r\n\r\n // TODO: asynchronous close() method.\r\n // Worker code must recognize message and call self.close().\r\n\r\n private onMessage(event: MessageEvent): void {\r\n let payload: OutgoingMessage = event.data;\r\n if (payload.message === 'error') {\r\n console.error(payload.log);\r\n if(payload.error) {\r\n console.error(payload.error);\r\n }\r\n }\r\n else if (payload.message === 'ready') {\r\n this._declareLMLayerReady(event.data.configuration);\r\n } else if (payload.message === 'suggestions') {\r\n this._predictPromises.keep(payload.token, payload.suggestions);\r\n } else if (payload.message === 'currentword') {\r\n this._wordbreakPromises.keep(payload.token, payload.word);\r\n } else if (payload.message === 'postaccept') {\r\n this._acceptPromises.keep(payload.token, payload.reversion);\r\n } else if (payload.message === 'postrevert') {\r\n this._revertPromises.keep(payload.token, payload.suggestions);\r\n } else {\r\n // This branch should never execute, but just in case...\r\n //@ts-ignore\r\n throw new Error(`Message not implemented: ${payload.message}`);\r\n }\r\n }\r\n\r\n /**\r\n * Clears out any computational resources in use by the LMLayer, including shutting\r\n * down any internal WebWorkers.\r\n */\r\n public shutdown() {\r\n this._worker.terminate();\r\n }\r\n}\r\n", + "/**\r\n * Given a function, this utility returns the source code within it, as a string.\r\n * This is intended to unwrap the \"wrapped\" source code created in the LMLayerWorker\r\n * build process.\r\n *\r\n * @param fn The function whose body will be returned.\r\n */\r\nexport default function unwrap(encodedSrc: string): string {\r\n // There used to be more to this, but now it's a pretty simple passthrough!\r\n return encodedSrc;\r\n}", + "\n// Autogenerated code. Do not modify!\n// --START:LMLayerWorkerCode--\n\nexport var LMLayerWorkerCode = \"\\\"use strict\\\";(()=>{var Oe=Object.defineProperty,Kt=Object.defineProperties;var Qt=Object.getOwnPropertyDescriptors;var ft=Object.getOwnPropertySymbols;var Ht=Object.prototype.hasOwnProperty,Vt=Object.prototype.propertyIsEnumerable;var gt=(o,e)=>{if(e=Symbol[o])return e;throw Error(\\\"Symbol.\\\"+o+\\\" is not defined\\\")};var Tt=(o,e,t)=>e in o?Oe(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t,A=(o,e)=>{for(var t in e||(e={}))Ht.call(e,t)&&Tt(o,t,e[t]);if(ft)for(var t of ft(e))Vt.call(e,t)&&Tt(o,t,e[t]);return o},Re=(o,e)=>Kt(o,Qt(e)),h=(o,e)=>Oe(o,\\\"name\\\",{value:e,configurable:!0});var _e=(o,e)=>{for(var t in e)Oe(o,t,{get:e[t],enumerable:!0})};var D=(o,e,t)=>new Promise((r,n)=>{var i=a=>{try{l(t.next(a))}catch(u){n(u)}},s=a=>{try{l(t.throw(a))}catch(u){n(u)}},l=a=>a.done?r(a.value):Promise.resolve(a.value).then(i,s);l((t=t.apply(o,e)).next())}),be=function(o,e){this[0]=o,this[1]=e},Ct=(o,e,t)=>{var r=(s,l,a,u)=>{try{var c=t[s](l),p=(l=c.value)instanceof be,f=c.done;Promise.resolve(p?l[0]:l).then(d=>p?r(s===\\\"return\\\"?s:\\\"next\\\",l[1]?{done:d.done,value:d.value}:d,a,u):a({value:d,done:f})).catch(d=>r(\\\"throw\\\",d,a,u))}catch(d){u(d)}},n=s=>i[s]=l=>new Promise((a,u)=>r(s,l,a,u)),i={};return t=t.apply(o,e),i[Symbol.asyncIterator]=()=>i,n(\\\"next\\\"),n(\\\"throw\\\"),n(\\\"return\\\"),i};var St=(o,e,t)=>(e=o[gt(\\\"asyncIterator\\\")])?e.call(o):(o=o[gt(\\\"iterator\\\")](),e={},t=(r,n)=>(n=o[r])&&(e[r]=i=>new Promise((s,l,a)=>(i=n.call(o,i),a=i.done,Promise.resolve(i.value).then(u=>s({value:u,done:a}),l)))),t(\\\"next\\\"),t(\\\"return\\\"),e);function O(){String.kmwFromCharCode=function(o){var e=[],t;for(t=0;t1114111||Math.floor(r)!==r)throw new RangeError(\\\"Invalid code point \\\"+r);r<65536?e.push(r):(r-=65536,e.push((r>>10)+55296),e.push(r%1024+56320))}return String.fromCharCode.apply(void 0,e)},String.prototype.kmwCharCodeAt=function(o){var e=String(this),t=0;if(o<0||o>=e.length)return NaN;for(var r=0;r=55296&&n<=56319&&e.length>t+1){var i=e.charCodeAt(t+1);if(i>=56320&&i<=57343)return(n-55296<<10)+(i-56320)+65536}return n},String.prototype.kmwIndexOf=function(o,e){var t=String(this),r=t.indexOf(o,e);if(r<0)return r;for(var n=0,i=0;i!==null&&ie){var i=o;o=e,e=i}r=t.kmwCodePointToCodeUnit(o),n=t.kmwCodePointToCodeUnit(e)}return(isNaN(r)||r===null)&&(r=0),(isNaN(n)||n===null)&&(n=t.length),t.substring(r,n)},String.prototype.kmwNextChar=function(o){var e=String(this);if(o===null||o<0||o>=e.length-1)return null;var t=e.charCodeAt(o);if(t>=55296&&t<=56319&&e.length>o+1){var r=e.charCodeAt(o+1);if(r>=56320&&r<=57343)return o==e.length-2?null:o+2}return o+1},String.prototype.kmwPrevChar=function(o){var e=String(this);if(o==null||o<=0||o>e.length)return null;var t=e.charCodeAt(o-1);if(t>=56320&&t<=57343&&o>1){var r=e.charCodeAt(o-2);if(r>=55296&&r<=56319)return o-2}return o-1},String.prototype.kmwCodePointToCodeUnit=function(o){if(o===null)return null;var e=String(this),t=0;if(o<0){t=e.length;for(var r=0;r>o;r--)t=e.kmwPrevChar(t);return t}if(o==e.kmwLength())return e.length;for(var r=0;r=0?e.kmwSubstr(o,1):\\\"\\\"},String.prototype.kmwBMPNextChar=function(o){var e=String(this);return o<0||o>=e.length-1?null:o+1},String.prototype.kmwBMPPrevChar=function(o){var e=String(this);return o<=0||o>e.length?null:o-1},String.prototype.kmwBMPCodePointToCodeUnit=function(o){return o},String.prototype.kmwBMPCodeUnitToCodePoint=function(o){return o},String.prototype.kmwBMPLength=function(){var o=String(this);return o.length},String.prototype.kmwBMPSubstr=function(o,e){var t=String(this);return o>-1?t.substr(o,e):t.substr(t.length+o,-o)},String.kmwEnableSupplementaryPlane=function(o){var e=String.prototype;String._kmwFromCharCode=o?String.kmwFromCharCode:String.fromCharCode,e._kmwCharAt=o?e.kmwCharAt:e.charAt,e._kmwCharCodeAt=o?e.kmwCharCodeAt:e.charCodeAt,e._kmwIndexOf=o?e.kmwIndexOf:e.indexOf,e._kmwLastIndexOf=o?e.kmwLastIndexOf:e.lastIndexOf,e._kmwSlice=o?e.kmwSlice:e.slice,e._kmwSubstring=o?e.kmwSubstring:e.substring,e._kmwSubstr=o?e.kmwSubstr:e.kmwBMPSubstr,e._kmwLength=o?e.kmwLength:e.kmwBMPLength,e._kmwNextChar=o?e.kmwNextChar:e.kmwBMPNextChar,e._kmwPrevChar=o?e.kmwPrevChar:e.kmwBMPPrevChar,e._kmwCodePointToCodeUnit=o?e.kmwCodePointToCodeUnit:e.kmwBMPCodePointToCodeUnit,e._kmwCodeUnitToCodePoint=o?e.kmwCodeUnitToCodePoint:e.kmwBMPCodeUnitToCodePoint},String._kmwFromCharCode||String.kmwEnableSupplementaryPlane(!1)}h(O,\\\"extendString\\\");O();var De=class De{constructor(e){this._isFulfilled=!1;this._isRejected=!1;this._promise=new Promise((t,r)=>{this._resolve=n=>{this._isFulfilled=!0,t(n)},this._reject=n=>{this._isRejected=!0,r(n)},e&&e(this._resolve,this._reject)})}get resolve(){return this._resolve}get reject(){return this._reject}get isFulfilled(){return this._isFulfilled}get isRejected(){return this._isRejected}get isResolved(){return this.isFulfilled||this.isRejected}then(e,t){return this._promise.then(e,t)}catch(e){return this._promise.catch(e)}finally(e){return this._promise.finally(e)}get corePromise(){return this._promise}};h(De,\\\"ManagedPromise\\\");var ne=De;var Fe=class Fe extends ne{constructor(t){let r=null;super(s=>{r=setTimeout(()=>{this.isResolved||s(!0)},t)});this.timerHandle=r;let n=this._resolve;this._resolve=s=>{clearTimeout(this.timerHandle),n(s)};let i=this._reject;this._reject=s=>{clearTimeout(this.timerHandle),i(s)}}};h(Fe,\\\"TimeoutPromise\\\");var ie=Fe,Ue=h(o=>new ie(o).corePromise,\\\"timedPromise\\\");var I=class I{constructor(e,t){if(typeof e!=\\\"function\\\"){this.comparator=e.comparator,this.heap=[].concat(e.heap);return}let r=e;this.comparator=r,this.heap=(t!=null?t:[]).slice(0),this.heapify()}static leftChildIndex(e){return e*2+1}static rightChildIndex(e){return e*2+2}static parentIndex(e){return Math.floor((e-1)/2)}heapify(e,t){if(e==null||t==null){this.heapify(0,this.count-1);return}let r=[],n=-1;for(let i=t;i>=e;i--){let s=I.parentIndex(i);this.siftDown(i)&&s0;){let i=r.shift(),s=I.parentIndex(i);this.siftDown(i)&&s>=0&&n!=s&&(r.push(s),n=s)}}get count(){return this.heap.length}peek(){return this.heap[0]}enqueue(e){let t=this.heap.length;this.heap.push(e);let r=I.parentIndex,n=r(t);for(;t!==0&&this.comparator(this.heap[t],this.heap[n])<0;){let i=this.heap[t];this.heap[t]=this.heap[n],this.heap[n]=i,t=n,n=r(t)}}enqueueAll(e){if(e.length==0)return;let t=this.count;this.heap=this.heap.concat(e);let r=I.parentIndex(t);this.heapify(r>=0?r:0,I.parentIndex(this.count-1))}dequeue(){if(this.count==0)return;let e=this.heap[0],t=this.heap.pop();return this.heap.length>0&&(this.heap[0]=t,this.siftDown(0)),e}siftDown(e){let t=I.leftChildIndex(e),r=I.rightChildIndex(e),n=e;if(tEt,QuoteBehavior:()=>j,SENTINEL_CODE_UNIT:()=>N,TrieModel:()=>ue,applyTransform:()=>g,buildMergedTransform:()=>U,defaultApplyCasing:()=>we,getLastPreCaretToken:()=>ae,isHighSurrogate:()=>F,isLowSurrogate:()=>We,isSentinel:()=>ke,tokenize:()=>se,transformToSuggestion:()=>W,wordbreak:()=>X});O();var N=\\\"﷐\\\";function g(o,e){var c,p;let t=e.left||\\\"\\\",r=t.kmwLength(),n=r=55296&&o<=56319}h(F,\\\"isHighSurrogate\\\");function We(o){return typeof o==\\\"string\\\"&&(o=o.charCodeAt(0)),o>=56320&&o<=57343}h(We,\\\"isLowSurrogate\\\");function ke(o){return o==N}h(ke,\\\"isSentinel\\\");function W(o,e){let t={transform:o,displayAs:o.insert};return o.id!==void 0&&(t.transformId=o.id),(e===0||e)&&(t.p=e),t}h(W,\\\"transformToSuggestion\\\");function we(o,e){switch(o){case\\\"lower\\\":return e.toLowerCase();case\\\"upper\\\":return e.toUpperCase();case\\\"initial\\\":let t=1;return e.length>1&&F(e.charAt(0))&&We(e.charCodeAt(1))&&(t=2),e.substring(0,t).toUpperCase().concat(e.substring(t))}}h(we,\\\"defaultApplyCasing\\\");var oe=(r=>(r.noQuotes=\\\"no-quotes\\\",r.useQuotes=\\\"use-quotes\\\",r.default=\\\"default-quotes\\\",r))(oe||{});(e=>{function o(t,r,n,i){if(i==\\\"default-quotes\\\"||!i)throw\\\"Specified quote behavior may be ambiguous - default behavior not specified (may not be .default)\\\";switch(t==\\\"default-quotes\\\"&&(t=i),t){case\\\"no-quotes\\\":return r;case\\\"use-quotes\\\":let{open:s,close:l}=n.quotesForKeepSuggestion;return s+r+l;default:throw\\\"Unsupported quote behavior state detected; implementation missing!\\\"}}e.apply=o,h(o,\\\"apply\\\")})(oe||(oe={}));var j=oe;function se(o,e,t){let r=(t==null?void 0:t.rejoins)||[\\\"'\\\"];e=e||{left:void 0,startOfBuffer:void 0,endOfBuffer:void 0};let n=o(e.left||\\\"\\\")||[],i=o(e.right||\\\"\\\")||[],s={left:[],right:[],caretSplitsToken:!1},l=0;for(;n.length>0;){let c=n[0];if(Math.max(c.start,l)!=l){let p=Math.max(l,c.start);s.left.push({text:e.left.substring(l,p),isWhitespace:!0}),l=p}else n.shift(),s.left.push({text:c.text}),l=Math.max(l,c.end)}if(e.left!=null&&l!=e.left.length){let c=Math.max(l,e.left.length);s.left.push({text:e.left.substring(l,c),isWhitespace:!0}),l=c}let a=s.left.length;if(a>1){let c=s.left[a-2],p=s.left[a-1];!c.isWhitespace&&!p.isWhitespace&&r.indexOf(p.text)!=-1&&(s.left.pop(),s.left.pop(),s.left.push({text:c.text+p.text}),a--)}l=0;let u=!0;for(;i.length>0;){let c=i[0];if(Math.max(c.start,l)!=l){let p=Math.max(l,c.start);s.right.push({text:e.right.substring(l,p),isWhitespace:!0}),l=p}else{let p=s.left[a-1];p&&u&&!p.isWhitespace&&o(p.text+c.text).length==1&&(s.caretSplitsToken=!0),i.shift(),s.right.push({text:c.text}),l=Math.max(l,c.end)}u=!1}if(e.right&&l!=e.right.length){let c=Math.max(l,e.right.length);s.right.push({text:e.right.substring(l,c),isWhitespace:!0}),l=c}return s}h(se,\\\"tokenize\\\");function ae(o,e){let t=se(o,e);if(t.left.length>0){let r=t.left.pop();return r.isWhitespace?\\\"\\\":r.text}return\\\"\\\"}h(ae,\\\"getLastPreCaretToken\\\");function X(o,e){return ae(o,e)}h(X,\\\"wordbreak\\\");var le={};_e(le,{ascii:()=>Ke,default:()=>R,defaultWordbreaker:()=>R,placeholder:()=>qe});function qe(o){let e=0;return o.split(/\\\\s+/).map(t=>{let r={start:e,end:e+t.length,text:t,length:t.length};return e=r.end,r})}h(qe,\\\"placeholder\\\");function Ke(o){let e=/[A-Za-z0-9']+/g,t=[],r;for(;(r=e.exec(o))!==null;)t.push(new Be(r[0],r.index));return t}h(Ke,\\\"ascii\\\");var Qe=class Qe{constructor(e,t){this.text=e,this.start=t}get length(){return this.text.length}get end(){return this.start+this.text.length}};h(Qe,\\\"RegExpDerivedSpan\\\");var Be=Qe;var xt=[\\\"Other\\\",\\\"LF\\\",\\\"Newline\\\",\\\"CR\\\",\\\"WSegSpace\\\",\\\"Double_Quote\\\",\\\"Single_Quote\\\",\\\"MidNum\\\",\\\"MidNumLet\\\",\\\"Numeric\\\",\\\"MidLetter\\\",\\\"ALetter\\\",\\\"ExtendNumLet\\\",\\\"Format\\\",\\\"Extend\\\",\\\"Hebrew_Letter\\\",\\\"ZWJ\\\",\\\"Katakana\\\",\\\"Regional_Indicator\\\",\\\"sot\\\",\\\"eot\\\"],bt=`\\\\0 \\n!\\\\v\\\"\\n#\\u000e $! \\\"%# '&( ,'- .(/ 0):*;'< A+[ _,\\\\` a+{ …\\\"† ª+« ­-® µ+¶ ·*¸ º+» À+× Ø+÷ ø+˘ ˞+̀.Ͱ+͵ Ͷ+͸ ͺ+;'Ϳ+΀ Ά+·*Έ+΋ Ό+΍ Ύ+΢ Σ+϶ Ϸ+҂ ҃.Ҋ+԰ Ա+՗ ՙ+՝ ՞+՟*ՠ+։'֊+֋ ֑.־ ֿ.׀ ׁ.׃ ׄ.׆ ׇ.׈ א/׫ ׯ/׳+״*׵ ؀)؆ ،'؎ ؐ.؛ ؜-؝ ؠ+ً.٠)٪ ٫)٬'٭ ٮ+ٰ.ٱ+۔ ە+ۖ.۝)۞ ۟.ۥ+ۧ.۩ ۪.ۮ+۰)ۺ+۽ ۿ+܀ ܏+ܑ.ܒ+ܰ.݋ ݍ+ަ.ޱ+޲ ߀)ߊ+߫.ߴ+߶ ߸'߹ ߺ+߻ ߽.߾ ࠀ+ࠖ.ࠚ+ࠛ.ࠤ+ࠥ.ࠨ+ࠩ.࠮ ࡀ+࡙.࡜ ࡠ+࡫ ࡰ+࢈ ࢉ+࢏ ࢐)࢒ ࢗ.ࢠ+࣊.࣢)ࣣ.ऄ+ऺ.ऽ+ा.ॐ+॑.क़+ॢ.। ०)॰ ॱ+ঁ.঄ অ+঍ এ+঑ ও+঩ প+঱ ল+঳ শ+঺ ়.ঽ+া.৅ ে.৉ ো.ৎ+৏ ৗ.৘ ড়+৞ য়+ৢ.৤ ০)ৰ+৲ ৼ+৽ ৾.৿ ਁ.਄ ਅ+਋ ਏ+਑ ਓ+਩ ਪ+਱ ਲ+਴ ਵ+਷ ਸ+਺ ਼.਽ ਾ.੃ ੇ.੉ ੋ.੎ ੑ.੒ ਖ਼+੝ ਫ਼+੟ ੦)ੰ.ੲ+ੵ.੶ ઁ.઄ અ+઎ એ+઒ ઓ+઩ પ+઱ લ+઴ વ+઺ ઼.ઽ+ા.૆ ે.૊ ો.૎ ૐ+૑ ૠ+ૢ.૤ ૦)૰ ૹ+ૺ.଀ ଁ.଄ ଅ+଍ ଏ+଑ ଓ+଩ ପ+଱ ଲ+଴ ଵ+଺ ଼.ଽ+ା.୅ େ.୉ ୋ.୎ ୕.୘ ଡ଼+୞ ୟ+ୢ.୤ ୦)୰ ୱ+୲ ஂ.ஃ+஄ அ+஋ எ+஑ ஒ+஖ ங+஛ ஜ+஝ ஞ+஠ ண+஥ ந+஫ ம+஺ ா.௃ ெ.௉ ொ.௎ ௐ+௑ ௗ.௘ ௦)௰ ఀ.అ+఍ ఎ+఑ ఒ+఩ ప+఺ ఼.ఽ+ా.౅ ె.౉ ొ.౎ ౕ.౗ ౘ+౛ ౝ+౞ ౠ+ౢ.౤ ౦)౰ ಀ+ಁ.಄ ಅ+಍ ಎ+಑ ಒ+಩ ಪ+಴ ವ+಺ ಼.ಽ+ಾ.೅ ೆ.೉ ೊ.೎ ೕ.೗ ೝ+೟ ೠ+ೢ.೤ ೦)೰ ೱ+ೳ.೴ ഀ.ഄ+഍ എ+഑ ഒ+഻.ഽ+ാ.൅ െ.൉ ൊ.ൎ+൏ ൔ+ൗ.൘ ൟ+ൢ.൤ ൦)൰ ൺ+඀ ඁ.඄ අ+඗ ක+඲ ඳ+඼ ල+඾ ව+෇ ්.෋ ා.෕ ූ.෗ ෘ.෠ ෦)෰ ෲ.෴ ั.า ิ.฻ ็.๏ ๐)๚ ັ.າ ິ.ຽ ່.໏ ໐)໚ ༀ+༁ ༘.༚ ༠)༪ ༵.༶ ༷.༸ ༹.༺ ༾.ཀ+཈ ཉ+཭ ཱ.྅ ྆.ྈ+ྍ.྘ ྙ.྽ ࿆.࿇ ါ.ဿ ၀)၊ ၖ.ၚ ၞ.ၡ ၢ.ၥ ၧ.ၮ ၱ.ၵ ႂ.ႎ ႏ.႐)ႚ.႞ Ⴀ+჆ Ⴧ+჈ Ⴭ+჎ ა+჻ ჼ+቉ ቊ+቎ ቐ+቗ ቘ+቙ ቚ+቞ በ+኉ ኊ+኎ ነ+኱ ኲ+኶ ኸ+኿ ዀ+዁ ዂ+዆ ወ+዗ ዘ+጑ ጒ+጖ ጘ+፛ ፝.፠ ᎀ+᎐ Ꭰ+᏶ ᏸ+᏾ ᐁ+᙭ ᙯ+ $ᚁ+᚛ ᚠ+᛫ ᛮ+᛹ ᜀ+ᜒ.᜖ ᜟ+ᜲ.᜵ ᝀ+ᝒ.᝔ ᝠ+᝭ ᝮ+᝱ ᝲ.᝴ ឴.។ ៝.៞ ០)៪ ᠋.᠎-᠏.᠐)᠚ ᠠ+᡹ ᢀ+ᢅ.ᢇ+ᢩ.ᢪ+᢫ ᢰ+᣶ ᤀ+᤟ ᤠ.᤬ ᤰ.᤼ ᥆)ᥐ ᧐)᧛ ᨀ+ᨗ.᨜ ᩕ.᩟ ᩠.᩽ ᩿.᪀)᪊ ᪐)᪚ ᪰.᫏ ᬀ.ᬅ+᬴.ᭅ+᭍ ᭐)᭚ ᭫.᭴ ᮀ.ᮃ+ᮡ.ᮮ+᮰)ᮺ+᯦.᯴ ᰀ+ᰤ.᰸ ᱀)᱊ ᱍ+᱐)ᱚ+᱾ ᲀ+᲋ Ა+᲻ Ჽ+᳀ ᳐.᳓ ᳔.ᳩ+᳭.ᳮ+᳴.ᳵ+᳷.ᳺ+᳻ ᴀ+᷀.Ḁ+἖ Ἐ+἞ ἠ+὆ Ὀ+὎ ὐ+὘ Ὑ+὚ Ὓ+὜ Ὕ+὞ Ὗ+὾ ᾀ+᾵ ᾶ+᾽ ι+᾿ ῂ+῅ ῆ+῍ ῐ+῔ ῖ+῜ ῠ+῭ ῲ+῵ ῶ+´  $   $​ ‌.‍0‎-‐ ‘(‚ ․(‥ ‧*\\\\u2028\\\"‪- ,‰ ‿,⁁ ⁄'⁅ ⁔,⁕  $⁠-⁥ ⁦-⁰ ⁱ+⁲ ⁿ+₀ ₐ+₝ ⃐.⃱ ℂ+℃ ℇ+℈ ℊ+℔ ℕ+№ ℙ+℞ ℤ+℥ Ω+℧ ℨ+℩ K+℮ ℯ+℺ ℼ+⅀ ⅅ+⅊ ⅎ+⅏ Ⅰ+↉ Ⓐ+⓪ Ⰰ+⳥ Ⳬ+⳯.Ⳳ+⳴ ⴀ+⴦ ⴧ+⴨ ⴭ+⴮ ⴰ+⵨ ⵯ+⵰ ⵿.ⶀ+⶗ ⶠ+⶧ ⶨ+⶯ ⶰ+⶷ ⶸ+⶿ ⷀ+⷇ ⷈ+⷏ ⷐ+⷗ ⷘ+⷟ ⷠ.⸀ ⸯ+⸰  $、 々+〆 〪.〰 〱1〶 〻+〽 ゙.゛1ゝ ゠1・ ー1㄀ ㄅ+㄰ ㄱ+㆏ ㆠ+㇀ ㇰ1㈀ ㋐1㋿ ㌀1㍘ ꀀ+꒍ ꓐ+꓾ ꔀ+꘍ ꘐ+꘠)ꘪ+꘬ Ꙁ+꙯.꙳ ꙴ.꙾ ꙿ+ꚞ.ꚠ+꛰.꛲ ꜈+꟎ Ꟑ+꟒ ꟓ+꟔ ꟕ+꟝ ꟲ+ꠂ.ꠃ+꠆.ꠇ+ꠋ.ꠌ+ꠣ.꠨ ꠬.꠭ ꡀ+꡴ ꢀ.ꢂ+ꢴ.꣆ ꣐)꣚ ꣠.ꣲ+꣸ ꣻ+꣼ ꣽ+ꣿ.꤀)ꤊ+ꤦ.꤮ ꤰ+ꥇ.꥔ ꥠ+꥽ ꦀ.ꦄ+꦳.꧁ ꧏ+꧐)꧚ ꧥ.ꧦ ꧰)ꧺ ꨀ+ꨩ.꨷ ꩀ+ꩃ.ꩄ+ꩌ.꩎ ꩐)꩚ ꩻ.ꩾ ꪰ.ꪱ ꪲ.ꪵ ꪷ.ꪹ ꪾ.ꫀ ꫁.ꫂ ꫠ+ꫫ.꫰ ꫲ+ꫵ.꫷ ꬁ+꬇ ꬉ+꬏ ꬑ+꬗ ꬠ+꬧ ꬨ+꬯ ꬰ+꭪ ꭰ+ꯣ.꯫ ꯬.꯮ ꯰)꯺ 가+힤 ힰ+퟇ ퟋ+퟼ ff+﬇ ﬓ+﬘ יִ/ﬞ.ײַ/﬩ שׁ/﬷ טּ/﬽ מּ/﬿ נּ/﭂ ףּ/﭅ צּ/ﭐ+﮲ ﯓ+﴾ ﵐ+﶐ ﶒ+﷈ ﷰ+﷼ ︀.︐ ︓*︔ ︠.︰ ︳,︵ ﹍,﹐'﹑ ﹒(﹓ ﹔'﹕*﹖ ﹰ+﹵ ﹶ+﻽ \\\\uFEFF-＀ '(( ,'- .(/ 0):*;'< A+[ _,` a+{ ヲ1゙.ᅠ+﾿ ᅡ+￈ ᅧ+￐ ᅭ+￘ ᅳ+￝ - ￿ `,kt=\\\"𐀀+𐀌 𐀍+𐀧 𐀨+𐀻 𐀼+𐀾 𐀿+𐁎 𐁐+𐁞 𐂀+𐃻 𐅀+𐅵 𐇽.𐇾 𐊀+𐊝 𐊠+𐋑 𐋠.𐋡 𐌀+𐌠 𐌭+𐍋 𐍐+𐍶.𐍻 𐎀+𐎞 𐎠+𐏄 𐏈+𐏐 𐏑+𐏖 𐐀+𐒞 𐒠)𐒪 𐒰+𐓔 𐓘+𐓼 𐔀+𐔨 𐔰+𐕤 𐕰+𐕻 𐕼+𐖋 𐖌+𐖓 𐖔+𐖖 𐖗+𐖢 𐖣+𐖲 𐖳+𐖺 𐖻+𐖽 𐗀+𐗴 𐘀+𐜷 𐝀+𐝖 𐝠+𐝨 𐞀+𐞆 𐞇+𐞱 𐞲+𐞻 𐠀+𐠆 𐠈+𐠉 𐠊+𐠶 𐠷+𐠹 𐠼+𐠽 𐠿+𐡖 𐡠+𐡷 𐢀+𐢟 𐣠+𐣳 𐣴+𐣶 𐤀+𐤖 𐤠+𐤺 𐦀+𐦸 𐦾+𐧀 𐨀+𐨁.𐨄 𐨅.𐨇 𐨌.𐨐+𐨔 𐨕+𐨘 𐨙+𐨶 𐨸.𐨻 𐨿.𐩀 𐩠+𐩽 𐪀+𐪝 𐫀+𐫈 𐫉+𐫥.𐫧 𐬀+𐬶 𐭀+𐭖 𐭠+𐭳 𐮀+𐮒 𐰀+𐱉 𐲀+𐲳 𐳀+𐳳 𐴀+𐴤.𐴨 𐴰)𐴺 𐵀)𐵊+𐵦 𐵩.𐵮 𐵯+𐶆 𐺀+𐺪 𐺫.𐺭 𐺰+𐺲 𐻂+𐻅 𐻼.𐼀+𐼝 𐼧+𐼨 𐼰+𐽆.𐽑 𐽰+𐾂.𐾆 𐾰+𐿅 𐿠+𐿷 𑀀.𑀃+𑀸.𑁇 𑁦)𑁰.𑁱+𑁳.𑁵+𑁶 𑁿.𑂃+𑂰.𑂻 𑂽)𑂾 𑃂.𑃃 𑃍)𑃎 𑃐+𑃩 𑃰)𑃺 𑄀.𑄃+𑄧.𑄵 𑄶)𑅀 𑅄+𑅅.𑅇+𑅈 𑅐+𑅳.𑅴 𑅶+𑅷 𑆀.𑆃+𑆳.𑇁+𑇅 𑇉.𑇍 𑇎.𑇐)𑇚+𑇛 𑇜+𑇝 𑈀+𑈒 𑈓+𑈬.𑈸 𑈾.𑈿+𑉁.𑉂 𑊀+𑊇 𑊈+𑊉 𑊊+𑊎 𑊏+𑊞 𑊟+𑊩 𑊰+𑋟.𑋫 𑋰)𑋺 𑌀.𑌄 𑌅+𑌍 𑌏+𑌑 𑌓+𑌩 𑌪+𑌱 𑌲+𑌴 𑌵+𑌺 𑌻.𑌽+𑌾.𑍅 𑍇.𑍉 𑍋.𑍎 𑍐+𑍑 𑍗.𑍘 𑍝+𑍢.𑍤 𑍦.𑍭 𑍰.𑍵 𑎀+𑎊 𑎋+𑎌 𑎎+𑎏 𑎐+𑎶 𑎷+𑎸.𑏁 𑏂.𑏃 𑏅.𑏆 𑏇.𑏋 𑏌.𑏑+𑏒.𑏓+𑏔 𑏡.𑏣 𑐀+𑐵.𑑇+𑑋 𑑐)𑑚 𑑞.𑑟+𑑢 𑒀+𑒰.𑓄+𑓆 𑓇+𑓈 𑓐)𑓚 𑖀+𑖯.𑖶 𑖸.𑗁 𑗘+𑗜.𑗞 𑘀+𑘰.𑙁 𑙄+𑙅 𑙐)𑙚 𑚀+𑚫.𑚸+𑚹 𑛀)𑛊 𑛐)𑛤 𑜝.𑜬 𑜰)𑜺 𑠀+𑠬.𑠻 𑢠+𑣠)𑣪 𑣿+𑤇 𑤉+𑤊 𑤌+𑤔 𑤕+𑤗 𑤘+𑤰.𑤶 𑤷.𑤹 𑤻.𑤿+𑥀.𑥁+𑥂.𑥄 𑥐)𑥚 𑦠+𑦨 𑦪+𑧑.𑧘 𑧚.𑧡+𑧢 𑧣+𑧤.𑧥 𑨀+𑨁.𑨋+𑨳.𑨺+𑨻.𑨿 𑩇.𑩈 𑩐+𑩑.𑩜+𑪊.𑪚 𑪝+𑪞 𑪰+𑫹 𑯀+𑯡 𑯰)𑯺 𑰀+𑰉 𑰊+𑰯.𑰷 𑰸.𑱀+𑱁 𑱐)𑱚 𑱲+𑲐 𑲒.𑲨 𑲩.𑲷 𑴀+𑴇 𑴈+𑴊 𑴋+𑴱.𑴷 𑴺.𑴻 𑴼.𑴾 𑴿.𑵆+𑵇.𑵈 𑵐)𑵚 𑵠+𑵦 𑵧+𑵩 𑵪+𑶊.𑶏 𑶐.𑶒 𑶓.𑶘+𑶙 𑶠)𑶪 𑻠+𑻳.𑻷 𑼀.𑼂+𑼃.𑼄+𑼑 𑼒+𑼴.𑼻 𑼾.𑽃 𑽐)𑽚.𑽛 𑾰+𑾱 𒀀+𒎚 𒐀+𒑯 𒒀+𒕄 𒾐+𒿱 𓀀+𓐰-𓑀.𓑁+𓑇.𓑖 𓑠+𔏻 𔐀+𔙇 𖄀+𖄞.𖄰)𖄺 𖠀+𖨹 𖩀+𖩟 𖩠)𖩪 𖩰+𖪿 𖫀)𖫊 𖫐+𖫮 𖫰.𖫵 𖬀+𖬰.𖬷 𖭀+𖭄 𖭐)𖭚 𖭣+𖭸 𖭽+𖮐 𖵀+𖵭 𖵰)𖵺 𖹀+𖺀 𖼀+𖽋 𖽏.𖽐+𖽑.𖾈 𖾏.𖾓+𖾠 𖿠+𖿢 𖿣+𖿤.𖿥 𖿰.𖿲 𚿰1𚿴 𚿵1𚿼 𚿽1𚿿 𛀀1𛀁 𛄠1𛄣 𛅕1𛅖 𛅤1𛅨 𛰀+𛱫 𛱰+𛱽 𛲀+𛲉 𛲐+𛲚 𛲝.𛲟 𛲠-𛲤 𜳰)𜳺 𜼀.𜼮 𜼰.𜽇 𝅥.𝅪 𝅭.𝅳-𝅻.𝆃 𝆅.𝆌 𝆪.𝆮 𝉂.𝉅 𝐀+𝑕 𝑖+𝒝 𝒞+𝒠 𝒢+𝒣 𝒥+𝒧 𝒩+𝒭 𝒮+𝒺 𝒻+𝒼 𝒽+𝓄 𝓅+𝔆 𝔇+𝔋 𝔍+𝔕 𝔖+𝔝 𝔞+𝔺 𝔻+𝔿 𝕀+𝕅 𝕆+𝕇 𝕊+𝕑 𝕒+𝚦 𝚨+𝛁 𝛂+𝛛 𝛜+𝛻 𝛼+𝜕 𝜖+𝜵 𝜶+𝝏 𝝐+𝝯 𝝰+𝞉 𝞊+𝞩 𝞪+𝟃 𝟄+𝟌 𝟎)𝠀 𝨀.𝨷 𝨻.𝩭 𝩵.𝩶 𝪄.𝪅 𝪛.𝪠 𝪡.𝪰 𝼀+𝼟 𝼥+𝼫 𞀀.𞀇 𞀈.𞀙 𞀛.𞀢 𞀣.𞀥 𞀦.𞀫 𞀰+𞁮 𞂏.𞂐 𞄀+𞄭 𞄰.𞄷+𞄾 𞅀)𞅊 𞅎+𞅏 𞊐+𞊮.𞊯 𞋀+𞋬.𞋰)𞋺 𞓐+𞓬.𞓰)𞓺 𞗐+𞗮.𞗰+𞗱)𞗻 𞟠+𞟧 𞟨+𞟬 𞟭+𞟯 𞟰+𞟿 𞠀+𞣅 𞣐.𞣗 𞤀+𞥄.𞥋+𞥌 𞥐)𞥚 𞸀+𞸄 𞸅+𞸠 𞸡+𞸣 𞸤+𞸥 𞸧+𞸨 𞸩+𞸳 𞸴+𞸸 𞸹+𞸺 𞸻+𞸼 𞹂+𞹃 𞹇+𞹈 𞹉+𞹊 𞹋+𞹌 𞹍+𞹐 𞹑+𞹓 𞹔+𞹕 𞹗+𞹘 𞹙+𞹚 𞹛+𞹜 𞹝+𞹞 𞹟+𞹠 𞹡+𞹣 𞹤+𞹥 𞹧+𞹫 𞹬+𞹳 𞹴+𞹸 𞹹+𞹽 𞹾+𞹿 𞺀+𞺊 𞺋+𞺜 𞺡+𞺤 𞺥+𞺪 𞺫+𞺼 🄰+🅊 🅐+🅪 🅰+🆊 🇦2🈀 🏻.🐀 🯰)🯺 󠀁-󠀂 󠀠.󠂀 󠄀.󠇰 \\\";function wt(o){let e=o<=65535?2:3,t=e==2?bt:kt;return He(t,o,e,0,t.length/e-1)-32}h(wt,\\\"searchForProperty\\\");function He(o,e,t,r,n){if(n=a?He(o,e,t,i+1,n):o.charCodeAt(t*(i+1)-1)}h(He,\\\"_searchForProperty\\\");function R(o,e){let t=jt(o,e);if(t.length==0)return[];let r=[];for(let n=0;n=this.text.length?20:yt(this.text[e])?ze(this.text[e]+this.text[e+1]):ze(this.text[e],this.options)}match(e,t,r,n){var s,l,a,u;let i=(s=e==null?void 0:e.includes(this.lookbehind))!=null?s:!0;return i=i&&((l=t==null?void 0:t.includes(this.left))!=null?l:!0),i=i&&((a=r==null?void 0:r.includes(this.right))!=null?a:!0),i&&((u=n==null?void 0:n.includes(this.lookahead))!=null?u:!0)}propertyMatch(e,t,r,n){let i=h(s=>vt(s,this.options),\\\"propMapper\\\");return this.match(e==null?void 0:e.map(i),t==null?void 0:t.map(i),r==null?void 0:r.map(i),n==null?void 0:n.map(i))}};h(Y,\\\"BreakerContext\\\");var Ve=Y;function Gt(o,e){return!o.split(\\\"\\\").map(t=>ze(t,e)).every(t=>t===3||t===1||t===2||t===4)}h(Gt,\\\"isNonSpace\\\");function jt(o,e){if(o.length===0)return[];e&&!e.rules&&(e.rules=[]);let t=[],r,n=0,i=new Ve(o,e,n),s=0;do{if(r=n,n=l(n),i=i.next(n),i.match(null,[19],null,null)){t.push(r);continue}if(i.match(null,null,[20],null)){t.push(r);break}if(i.match(null,[3],[1],null))continue;let a=[2,3,1];if(i.match(null,a,null,null)){t.push(r);continue}if(i.match(null,null,a,null)){t.push(r);continue}if(i.match(null,[4],[4],null))continue;let u=[13,14,16];for(;i.match(null,null,u,null);)[r,n]=[n,l(n)],i=i.ignoringRight(n);if(i.right===20){t.push(r);break}for(;i.match(null,null,null,u);)n=l(n),i=i.ignoringLookahead(n);let c=[11,15],p=[8,6];if(e!=null&&e.rules){let x=!1;for(let m of e.rules)if(x=m.match(i),x){m.breakIfMatch&&t.push(r);break}if(x)continue}if(i.match(null,c,c,null))continue;let f=[10].concat(p);if(i.match(null,c,f,c)||i.match(c,f,c,null)||i.match(null,[15],[6],null)||i.match(null,[15],[5],[15])||i.match([15],[5],[15],null)||i.match(null,[9],[9],null)||i.match(null,c,[9],null)||i.match(null,[9],c,null))continue;let d=[7].concat(p);if(i.match([9],d,[9],null)||i.match(null,[9],d,[9])||i.match(null,[17],[17],null))continue;let C=[17,9].concat(c);if(!i.match(null,C,[12],null)&&!i.match(null,[12],[12],null)&&!i.match(null,[12],C,null)){if(i.right===18){if(s+=1,s%2==1)continue}else s=0;t.push(r)}}while(r=o.length?o.length:yt(o[a])?a+2:a+1}}h(jt,\\\"findBoundaries\\\");function yt(o){let e=o.charCodeAt(0);return e>=55296&&e<=56319}h(yt,\\\"isStartOfSurrogatePair\\\");function ze(o,e){if(e!=null&&e.propertyMapping){let r=e.propertyMapping(o);if(r)return vt(r,e)}let t=o.codePointAt(0);return wt(t)}h(ze,\\\"property\\\");function vt(o,e){var n,i;let t=h(s=>s.toLowerCase()==o.toLowerCase(),\\\"matcher\\\"),r=(i=(n=e==null?void 0:e.customProperties)==null?void 0:n.findIndex(t))!=null?i:-1;return r!=-1?-r-1:xt.findIndex(t)}h(vt,\\\"propertyVal\\\");O();var Lt=12,_=class _{constructor(e,t,r){this.root=e,this.prefix=t,this.totalWeight=r}child(e){if(e==\\\"\\\")return this;let t=e.split(\\\"\\\"),r=this;for(;t.length>0&&r;){let n=t.shift();r=r._child(n)}return r}_child(e){let t=this.root,r=this.totalWeight,n=this.prefix+e;if(t.type==\\\"internal\\\"){let i=t.children[e];return i?new _(i,n,r):void 0}else return t.entries.filter(function(s){return s.key.indexOf(n)==0}).length?new _(t,n,r):void 0}*children(){let e=this.root,t=this.totalWeight;if(e.type==\\\"internal\\\"){for(let r of e.values){let n=e.children[r];if(F(r))if(n.type==\\\"internal\\\"){let i=n;for(let s of i.values){let l=this.prefix+r+s;yield{char:r+s,traversal:function(){return new _(i.children[s],l,t)}}}}else{let i=n.entries[0].key;r=r+i[this.prefix.length+1];let s=this.prefix+r;yield{char:r,traversal:function(){return new _(n,s,t)}}}else{if(ke(r))continue;if(r){let i=this.prefix+r;yield{char:r,traversal:function(){return new _(n,i,t)}}}else continue}}return}else{let r=this.prefix,n=e.entries.filter(function(i){return i.key!=r&&r.length({text:t.content,p:t.weight/this.totalWeight}),\\\"entryMapper\\\");if(this.root.type==\\\"leaf\\\"){let t=this.prefix;return this.root.entries.filter(function(n){return n.key==t}).map(e)}else{let t=this.root.children[N];return t&&t.type==\\\"leaf\\\"?t.entries.map(e):[]}}get p(){return this.root.weight/this.totalWeight}};h(_,\\\"Traversal\\\");var je=_,Ye=class Ye{constructor(e,t={}){this.languageUsesCasing=t.languageUsesCasing,this.applyCasing=t.applyCasing,this._trie=new Xe(e.root,e.totalWeight,t.searchTermToKey||Xt),this.breakWords=t.wordBreaker||R,this.punctuation=t.punctuation}configure(e){var t;return this.configuration={leftContextCodePoints:e.maxLeftContextCodePoints,rightContextCodePoints:(t=e.maxRightContextCodePoints)!=null?t:0}}toKey(e){return this._trie.toKey(e)}predict(e,t){if(!e.insert&&!t.left&&!t.right&&t.startOfBuffer&&t.endOfBuffer)return s(this._trie.firstN(Lt).map(({text:l,p:a})=>({transform:{insert:l,deleteLeft:0},displayAs:l,p:a})));let r=g(e,t),n=e.deleteLeft-e.insert.kmwLength(),i=ae(this.breakWords,r);return s(this._trie.lookup(i).map(({text:l,p:a})=>W({insert:l,deleteLeft:n+i.kmwLength()},a)));function s(l){let a=[];for(let u of l)a.push({sample:u,p:u.p});return a}}get wordbreaker(){return this.breakWords}traverseFromRoot(){return this._trie.traverseFromRoot()}};h(Ye,\\\"TrieModel\\\");var ue=Ye,$e=class $e{constructor(e,t,r){this.root=e,this.toKey=r,this.totalWeight=t}traverseFromRoot(){return new je(this.root,\\\"\\\",this.totalWeight)}lookup(e){let t=this.toKey(e),r=this.traverseFromRoot().child(t);if(!r)return[];let n=r.entries,i={};for(let a of n)i[a.text]=a.text;let l=Mt(r).filter(a=>!i[a.text]);return n.concat(l)}firstN(e){return Mt(this.traverseFromRoot(),e)}};h($e,\\\"Trie\\\");var Xe=$e;function Mt(o,e=Lt){let t=new k(function(n,i){return(i?i.p:0)-(n?n.p:0)}),r=[];for(t.enqueue(o);t.count>0;){let n=t.dequeue();if(n.text!==void 0){let i=n;if(r.push(i),r.length>=e)return r}else{let i=n;t.enqueueAll(i.entries);let s=[];for(let l of i.children())s.push(l.traversal());t.enqueueAll(s)}}return r}h(Mt,\\\"getSortedResults\\\");function Xt(o){return o.normalize(\\\"NFD\\\").replace(/[\\\\u0300-\\\\u036f]/g,\\\"\\\").toLowerCase()}h(Xt,\\\"defaultSearchTermToKey\\\");var et=class et{constructor(e){e=e||{},this._futureSuggestions=e.futureSuggestions?e.futureSuggestions.slice():[],e.punctuation&&(this.punctuation=e.punctuation),this.toKey=e.toKey,this.wordbreaker=e.wordbreaker,this.applyCasing=e.applyCasing,this.languageUsesCasing=e.languageUsesCasing}configure(e){return this.configuration={leftContextCodePoints:e.maxLeftContextCodePoints,rightContextCodePoints:e.maxRightContextCodePoints},this.configuration}predict(e,t,r){let n=h(function(s){let l=[];for(let a of s)l.push({sample:a,p:a.p!==void 0?a.p:1});return l},\\\"makeUniformDistribution\\\");if(r)return n(r);let i=this._futureSuggestions.shift();return i?n(i):[]}};h(et,\\\"DummyModel\\\");var Je=et,Et=Je;var Ce={};_e(Ce,{ClassicalDistanceCalculation:()=>K,ContextTracker:()=>Te,ExecutionBucket:()=>ce,ExecutionSpan:()=>Q,ExecutionTimer:()=>he,QUEUE_NODE_COMPARATOR:()=>Le,STANDARD_TIME_BETWEEN_DEFERS:()=>Me,SearchNode:()=>Ee,SearchResult:()=>de,SearchSpace:()=>v,TrackedContextState:()=>V,TrackedContextSuggestion:()=>lt,TrackedContextToken:()=>z});var T=class T{constructor(e){this.diagonalWidth=2;this.inputSequence=[];this.matchSequence=[];if(e){let t=e.resolvedDistances.length;this.resolvedDistances=Array(t);for(let r=0;r2*r)&&(n.sparse=!0),n}getCostAt(e,t,r=this.diagonalWidth){if(e<0||t<0)return e==-1&&t>=-1?t+1:t==-1&&e>=-1?e+1:Number.MAX_VALUE;let n=this.getTrueIndex(e,t,r);return n.sparse?Number.MAX_VALUE:this.resolvedDistances[n.row][n.col]}getFinalCost(){let e=this,t=e.getHeuristicFinalCost();for(;t>e.diagonalWidth;)e=e.increaseMaxDistance(),t=e.getHeuristicFinalCost();return t}getHeuristicFinalCost(){return this.getCostAt(this.inputSequence.length-1,this.matchSequence.length-1)}hasFinalCostWithin(e){let t=this,r=t.getHeuristicFinalCost(),n=this.diagonalWidth;do{if(r<=e)return!0;if(n=0&&c>=0){let p=1;if(n=[\\\"transpose-start\\\"],u!=e-1){let f=e-u-1;n=n.concat(Array(f).fill(\\\"transpose-delete\\\")),p+=f}else{let f=t-c-1;n=n.concat(Array(f).fill(\\\"transpose-insert\\\")),p+=f}n.push(\\\"transpose-end\\\"),this.getCostAt(u-1,c-1)!=r-p&&(n=null),i=[u-1,c-1]}return n||(a==r-1?(n=[\\\"substitute\\\"],i=[e-1,t-1]):s==r-1?(n=[\\\"insert\\\"],i=[e,t-1]):l==r-1?(n=[\\\"delete\\\"],i=[e-1,t]):(n=[\\\"match\\\"],i=[e-1,t-1])),i[0]>=0&&i[1]>=0?this.editPath(i[0],i[1]).concat(n):i[0]>-1?Array(i[0]+1).fill(\\\"delete\\\").concat(n):i[1]>-1?Array(i[1]+1).fill(\\\"insert\\\").concat(n):n}static getTransposeParent(e,t,r){if(t<0||r<0||e.inputSequence[t].key==e.matchSequence[r].key)return[-1,-1];let n=-1;for(let s=t-1;s>=0;s--)if(e.inputSequence[s].key==e.matchSequence[r].key){n=s;break}let i=-1;for(let s=r-1;s>=0;s--)if(e.matchSequence[s].key==e.inputSequence[t].key){i=s;break}return[n,i]}static initialCostAt(e,t,r,n,i){var s=e.inputSequence[t].key==e.matchSequence[r].key?0:1,l=e.getCostAt(t-1,r-1)+s,a=n||e.getCostAt(t,r-1)+1,u=i||e.getCostAt(t-1,r)+1,c=Number.MAX_VALUE;if(t>0&&r>0){let[p,f]=T.getTransposeParent(e,t,r);c=e.getCostAt(p-1,f-1)+(t-p-1)+1+(r-f-1)}return Math.min(l,u,a,c)}getSubset(e,t){let r=new T(this);if(e>this.inputSequence.length||t>this.matchSequence.length)throw\\\"Invalid dimensions specified for trim operation\\\";r.inputSequence.splice(e),r.matchSequence.splice(t),r.resolvedDistances.splice(e);let n=this.getTrueIndex(e-1,t-1,this.diagonalWidth);for(let i=n.col;i<=2*this.diagonalWidth;i++){let s=n.row-(i-n.col);if(s<0)break;if(i<0)r.resolvedDistances[s]=Array(2*r.diagonalWidth+1).fill(Number.MAX_VALUE);else{let l=2*this.diagonalWidth-i,a=r.resolvedDistances[s].splice(0,i+1),u=Array(l).fill(Number.MAX_VALUE);r.resolvedDistances[s]=a.concat(u)}}return r}static forDiagonalOfAxis(e,t,r,n){let i=r-t=0){let a=s==0?n+2:Number.MAX_VALUE;i=T.initialCostAt(e,n,s,a,void 0);let u=i;if(s0&&s){let a=n+1;this.propagateUpdateFrom(e,t+1,r,a,i-1)}if(s&&l){let a=n+(e.inputSequence[t+1].key==e.matchSequence[r+1].key?0:1);this.propagateUpdateFrom(e,t+1,r+1,a,i);let u=-1;for(let p=t+2;p0&&c>0){let p=n+(u-t-2)+1+(c-r-2);this.propagateUpdateFrom(e,u,c,p,e.diagonalWidth-1+c-u)}}}get mapKey(){let e=this.inputSequence.map(r=>r.key).join(\\\"\\\"),t=this.matchSequence.map(r=>r.key).join(\\\"\\\");return e+N+t+N+this.diagonalWidth}get lastInputEntry(){return this.inputSequence[this.inputSequence.length-1]}get lastMatchEntry(){return this.matchSequence[this.matchSequence.length-1]}static computeDistance(e,t,r=1){let n=new T;r=r||1,n.diagonalWidth=r;for(let i=0;i=Yt&&(this.nearOutliers.lengthr-t)),this.checkForOutlier()}checkForOutlier(){if(this.preventOutliers||this.eventCount<4||this.nearOutliers.length==0)return;let e=this.nearOutliers[0];this.timeSpent-=e,this.timeSquared-=e*e,this.eventCount--;let t=this.average,r=e-t,n=this.variance,i=r*r/n;i>=49||this.eventCount>=8&&i>=9?(this.nearOutliers.shift(),this.outliers.push(e)):(this.timeSpent+=e,this.timeSquared+=e*e,this.eventCount++)}get average(){return this.timeSpent/this.eventCount}get variance(){let e=this.eventCount;return e<=1?NaN:this.timeSquared/e-this.timeSpent*this.timeSpent/(e*e)}get outlierTime(){let e=0;for(let t=0;tthis.activeSpan=null),yield Ue(e),this.activeSpan.end(),this.spanSinceLastDefer=new Q})}get timeSinceLastDefer(){return this.spanSinceLastDefer.duration}start(e){this.validateStart();let t=this.getBucket(e);return this.activeSpan=new Q(t,()=>{this.activeSpan=null}),this.activeSpan}terminate(){this.maxTrueTime=0}get elapsed(){return performance.now()-this.trueStart>=this.maxTrueTime?!0:this.executionTime>=this.maxExecutionTime}};h(nt,\\\"ExecutionTimer\\\");var he=nt;var Le=h(function(o,e){return o.currentCost-e.currentCost},\\\"QUEUE_NODE_COMPARATOR\\\");var H=class H{constructor(e,t){this.toKey=h(e=>e,\\\"toKey\\\");if(t=t||(r=>r),e instanceof H){let r=e;this.calculation=r.calculation,this.currentTraversal=r.currentTraversal,this.priorInput=r.priorInput,this.toKey=r.toKey}else this.calculation=new K,this.currentTraversal=e,this.priorInput=[],this.toKey=t}get knownCost(){return this.calculation.getHeuristicFinalCost()}get inputSamplingCost(){if(this._inputCost!==void 0)return this._inputCost;{let e=v.MIN_KEYSTROKE_PROBABILITY;return this._inputCost=this.priorInput.map(t=>t.p>e?t.p:e).reduce((t,r)=>t-Math.log(r),0),this._inputCost}}get currentCost(){return v.EDIT_DISTANCE_COST_SCALE*this.knownCost+this.inputSamplingCost}buildInsertionEdges(){let e=[];for(let t of this.currentTraversal.children()){let r=t.traversal(),n={key:t.char,traversal:r},i=this.calculation.addMatchChar(n),s=new H(this);s.calculation=i,s.priorInput=this.priorInput,s.currentTraversal=r,e.push(s)}return e}buildDeletionEdges(e){let t=[];for(let r of e){if(r.p\\\"+\\\"+r.sample.insert+\\\"-\\\"+r.sample.deleteLeft).join(\\\"\\\"),t=this.calculation.matchSequence.map(r=>r.key).join(\\\"\\\");return e+N+t}get resultKey(){return this.calculation.matchSequence.map(e=>e.key).join(\\\"\\\")}get isFullReplacement(){return this.knownCost&&this.knownCost==this.priorInput.length}};h(H,\\\"SearchNode\\\");var Ee=H,it=class it{constructor(e,t){this.processed=[];if(typeof e==\\\"number\\\"){this.index=e,this.correctionQueue=new k(Le,t);return}else this.index=e.index,this.processed=[].concat(e.processed),this.correctionQueue=new k(e.correctionQueue)}increaseMaxEditDistance(){let e=this.correctionQueue.toArray();e.forEach(function(t){t.calculation=t.calculation.increaseMaxDistance()}),this.correctionQueue=new k(Le,e)}};h(it,\\\"SearchSpaceTier\\\");var pe=it,ot=class ot{constructor(e){this.resultNode=e}get inputSequence(){return this.resultNode.priorInput}get matchSequence(){return this.resultNode.calculation.matchSequence}get matchString(){return this.resultNode.resultKey}get knownCost(){return this.resultNode.knownCost}get inputSamplingCost(){return this.resultNode.inputSamplingCost}get totalCost(){return this.resultNode.currentCost}get finalTraversal(){return this.resultNode.currentTraversal}};h(ot,\\\"SearchResult\\\");var de=ot,q=class q{constructor(e){this.tierOrdering=[];this.inputSequence=[];this.minInputCost=[];this.returnedValues={};this.processedEdgeSet={};if(this.buildQueueSpaceComparator(),e instanceof q){this.inputSequence=[].concat(e.inputSequence),this.minInputCost=[].concat(e.minInputCost),this.rootNode=e.rootNode,this.completedPaths=[].concat(e.completedPaths),this.returnedValues=A({},e.returnedValues),this.processedEdgeSet=A({},e.processedEdgeSet),this.tierOrdering=e.tierOrdering.map(n=>new pe(n)),this.selectionQueue=new k(this.QUEUE_SPACE_COMPARATOR,this.tierOrdering);return}let t=e;if(t){if(!t.traverseFromRoot)throw\\\"The provided model does not implement the `traverseFromRoot` function, which is needed to support robust correction searching.\\\"}else throw\\\"The LexicalModel parameter must not be null / undefined.\\\";this.selectionQueue=new k(this.QUEUE_SPACE_COMPARATOR),this.rootNode=new Ee(t.traverseFromRoot(),t.toKey?t.toKey.bind(t):null),this.completedPaths=[this.rootNode];let r=new pe(0,[this.rootNode]);this.tierOrdering.push(r),this.selectionQueue.enqueue(r)}buildQueueSpaceComparator(){let e=this;this.QUEUE_SPACE_COMPARATOR=function(t,r){let n=t.correctionQueue.peek(),i=r.correctionQueue.peek(),s=t.index,l=r.index,a=0,u=1;if(l0:!1}handleNextNode(){if(!this.hasNextMatchEntry())return{type:\\\"none\\\"};let e=this.selectionQueue.dequeue(),t=e.correctionQueue.dequeue(),r={type:\\\"intermediate\\\",cost:t.currentCost};if(this.processedEdgeSet[t.pathKey])return this.selectionQueue.enqueue(e),r;this.processedEdgeSet[t.pathKey]=!0;let n=!1;if(t.knownCost>2)return r;t.knownCost==2&&(n=!0);let i=0;for(let s=0;s<=e.index;s++)i+=this.minInputCost[s];if(t.currentCost>i+2.5*q.EDIT_DISTANCE_COST_SCALE)return r;if(!n){let s=t.buildInsertionEdges();e.correctionQueue.enqueueAll(s)}if(e.index==this.tierOrdering.length-1)return this.completedPaths.push(t),this.selectionQueue.enqueue(e),{type:\\\"complete\\\",cost:t.currentCost,finalNode:t};{let s=this.tierOrdering[e.index+1],l=s.index,a=[];n||(a=t.buildDeletionEdges(this.inputSequence[l-1]));let u=t.buildSubstitutionEdges(this.inputSequence[l-1]);s.correctionQueue.enqueueAll(a.concat(u)),this.selectionQueue=new k(this.QUEUE_SPACE_COMPARATOR,this.tierOrdering)}return r}getBestMatches(e){return Ct(this,null,function*(){let t={},r=Object.values(this.returnedValues);if(r.length>0){let n=new k(Le,r);for(;n.count>0;){let i=e.time(()=>{let s=n.dequeue();return s.isFullReplacement?null:(t[s.resultKey]=s,new de(s))},0);if(i){let s=e.start(1);yield i,s.end(),e.timeSinceLastDefer>Me&&(yield new be(e.defer()))}}}do{let n=e.time(()=>{var s,l;let i=this.handleNextNode();if(i.type==\\\"none\\\")return null;if(i.type==\\\"complete\\\"){if(i.finalNode.isFullReplacement)return null;let u=i.finalNode;if(((l=(s=t[u.resultKey])==null?void 0:s.currentCost)!=null?l:Number.MAX_VALUE)>u.currentCost)return t[u.resultKey]=u,this.returnedValues[u.resultKey]=u,new de(u)}return null},2);if(n){let i=e.start(1);yield n,i.end()}e.timeSinceLastDefer>Me&&(yield new be(e.defer()))}while(!e.elapsed&&this.hasNextMatchEntry());return null})}};h(q,\\\"SearchSpace\\\"),q.EDIT_DISTANCE_COST_SCALE=5,q.MIN_KEYSTROKE_PROBABILITY=1e-4,q.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL=33;var v=q;var st=class st{static isWhitespace(e){let t=/^[\\\\u0009\\\\u000A\\\\u000D\\\\u0020\\\\u00a0\\\\u1680\\\\u2000\\\\u2001\\\\u2002\\\\u2003\\\\u2004\\\\u2005\\\\u2006\\\\u2007\\\\u2008\\\\u2009\\\\u200a\\\\u200b\\\\u2028\\\\u2029\\\\u202f\\\\u205f\\\\u3000]+$/i;return e.insert.match(t)!=null}static isBackspace(e){return e.insert==\\\"\\\"&&e.deleteLeft>0&&!e.deleteRight}static isEmpty(e){return e.insert==\\\"\\\"&&e.deleteLeft==0&&!e.deleteRight}};h(st,\\\"TransformUtils\\\");var w=st;var $t={quotesForKeepSuggestion:{open:\\\"“\\\",close:\\\"”\\\"},insertAfterWord:\\\" \\\"};function me(o){let e=$t;if(!o.punctuation)return e;let t=o.punctuation,r=t.insertAfterWord;r!==\\\"\\\"&&!r&&(r=e.insertAfterWord);let n=t.quotesForKeepSuggestion;n||(n=e.quotesForKeepSuggestion);let i=t.isRTL;return{insertAfterWord:r,quotesForKeepSuggestion:n,isRTL:i}}h(me,\\\"determinePunctuationFromModel\\\");function B(o){return e=>{if(o.wordbreaker||!o.wordbreak){let t=o.wordbreaker||R;return X(t,e)}else return o.wordbreak(e)}}h(B,\\\"determineModelWordbreaker\\\");function $(o){return e=>o.wordbreaker?se(o.wordbreaker,e):null}h($,\\\"determineModelTokenizer\\\");function It(o,e){var i;let t=o,n=B(o)(e);if(!t.languageUsesCasing)throw\\\"Invalid attempt to detect casing: languageUsesCasing is set to false\\\";if(!t.applyCasing)throw\\\"Invalid LMLayer state: languageUsesCasing is set to true, but no applyCasing function exists\\\";return e.casingForm==\\\"upper\\\"||e.casingForm==\\\"initial\\\"?e.casingForm:t.applyCasing(\\\"lower\\\",n)==n?\\\"lower\\\":t.applyCasing(\\\"upper\\\",n)==n?n.kmwLength()>1?\\\"upper\\\":\\\"initial\\\":t.applyCasing(\\\"initial\\\",n)==n?\\\"initial\\\":(i=e.casingForm)!=null?i:null}h(It,\\\"detectCurrentCasing\\\");function at(o,e,t){let r=g(t,e),n=o(r).left,i=t.insert,s=[];for(let l=n.length-1;l>=0;l--){let a=n[l],u=a.text.length;if(u({sample:at(o,e,r.sample),p:r.p}))}h(Nt,\\\"tokenizeTransformDistribution\\\");function At(o,e){let t=[];for(let r=0;r0&&this.transformDistributions.push(e),this.raw=t}};h(ht,\\\"TrackedContextToken\\\");var z=ht,Pe=class Pe{constructor(e){this.searchSpace=[];if(e instanceof Pe){let t=e;this.tokens=t.tokens.map(function(n){let i=new z;return i.raw=n.raw,i.replacements=[].concat(n.replacements),i.activeReplacementId=n.activeReplacementId,i.transformDistributions=[].concat(n.transformDistributions),n.replacementText&&(i.replacementText=n.replacementText),i}),this.indexOffset=0;let r=this.model=e.model;this.taggedContext=e.taggedContext,r!=null&&r.traverseFromRoot&&(this.searchSpace=e.searchSpace.map(n=>new v(n)))}else{let t=e;this.tokens=[],this.indexOffset=Number.MIN_SAFE_INTEGER,this.model=t,t&&t.traverseFromRoot&&(this.searchSpace=[new v(t)])}}get head(){return this.tokens[0]}get tail(){return this.tokens[this.tokens.length-1]}popHead(){this.tokens.splice(0,1),this.indexOffset-=1}pushTail(e){this.model&&this.model.traverseFromRoot?this.searchSpace=[new v(this.model)]:this.searchSpace=[],this.tokens.push(e);let t=this;t.searchSpace.length>0&&e.transformDistributions.forEach(r=>t.searchSpace[0].addInput(r))}toRawTokenization(){let e=[];for(let t of this.tokens)t.currentText!==null&&e.push(t.currentText);return e}};h(Pe,\\\"TrackedContextState\\\");var V=Pe,fe=class fe{constructor(e=fe.DEFAULT_ARRAY_SIZE){this.currentHead=0;this.currentTail=0;this.circle=Array(e)}get count(){let e=this.currentHead-this.currentTail;return e<0&&(e=e+this.circle.length),e}get maxCount(){return this.circle.length}get oldest(){if(this.count!=0)return this.item(0)}get newest(){if(this.count!=0)return this.item(this.count-1)}enqueue(e){var t=null;let r=(this.currentHead+1)%this.maxCount;return r==this.currentTail&&(t=this.circle[this.currentTail],this.currentTail=(this.currentTail+1)%this.maxCount),this.circle[this.currentHead]=e,this.currentHead=r,t}dequeue(){if(this.currentTail==this.currentHead)return null;{let e=this.circle[this.currentTail];return this.currentTail=(this.currentTail+1)%this.maxCount,e}}popNewest(){if(this.currentTail==this.currentHead)return null;{let e=this.circle[this.currentHead];return this.currentHead=(this.currentHead-1+this.maxCount)%this.maxCount,e}}item(e){if(e>=this.count)return;let t=(this.currentTail+e)%this.maxCount;return this.circle[t]}};h(fe,\\\"CircularArray\\\"),fe.DEFAULT_ARRAY_SIZE=5;var ut=fe,ge=class ge extends ut{static attemptMatchContext(e,t,r){var C,x;let n=t.toRawTokenization(),s=K.computeDistance(n.map(m=>({key:m})),e.map(m=>({key:m.text})),3).editPath();s.length==2&&s[0]==\\\"insert\\\"&&s[1]==\\\"substitute\\\"&&(s[0]=\\\"substitute\\\",s[1]=\\\"insert\\\");let l=s.indexOf(\\\"match\\\"),a=s.lastIndexOf(\\\"match\\\");if(s.length>=2&&s[s.length-2]==\\\"substitute\\\"&&s[s.length-1]==\\\"match\\\"&&(a=s.lastIndexOf(\\\"match\\\",s.length-2)),l){for(let m=l+1;m({sample:G.sample[y],p:G.p})),M=f?(C=L[0])==null?void 0:C.sample:null;M&&M.insert==\\\"\\\"&&M.deleteLeft==0&&!M.deleteRight&&(M=null),b||(d=d?U(d,M):M);let qt=M&&w.isBackspace(M),te=e[m-p];switch(s[m]){case\\\"substitute\\\":b&&(u=new V(u));let G=u.tokens[m-p],S=t.tokens[m];qt?(G.updateWithBackspace(te.text,M.id),b&&(u.tokens.pop(),u.pushTail(G))):(G.update(L,te.text),b&&((x=u.searchSpace[0])==null||x.addInput(L))),u!=t&&!b&&(S.replacementText=te.text);break;case\\\"insert\\\":if(c&&c!=\\\"substitute\\\"&&c!=\\\"match\\\"&&c!=\\\"insert\\\")return null;d||(d={insert:\\\"\\\",deleteLeft:0}),u==t&&(u=new V(u));let E=new z;E.raw=te.text,M&&(E.transformDistributions=L?[L]:[]),E.isWhitespace=te.isWhitespace,u.pushTail(E);break;case\\\"match\\\":if(c==\\\"substitute\\\"&&e[e.length-1].text==\\\"\\\")continue;default:return null}c=s[m]}return{state:u,baseState:t,preservationTransform:d}}static modelContextState(e,t){let r=e.map(function(i){let s=new z;return s.raw=i.text,i.isWhitespace&&(s.isWhitespace=!0),s.raw?s.transformDistributions=At(s.raw).map(function(l){return[{sample:l,p:1}]}):s.transformDistributions=[],s}),n=new V(t);for(;r.length>0;)r.length==1&&r[0].updateWithBackspace(r[0].raw,null),n.pushTail(r.splice(0,1)[0]);if(n.tokens.length==0){let i=new z;i.raw=\\\"\\\",n.pushTail(i)}return n}analyzeState(e,t,r){if(!e.traverseFromRoot)throw\\\"This lexical model does not provide adequate data for correction algorithms and context reuse\\\";let n=$(e),i=r==null?void 0:r[0],s=0,l=null;i&&(s=at(n,t,i.sample).length,l=Nt(n,t,r),t=g(i.sample,t),l=l.filter(c=>c.sample.length==s));let a=n(t);if(a.left.length>0)for(let c=this.count-1;c>=0;c--){let p=this.item(c),f=p.taggedContext;if(f&&r&&r.length>0){if(g(r[0].sample,f).left!=t.left)continue}else if((f==null?void 0:f.left)!=t.left)continue;let d=ge.attemptMatchContext(a.left,this.item(c),l);if(d!=null&&d.state)return this.newest!=d.state&&this.newest!=p&&this.enqueue(p),d.state.taggedContext=t,d.state!=this.item(c)&&this.enqueue(d.state),d}let u=ge.modelContextState(a.left,e);return u.taggedContext=t,this.enqueue(u),{state:u,baseState:null}}clearCache(){for(;this.count>0;)this.dequeue()}};h(ge,\\\"ContextTracker\\\");var Te=ge;var Zt=.66,Ot={MAX_SEARCH_THRESHOLD:8,REPLACEMENT_SEARCH_THRESHOLD:4};function pt(o,e){let t=e.matchLevel-o.matchLevel;return t!=0?t:e.totalProb-o.totalProb}h(pt,\\\"tupleDisplayOrderSort\\\");function _t(o,e,t,r,n){return D(this,null,function*(){let i=B(e),s=r[0].sample,l=g(s,n),a=[];if(!o){let S,E=w.isWhitespace(s),Ae=w.isBackspace(s);return E?S=[{sample:s,p:1}]:S=r.map(P=>{let Se=P.sample;return w.isWhitespace(Se)&&!E||w.isBackspace(Se)&&!Ae?null:P}),S=S.filter(P=>!!P),a=Rt(e,S,n),E&&a.forEach(P=>P.preservationTransform=s),{postContextState:null,rawPredictions:a}}let{state:u}=o.analyzeState(e,n,null),c=o.analyzeState(e,n,w.isEmpty(s)?null:r),p=c.state,f=p.searchSpace[0],d=0,C=p.tokens,x=C.length-u.tokens.length;c.preservationTransform?(d=0,n=g(c.preservationTransform,n)):x<0?d=i(l).kmwLength()+s.deleteLeft:d=i(n).kmwLength();let m=C[C.length-1];m.raw==\\\"\\\"&&(d=0);let b=m.transformDistributions.length<=1,y,L={};try{for(var M=St(f.getBestMatches(t)),qt,te,G;qt=!(te=yield M.next()).done;qt=!1){let S=te.value;let E=S.matchString;if(S.matchSequence.length==0&&S.inputSequence.length!=0||S.matchSequence.length!=0&&S.matchSequence.length==S.knownCost)continue;let Ae={insert:E,deleteLeft:d,id:s.id},P=S.totalCost;b&&(P*=Z.SINGLE_CHAR_KEY_PROB_EXPONENT);let Se={sample:Ae,p:Math.exp(-P)},xe=Rt(e,[Se],n);xe.forEach(re=>re.preservationTransform=c.preservationTransform),xe.length>0&&y===void 0&&(y=P);let mt=L[S.matchString];if(mt&&(a=a.filter(re=>!mt.find(Bt=>re.prediction.sample==Bt.sample))),L[S.matchString]=xe.map(re=>re.prediction),a=a.concat(xe),Jt(y,S.totalCost,a))break}}catch(te){G=[te]}finally{try{qt&&(te=M.return)&&(yield te.call(M))}finally{if(G)throw G[0]}}return{postContextState:p,rawPredictions:a}})}h(_t,\\\"correctAndEnumerate\\\");function Jt(o,e,t){if(e>=o+Ot.MAX_SEARCH_THRESHOLD)return!0;if(t.length>=Z.MAX_SUGGESTIONS){if(e>=o+Ot.REPLACEMENT_SEARCH_THRESHOLD)return!0;if(t.sort(pt),t[Z.MAX_SUGGESTIONS-1].totalProb>Math.exp(-e))return!0}return!1}h(Jt,\\\"shouldStopSearchingEarly\\\");function Rt(o,e,t){let r=[],n=B(o);for(let i of e){let s=o.predict(i.sample,t),{sample:l,p:a}=i,u=n(g(i.sample,t)),c=s.map(p=>(l.id!==void 0&&(p.sample.transformId=l.id),{prediction:p,correction:{sample:u,p:a},totalProb:p.p*a,matchLevel:0}));r=r.concat(c)}return r}h(Rt,\\\"predictFromCorrections\\\");function Dt(o,e,t){let r=B(o),n={},i=[];for(let s of e){let l=r(g(s.prediction.sample.transform,t)),a=n[l];a?a.totalProb+=s.totalProb:n[l]=s}for(let s in n){let l=n[s];i.push(l)}return i}h(Dt,\\\"dedupeSuggestions\\\");function Ut(o,e,t,r){let{sample:n,p:i}=r,s=B(o),l=g(n,t),a=s(l),u=h(m=>o.toKey?o.toKey(m):m,\\\"keyed\\\"),c=h(m=>o.applyCasing?o.applyCasing(\\\"lower\\\",m):m,\\\"keyCased\\\"),p=u(a),f=c(a),d;for(let m of e){n.id!==void 0&&(m.prediction.sample.transformId=n.id);let b=s(g(m.prediction.sample.transform,t));u(m.correction.sample)==p?b==a?(m.matchLevel=3,d=Ie(o,m.prediction.sample,\\\"keep\\\",j.noQuotes),d.matchesModel=!0,Object.assign(m.prediction.sample,d),d=m.prediction.sample):c(b)==f?m.matchLevel=2:u(b)==p?m.matchLevel=1:m.matchLevel=0:m.matchLevel=0}if(d||a==\\\"\\\")return;let C=A({},n),x=W(C,1);x.displayAs=a,d=Ie(o,x,\\\"keep\\\"),n.id!==void 0&&(d.transformId=n.id),d.matchesModel=!1,e.unshift({totalProb:d.p,prediction:{sample:d,p:d.p},correction:{sample:a,p:i},matchLevel:3})}h(Ut,\\\"processSimilarity\\\");function Ft(o){if(o.length==0)return;let e=o[0].prediction.sample;if(e.tag==\\\"keep\\\"&&e.matchesModel){e.autoAccept=!0;return}else if(o.length==1)return;if(o=o.slice(1),o.length==1){o[0].prediction.sample.autoAccept=!0;return}let t=o[0];if(t.correction.sample.length==0||o.reduce((a,u)=>(a==null?void 0:a.correction.p)>u.correction.p?a:u,null).correction.p>t.correction.p)return;let i=t.matchLevel,s=o.reduce((a,u)=>a+(u.matchLevel==i?u.totalProb:0),0);t.totalProb/s{let u=a.prediction;if(a.preservationTransform){let c=U(a.preservationTransform,u.sample.transform);c.id=u.sample.transformId;let p=u.sample;p.transform=c}return n?Re(A({},u.sample),{p:a.totalProb,\\\"lexical-p\\\":u.p,\\\"correction-p\\\":a.correction.p}):Re(A({},u.sample),{p:a.totalProb})});return l.forEach(a=>{let u=s(t);u&&u.caretSplitsToken?a.transform.insert+=i.insertAfterWord:t.right?i.insertAfterWord!=\\\"\\\"&&t.right.indexOf(i.insertAfterWord)!=0&&(a.transform.insert+=i.insertAfterWord):a.transform.insert+=i.insertAfterWord}),l}h(Wt,\\\"finalizeSuggestions\\\");function Ie(o,e,t,r=j.default){let n=j,i=me(o),s=n.noQuotes;(t==\\\"keep\\\"||t==\\\"revert\\\")&&(s=n.useQuotes);let l={transform:e.transform,displayAs:n.apply(r,e.displayAs,i,s),tag:t,p:e.p};return e.transformId!==void 0&&(l.transformId=e.transformId),l}h(Ie,\\\"toAnnotatedSuggestion\\\");var J=class J{constructor(e,t){this.SUGGESTION_ID_SEED=0;this.testMode=!1;this.verbose=!0;this.lexicalModel=e,e.traverseFromRoot&&(this.contextTracker=new Te),this.punctuation=me(e),this.testMode=!!t}predict(e,t){return D(this,null,function*(){var b;let r=this.lexicalModel;(b=this.activeTimer)==null||b.terminate(),e instanceof Array?e.length==0&&e.push({sample:{insert:\\\"\\\",deleteLeft:0},p:1}):e=[{sample:e,p:1}],e.sort(function(y,L){return L.p-y.p});let n=e[0].sample,i=w.isBackspace(n),s=w.isWhitespace(n),l=g(n,t),a=this.wordbreak(l),u=i||s?a:this.wordbreak(t),c=r.languageUsesCasing?It(r,l):null,p=v.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL,f=this.activeTimer=new he(this.testMode?Number.MAX_VALUE:p,this.testMode?Number.MAX_VALUE:p*1.5),{postContextState:d,rawPredictions:C}=yield _t(this.contextTracker,this.lexicalModel,f,e,t);this.activeTimer==f&&(this.activeTimer=null);for(let y of C)c&&c!=\\\"lower\\\"&&this.applySuggestionCasing(y.prediction.sample,u,c);let x=Dt(this.lexicalModel,C,t);Ut(this.lexicalModel,x,t,e[0]),x.sort(pt),Ft(x);let m=Wt(this.lexicalModel,x.splice(0,J.MAX_SUGGESTIONS),t,n,this.verbose);return m.forEach(y=>{y.id=this.SUGGESTION_ID_SEED,this.SUGGESTION_ID_SEED++}),d&&(d.tail.replacements=m.map(function(y){return{suggestion:y,tokenWidth:1}})),m})}applySuggestionCasing(e,t,r){let n=t.kmwLength()-e.transform.deleteLeft;n>0&&(e.transform.deleteLeft+=n,e.transform.insert=t.kmwSubstr(0,n)+e.transform.insert),e.transform.insert=this.lexicalModel.applyCasing(r,e.transform.insert),e.displayAs=this.lexicalModel.applyCasing(r,e.displayAs)}acceptSuggestion(e,t,r){let n=e.transform,i=t.left.kmwSubstr(-n.deleteLeft,n.deleteLeft),s=n.insert.kmwLength(),l={insert:i,deleteLeft:s},a=t;r&&(l=U(l,r),a=g(r,a));let u,c=this.tokenize(a);if(c){let d=c.left[c.left.length-1];u=d&&!d.isWhitespace?d.text:\\\"\\\",u+=c.caretSplitsToken?c.right[0].text:\\\"\\\"}else u=this.wordbreak(a);let p=W(l);p.displayAs=u;let f=Ie(this.lexicalModel,p,\\\"revert\\\");if(e.transformId!=null&&(f.transformId=-e.transformId),e.id!=null?f.id=-e.id:(f.id=-this.SUGGESTION_ID_SEED,this.SUGGESTION_ID_SEED++),this.contextTracker){let d=this.contextTracker.newest;d||(d=this.contextTracker.analyzeState(this.lexicalModel,t).state),d.tail.activeReplacementId=e.id;let C=g(e.transform,t);this.contextTracker.analyzeState(this.lexicalModel,C)}return f}applyReversion(e,t){return D(this,null,function*(){let r=this,n=h(function(){return D(this,null,function*(){let l=g(e.transform,t),a=yield r.predict({insert:\\\"\\\",deleteLeft:0},l);return a.forEach(function(u){u.transformId=-e.transformId,u.autoAccept=!1}),a})},\\\"fallbackSuggestions\\\");if(!this.contextTracker)return n();let i=!1;for(let l=this.contextTracker.count-1;l>=0;l--)if(this.contextTracker.item(l).tail.activeReplacementId==-e.id){i=!0;break}if(!i)return n();for(;this.contextTracker.newest.tail.activeReplacementId!=-e.id;)this.contextTracker.popNewest();this.contextTracker.newest.tail.revert();let s=this.contextTracker.newest.tail.replacements.map(function(l){return l.suggestion});return s.forEach(function(l){l.transformId=-e.transformId,l.autoAccept=!1}),s})}wordbreak(e){return B(this.lexicalModel)(e)}tokenize(e){return $(this.lexicalModel)(e)}resetContext(e){var t;(t=this.activeTimer)==null||t.terminate(),this.contextTracker&&(this.contextTracker.clearCache(),this.contextTracker.analyzeState(this.lexicalModel,e,null))}};h(J,\\\"ModelCompositor\\\"),J.MAX_SUGGESTIONS=12,J.SINGLE_CHAR_KEY_PROB_EXPONENT=16;var dt=J,Z=dt;O();var Ne=class Ne{constructor(e={importScripts:null,postMessage:null}){this._testMode=!1;this._postMessage=e.postMessage||postMessage,this._importScripts=e.importScripts||importScripts,this.setupConfigState()}error(e,t){this.cast(\\\"error\\\",{log:e,error:t&&t.stack?t.stack:void 0})}onMessage(e){let{message:t}=e.data;if(!t)throw new Error(`Missing required 'message' property: ${e.data}`);let r=e.data;if(r.message==\\\"load\\\"){let n=r,i=!1;if(this._currentModelSource&&n.source.type==this._currentModelSource.type&&(n.source.type==\\\"file\\\"&&n.source.file==this._currentModelSource.file||n.source.type==\\\"raw\\\"&&n.source.code==this._currentModelSource.code)&&(i=!0),i){typeof console!=\\\"undefined\\\"&&console.warn(\\\"Duplicate model load message detected - squashing!\\\");return}else this._currentModelSource=n.source}else r.message==\\\"unload\\\"&&(this._currentModelSource=null);this.state.handleMessage(r)}cast(e,t){let r=this._postMessage;r(A({message:e},t))}loadModel(e){try{let t=e.configure(this._platformCapabilities);t.leftContextCodePoints||(t.leftContextCodePoints=t.leftContextCodeUnits),t.rightContextCodePoints||(t.rightContextCodePoints=t.rightContextCodeUnits),t.leftContextCodePoints||(t.leftContextCodePoints=this._platformCapabilities.maxLeftContextCodePoints),t.rightContextCodePoints||(t.rightContextCodePoints=this._platformCapabilities.maxRightContextCodePoints||0),e.languageUsesCasing&&!e.applyCasing&&(e.applyCasing=we);let r=this.transitionToReadyState(e);t.wordbreaksAfterSuggestions===void 0&&(t.wordbreaksAfterSuggestions=r.punctuation.insertAfterWord!=\\\"\\\"),this.cast(\\\"ready\\\",{configuration:t})}catch(t){this.error(\\\"loadModel failed!\\\",t)}}loadModelFile(e){try{this._importScripts(e)}catch(t){this.error(\\\"Error occurred when attempting to load dictionary\\\",t)}}unloadModel(){this.transitionToLoadingState()}setupConfigState(){this.state={name:\\\"unconfigured\\\",handleMessage:e=>{if(e.message!==\\\"config\\\")throw new Error(`invalid message; expected 'config' but got ${e.message}`);this._platformCapabilities=e.capabilities,this._testMode=!!e.testMode,this.transitionToLoadingState()}}}transitionToLoadingState(){let e=this;this.state={name:\\\"modelless\\\",handleMessage:t=>{if(t.message!==\\\"load\\\")throw new Error(`invalid message; expected 'load' but got ${t.message}`);if(t.source.type==\\\"file\\\")e.loadModelFile(t.source.file);else{let r=t.source.code;new Function(\\\"LMLayerWorker\\\",\\\"models\\\",\\\"correction\\\",\\\"wordBreakers\\\",r)(e,ve,Ce,le)}}}}transitionToReadyState(e){let t=new Z(e,this._testMode);return this.state={name:\\\"ready\\\",handleMessage:r=>{switch(r.message){case\\\"predict\\\":var{transform:n,context:a}=r;t.predict(n,a).then(c=>{this.cast(\\\"suggestions\\\",{token:r.token,suggestions:c})});break;case\\\"wordbreak\\\":let u=X(e.wordbreaker||R,r.context);this.cast(\\\"currentword\\\",{token:r.token,word:u});break;case\\\"unload\\\":this.unloadModel();break;case\\\"accept\\\":var{suggestion:i,context:a,postTransform:s}=r,l=t.acceptSuggestion(i,a,s);this.cast(\\\"postaccept\\\",{token:r.token,reversion:l});break;case\\\"revert\\\":var{reversion:l,context:a}=r;t.applyReversion(l,a).then(c=>{this.cast(\\\"postrevert\\\",{token:r.token,suggestions:c})});break;case\\\"reset-context\\\":var{context:a}=r;t.resetContext(a);break;default:throw new Error(`invalid message; expected one of {'predict', 'wordbreak', 'accept', 'revert', 'reset-context', 'unload'} but got ${r.message}`)}},compositor:t},t}static install(e){let t=new Ne({postMessage:e.postMessage,importScripts:e.importScripts.bind(e)});return e.onmessage=t.onMessage.bind(t),t.self=e,e.LMLayerWorker=t,e.models=ve,e.correction=Ce,e.wordBreakers=le,t}};h(Ne,\\\"LMLayerWorker\\\");var ee=Ne;typeof self!=\\\"undefined\\\"&&\\\"postMessage\\\"in self&&\\\"importScripts\\\"in self?ee.install(self):window.LMLayerWorker=ee;})();\\n//# sourceMappingURL=worker-main.min.js.map\\n\";\n\n// Sourcemaps have been omitted for this release build.\nexport var LMLayerWorkerSourcemapComment = \"\";\n\n// --END:LMLayerWorkerCode\n", + "import unwrap from '../unwrap.js';\r\nimport { LMLayerWorkerCode, LMLayerWorkerSourcemapComment } from \"@keymanapp/lm-worker/worker-main.wrapped.min.js\";\r\n\r\nexport default class DefaultWorker {\r\n static constructInstance(): Worker {\r\n return new Worker(this.asBlobURI(LMLayerWorkerCode));\r\n }\r\n\r\n /**\r\n * Converts the INSIDE of a function into a blob URI that can\r\n * be passed as a valid URI for a Worker.\r\n * @param fn Function whose body will be referenced by a URI.\r\n *\r\n * This function makes the following possible:\r\n *\r\n * let worker = new Worker(LMLayer.asBlobURI(function myWorkerCode () {\r\n * postMessage('inside Web Worker')\r\n * function onmessage(event) {\r\n * // handle message inside Web Worker.\r\n * }\r\n * }));\r\n */\r\n static asBlobURI(encodedSrc: string): string {\r\n let code = unwrap(encodedSrc);\r\n\r\n // If this is definitively set to either true or false, tree-shaking can take effect.\r\n // An imported const variable doesn't seem to do it, though.\r\n // if(false) {\r\n code += '\\n' + LMLayerWorkerSourcemapComment;\r\n // }\r\n let blob = new Blob([code], { type: 'text/javascript' });\r\n return URL.createObjectURL(blob);\r\n }\r\n}", + "import { EventEmitter } from \"eventemitter3\";\r\nimport { LMLayer } from \"@keymanapp/lexical-model-layer/web\";\r\nimport { OutputTarget, Transcription, Mock } from \"keyman/engine/js-processor\";\r\nimport { LanguageProcessorEventMap, ModelSpec, StateChangeEnum, ReadySuggestions } from 'keyman/engine/interfaces';\r\nimport ContextWindow from \"./contextWindow.js\";\r\nimport { TranscriptionCache } from \"./transcriptionCache.js\";\r\nimport { Capabilities, Configuration, Reversion, Suggestion } from '@keymanapp/common-types';\r\n\r\n/* Is more like the model configuration engine */\r\nexport class LanguageProcessor extends EventEmitter {\r\n private lmEngine: LMLayer;\r\n private currentModel?: ModelSpec;\r\n private configuration?: Configuration;\r\n private currentPromise?: Promise;\r\n\r\n private readonly recentTranscriptions: TranscriptionCache;\r\n\r\n private _mayPredict: boolean = true;\r\n private _mayCorrect: boolean = true;\r\n\r\n private _state: StateChangeEnum = 'inactive';\r\n\r\n public constructor(predictiveTextWorker: Worker, transcriptionCache: TranscriptionCache, supportsRightDeletions: boolean = false) {\r\n super();\r\n\r\n this.recentTranscriptions = transcriptionCache;\r\n\r\n // Establishes KMW's platform 'capabilities', which limit the range of context a LMLayer\r\n // model may expect.\r\n let capabilities: Capabilities = {\r\n maxLeftContextCodePoints: 64,\r\n // Since the apps don't yet support right-deletions.\r\n maxRightContextCodePoints: supportsRightDeletions ? 0 : 64\r\n }\r\n\r\n if(!predictiveTextWorker) {\r\n return;\r\n }\r\n\r\n this.lmEngine = new LMLayer(capabilities, predictiveTextWorker);\r\n }\r\n\r\n public get activeModel(): ModelSpec {\r\n return this.currentModel;\r\n }\r\n\r\n public get isConfigured(): boolean {\r\n return !!this.configuration;\r\n }\r\n\r\n public get state(): StateChangeEnum {\r\n return this._state;\r\n }\r\n\r\n public unloadModel() {\r\n this.lmEngine.unloadModel();\r\n delete this.currentModel;\r\n delete this.configuration;\r\n\r\n this._state = 'inactive';\r\n this.emit('statechange', 'inactive');\r\n }\r\n\r\n loadModel(model: ModelSpec): Promise {\r\n if(!model) {\r\n throw new Error(\"Null reference not allowed.\");\r\n }\r\n\r\n let specType: 'file'|'raw' = model.path ? 'file' : 'raw';\r\n let source = specType == 'file' ? model.path : model.code;\r\n\r\n // We pre-emptively emit so that the banner's DOM elements may update synchronously.\r\n // Prevents an ugly \"flash of unstyled content\" layout issue during keyboard load\r\n // on our mobile platforms when embedded.\r\n this.currentModel = model;\r\n if(this.mayPredict) {\r\n this._state = 'active';\r\n this.emit('statechange', 'active');\r\n }\r\n\r\n return this.lmEngine.loadModel(source, specType).then((config: Configuration) => {\r\n this.configuration = config;\r\n if(this.mayPredict) {\r\n this._state = 'configured';\r\n this.emit('statechange', 'configured');\r\n }\r\n }).catch((error) => {\r\n // Does this provide enough logging information?\r\n let message: string;\r\n if(error instanceof Error) {\r\n message = error.message;\r\n } else {\r\n message = String(error);\r\n }\r\n console.error(\"Could not load model '\" + model.id + \"': \" + message);\r\n\r\n // Since the model couldn't load, immediately deactivate. Visually, it'll look\r\n // like the banner crashed shortly after load.\r\n this.currentModel = null;\r\n this._state = 'inactive';\r\n this.emit('statechange', 'inactive');\r\n });\r\n }\r\n\r\n public invalidateContext(outputTarget: OutputTarget, layerId: string): Promise {\r\n // Signal to any predictive text UI that the context has changed, invalidating recent predictions.\r\n this.emit('invalidatesuggestions', 'context');\r\n\r\n // If there's no active model, there can be no predictions.\r\n // We'll also be missing important data needed to even properly REQUEST the predictions.\r\n if(!this.currentModel || !this.configuration) {\r\n return Promise.resolve([]);\r\n }\r\n\r\n // Don't attempt predictions when disabled!\r\n // invalidateContext otherwise bypasses .predict()'s check against this.\r\n if(!this.isActive) {\r\n return Promise.resolve([]);\r\n } else if(outputTarget) {\r\n let transcription = outputTarget.buildTranscriptionFrom(outputTarget, null, false);\r\n return this.predict_internal(transcription, true, layerId);\r\n } else {\r\n // if there's no active context source, there's nothing to\r\n // provide suggestions for. In that case, there's no reason\r\n // to even request suggestions, so bypass the prediction\r\n // engine and say that there aren't any.\r\n return Promise.resolve([]);\r\n }\r\n }\r\n\r\n public wordbreak(target: OutputTarget, layerId: string): Promise {\r\n if(!this.isActive) {\r\n return null;\r\n }\r\n\r\n let context = new ContextWindow(Mock.from(target, false), this.configuration, layerId);\r\n return this.lmEngine.wordbreak(context);\r\n }\r\n\r\n public predict(transcription: Transcription, layerId: string): Promise {\r\n if(!this.isActive) {\r\n return null;\r\n }\r\n\r\n // If there's no active model, there can be no predictions.\r\n // We'll also be missing important data needed to even properly REQUEST the predictions.\r\n if(!this.currentModel || !this.configuration) {\r\n return null;\r\n }\r\n\r\n // We've already invalidated any suggestions resulting from any previously-existing Promise -\r\n // may as well officially invalidate them via event.\r\n this.emit(\"invalidatesuggestions\", 'new');\r\n\r\n return this.predict_internal(transcription, false, layerId);\r\n }\r\n\r\n /**\r\n *\r\n * @param suggestion\r\n * @param outputTarget\r\n * @param getLayerId a function that returns the current layerId,\r\n * required because layerid can be changed by PostKeystroke\r\n * @returns\r\n */\r\n public applySuggestion(suggestion: Suggestion, outputTarget: OutputTarget, getLayerId: ()=>string): Promise {\r\n if(!outputTarget) {\r\n throw \"Accepting suggestions requires a destination OutputTarget instance.\"\r\n }\r\n\r\n if(!this.isConfigured) {\r\n // If we're in this state, the suggestion is now outdated; the user must have swapped keyboard and model.\r\n console.warn(\"Could not apply suggestion; the corresponding model has been unloaded\");\r\n return null;\r\n }\r\n\r\n // Find the state of the context at the time the suggestion was generated.\r\n // This may refer to the context before an input keystroke or before application\r\n // of a predictive suggestion.\r\n const original = this.getPredictionState(suggestion.transformId);\r\n if(!original) {\r\n console.warn(\"Could not apply the Suggestion!\");\r\n return null;\r\n } else {\r\n // Apply the Suggestion!\r\n\r\n // Step 1: determine the final output text\r\n let final = Mock.from(original.preInput, false);\r\n final.apply(suggestion.transform);\r\n\r\n // Step 2: build a final, master Transform that will produce the desired results from the CURRENT state.\r\n // In embedded mode, both Android and iOS are best served by calculating this transform and applying its\r\n // values as needed for use with their IME interfaces.\r\n let transform = final.buildTransformFrom(outputTarget);\r\n outputTarget.apply(transform);\r\n\r\n // Tell the banner that a suggestion was applied, so it can call the\r\n // keyboard's PostKeystroke entry point as needed\r\n this.emit('suggestionapplied', outputTarget);\r\n\r\n // Build a 'reversion' Transcription that can be used to undo this apply() if needed,\r\n // replacing the suggestion transform with the original input text.\r\n let preApply = Mock.from(original.preInput, false);\r\n preApply.apply(original.transform);\r\n\r\n // Builds the reversion option according to the loaded lexical model's known\r\n // syntactic properties.\r\n let suggestionContext = new ContextWindow(original.preInput, this.configuration, getLayerId());\r\n\r\n // We must accept the Suggestion from its original context, which was before\r\n // `original.transform` was applied.\r\n let reversionPromise: Promise = this.lmEngine.acceptSuggestion(suggestion, suggestionContext, original.transform);\r\n\r\n // Also, request new prediction set based on the resulting context.\r\n reversionPromise = reversionPromise.then((reversion) => {\r\n let mappedReversion: Reversion = {\r\n // By mapping back to the original Transcription that generated the Suggestion,\r\n // the input will be automatically rewound to the preInput state.\r\n transform: original.transform,\r\n // The ID part is critical; the reversion can't be applied without it.\r\n transformId: -original.token, // reversions use the additive inverse.\r\n displayAs: reversion.displayAs, // The real reason we needed to call the LMLayer.\r\n id: reversion.id,\r\n tag: reversion.tag\r\n }\r\n // // If using the version from lm-layer:\r\n // let mappedReversion = reversion;\r\n // mappedReversion.transformId = reversionTranscription.token;\r\n this.predictFromTarget(outputTarget, getLayerId());\r\n return mappedReversion;\r\n });\r\n\r\n return reversionPromise;\r\n }\r\n }\r\n\r\n public applyReversion(reversion: Reversion, outputTarget: OutputTarget) {\r\n if(!outputTarget) {\r\n throw \"Accepting suggestions requires a destination OutputTarget instance.\"\r\n }\r\n\r\n // Find the state of the context at the time the suggestion was generated.\r\n // This may refer to the context before an input keystroke or before application\r\n // of a predictive suggestion.\r\n //\r\n // Reversions use the additive inverse of the id token of the Transcription being\r\n // reverted to.\r\n let original = this.getPredictionState(-reversion.transformId);\r\n if(!original) {\r\n console.warn(\"Could not apply the Suggestion!\");\r\n return Promise.resolve([] as Suggestion[]);\r\n }\r\n\r\n // Apply the Reversion!\r\n\r\n // Step 1: determine the final output text\r\n let final = Mock.from(original.preInput, false);\r\n final.apply(reversion.transform); // Should match original.transform, actually. (See applySuggestion)\r\n\r\n // Step 2: build a final, master Transform that will produce the desired results from the CURRENT state.\r\n // In embedded mode, both Android and iOS are best served by calculating this transform and applying its\r\n // values as needed for use with their IME interfaces.\r\n let transform = final.buildTransformFrom(outputTarget);\r\n outputTarget.apply(transform);\r\n\r\n // The reason we need to preserve the additive-inverse 'transformId' property on Reversions.\r\n let promise = this.currentPromise = this.lmEngine.revertSuggestion(reversion, new ContextWindow(original.preInput, this.configuration, null))\r\n // If the \"current Promise\" is as set above, clear it.\r\n // If another one has been triggered since... don't.\r\n promise.then(() => this.currentPromise = (this.currentPromise == promise) ? null : this.currentPromise);\r\n\r\n return promise;\r\n }\r\n\r\n public predictFromTarget(outputTarget: OutputTarget, layerId: string): Promise {\r\n if(!outputTarget) {\r\n return null;\r\n }\r\n\r\n let transcription = outputTarget.buildTranscriptionFrom(outputTarget, null, false);\r\n return this.predict(transcription, layerId);\r\n }\r\n\r\n /**\r\n * Called internally to do actual predictions after any relevant \"invalidatesuggestions\" events\r\n * have been raised.\r\n * @param transcription The triggering transcription (if it exists)\r\n */\r\n private predict_internal(transcription: Transcription, resetContext: boolean, layerId: string): Promise {\r\n if(!transcription) {\r\n return null;\r\n }\r\n\r\n let context = new ContextWindow(transcription.preInput, this.configuration, layerId);\r\n this.recordTranscription(transcription);\r\n\r\n if(resetContext) {\r\n this.lmEngine.resetContext(context);\r\n }\r\n\r\n let alternates = transcription.alternates;\r\n if(!alternates || alternates.length == 0) {\r\n alternates = [{\r\n sample: transcription.transform,\r\n p: 1.0\r\n }];\r\n }\r\n\r\n let transform = transcription.transform;\r\n var promise = this.currentPromise = this.lmEngine.predict(alternates, context);\r\n\r\n return promise.then((suggestions: Suggestion[]) => {\r\n if(promise == this.currentPromise) {\r\n let result = new ReadySuggestions(suggestions, transform.id);\r\n this.emit(\"suggestionsready\", result);\r\n this.currentPromise = null;\r\n }\r\n\r\n return suggestions;\r\n });\r\n }\r\n\r\n private recordTranscription(transcription: Transcription) {\r\n this.recentTranscriptions.save(transcription);\r\n }\r\n\r\n /**\r\n * Retrieves the context and output state of KMW immediately before the prediction with\r\n * token `id` was generated. Must correspond to a 'recent' one, as only so many are stored\r\n * in `ModelManager`'s history buffer.\r\n * @param id A unique identifier corresponding to a recent `Transcription`.\r\n * @returns The matching `Transcription`, or `null` none is found.\r\n */\r\n public getPredictionState(id: number): Transcription {\r\n return this.recentTranscriptions.get(id);\r\n }\r\n\r\n public shutdown() {\r\n this.lmEngine.shutdown();\r\n this.removeAllListeners();\r\n }\r\n\r\n public get isActive(): boolean {\r\n if(!this.canEnable()) {\r\n this._mayPredict = false;\r\n return false;\r\n }\r\n return (this.activeModel || false) && this._mayPredict;\r\n }\r\n\r\n canEnable(): boolean {\r\n // Is not initialized if there is no worker.\r\n return !!this.lmEngine;\r\n }\r\n\r\n public get mayPredict() {\r\n return this._mayPredict;\r\n }\r\n\r\n public set mayPredict(flag: boolean) {\r\n if(!this.canEnable()) {\r\n return;\r\n }\r\n\r\n let oldVal = this._mayPredict;\r\n this._mayPredict = flag;\r\n\r\n if(oldVal != flag) {\r\n // If there's no model to be activated and we've reached this point,\r\n // the banner should remain inactive, as it already was.\r\n // If it there was one and we've reached this point, we're globally\r\n // deactivating, so we're fine.\r\n if(this.activeModel) {\r\n // If someone toggles predictions on and off without changing the model, it is possible\r\n // that the model is already configured!\r\n let state: StateChangeEnum = flag ? 'active' : 'inactive';\r\n\r\n // We always signal the 'active' state here, even if 'configured', b/c of an\r\n // anti-banner-flicker optimization in the Android app.\r\n this._state = state;\r\n this.emit('statechange', state);\r\n\r\n // Only signal `'configured'` for a previously-loaded model if we're turning\r\n // things back on; don't send it if deactivated!\r\n if(flag && this.isConfigured) {\r\n this._state = 'configured';\r\n this.emit('statechange', 'configured');\r\n }\r\n }\r\n }\r\n }\r\n\r\n public get mayCorrect() {\r\n return this._mayCorrect;\r\n }\r\n\r\n public set mayCorrect(flag: boolean) {\r\n this._mayCorrect = flag;\r\n }\r\n\r\n public get wordbreaksAfterSuggestions() {\r\n return this.configuration.wordbreaksAfterSuggestions;\r\n }\r\n\r\n public tryAcceptSuggestion(source: string): boolean {\r\n // The object below is to facilitate a pass-by-reference on the boolean flag,\r\n // allowing the event's handler to signal if whitespace has been added via\r\n // auto-applied suggestion that should be blocked on the next keystroke.\r\n let returnObj = {shouldSwallow: false};\r\n this.emit('tryaccept', source, returnObj);\r\n\r\n return returnObj.shouldSwallow ?? false;\r\n }\r\n\r\n public tryRevertSuggestion(): boolean {\r\n // If and when we do auto-revert, the suggestion is to pass this object to the event and\r\n // denote any mutations to the contained value.\r\n //let returnObj = {shouldSwallow: false};\r\n this.emit('tryrevert');\r\n\r\n return false;\r\n }\r\n}\r\n", + "import { Transcription } from \"keyman/engine/js-processor\";\r\n\r\nconst TRANSCRIPTION_BUFFER_SIZE = 10;\r\n\r\nexport class TranscriptionCache {\r\n private readonly map = new Map();\r\n\r\n public get(key: number) {\r\n const value = this.map.get(key);\r\n\r\n // Update the entry's 'age' / position in the keys() ordering.\r\n if(value) {\r\n this.save(value);\r\n }\r\n\r\n return value;\r\n }\r\n\r\n public save(value: Transcription) {\r\n const key = value.token >= 0 ? value.token : -value.token;\r\n\r\n // Resets the key's ordering in Map.keys.\r\n this.map.delete(key);\r\n this.map.set(key, value);\r\n\r\n if(this.map.size > TRANSCRIPTION_BUFFER_SIZE) {\r\n /* Deletes the oldest entry. As per the specification of `Map.keys()`, the keys are in\r\n * insertion order. The earlier `map.delete` call resets a key's position in the list,\r\n * ensuring index 0 corresponds to the entry least-recently referenced.\r\n *\r\n * See also:\r\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys\r\n */\r\n this.map.delete(this.map.keys().next().value);\r\n }\r\n }\r\n}", + "// Defines a 'polyfill' of sorts for NPM's events module\r\n\r\nimport ContextWindow from \"./contextWindow.js\";\r\nimport { LanguageProcessor } from \"./languageProcessor.js\";\r\nimport type { ModelSpec } from \"keyman/engine/interfaces\";\r\nimport { globalObject, DeviceSpec } from \"@keymanapp/web-utils\";\r\n\r\nimport { Codes, type Keyboard, type KeyEvent } from \"keyman/engine/keyboard\";\r\nimport {\r\n type Alternate,\r\n isEmptyTransform,\r\n KeyboardInterface,\r\n KeyboardProcessor,\r\n Mock,\r\n type OutputTarget,\r\n RuleBehavior,\r\n type ProcessorInitOptions,\r\n SystemStoreIDs\r\n} from 'keyman/engine/js-processor';\r\n\r\nimport { TranscriptionCache } from \"./transcriptionCache.js\";\r\nimport { Transform } from '@keymanapp/common-types';\r\n\r\nexport class InputProcessor {\r\n public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = {\r\n baseLayout: 'us'\r\n }\r\n\r\n /**\r\n * Indicates the device (platform) to be used for non-keystroke events,\r\n * such as those sent to `begin postkeystroke` and `begin newcontext`\r\n * entry points.\r\n */\r\n private contextDevice: DeviceSpec;\r\n private kbdProcessor: KeyboardProcessor;\r\n private lngProcessor: LanguageProcessor;\r\n\r\n private readonly contextCache = new TranscriptionCache();\r\n\r\n constructor(device: DeviceSpec, predictiveTextWorker: Worker, options?: ProcessorInitOptions) {\r\n if(!device) {\r\n throw new Error('device must be defined');\r\n }\r\n\r\n if(!options) {\r\n options = InputProcessor.DEFAULT_OPTIONS;\r\n }\r\n\r\n this.contextDevice = device;\r\n this.kbdProcessor = new KeyboardProcessor(device, options);\r\n this.lngProcessor = new LanguageProcessor(predictiveTextWorker, this.contextCache);\r\n }\r\n\r\n public get languageProcessor(): LanguageProcessor {\r\n return this.lngProcessor;\r\n }\r\n\r\n public get keyboardProcessor(): KeyboardProcessor {\r\n return this.kbdProcessor;\r\n }\r\n\r\n public get keyboardInterface(): KeyboardInterface {\r\n return this.keyboardProcessor.keyboardInterface;\r\n }\r\n\r\n public get activeKeyboard(): Keyboard {\r\n return this.keyboardInterface.activeKeyboard;\r\n }\r\n\r\n public set activeKeyboard(keyboard: Keyboard) {\r\n this.keyboardInterface.activeKeyboard = keyboard;\r\n\r\n // All old deadkeys and keyboard-specific cache should immediately be invalidated\r\n // on a keyboard change.\r\n this.resetContext();\r\n }\r\n\r\n public get activeModel(): ModelSpec {\r\n return this.languageProcessor.activeModel;\r\n }\r\n\r\n /**\r\n * Simulate a keystroke according to the touched keyboard button element\r\n *\r\n * Handles default output and keyboard processing for both OSK and physical keystrokes.\r\n *\r\n * @param {Object} keyEvent The abstracted KeyEvent to use for keystroke processing\r\n * @param {Object} outputTarget The OutputTarget receiving the KeyEvent\r\n * @returns {Object} A RuleBehavior object describing the cumulative effects of\r\n * all matched keyboard rules.\r\n */\r\n processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior {\r\n const kbdMismatch = keyEvent.srcKeyboard && this.activeKeyboard != keyEvent.srcKeyboard;\r\n const trueActiveKeyboard = this.activeKeyboard;\r\n\r\n try {\r\n if(kbdMismatch) {\r\n // Avoid force-reset of context per our setter above.\r\n this.keyboardInterface.activeKeyboard = keyEvent.srcKeyboard;\r\n }\r\n\r\n // Support for multitap context reversion; multitap keys should act as if they were\r\n // the first thing typed since `preInput`, the state before the original base key.\r\n if(keyEvent.baseTranscriptionToken) {\r\n const transcription = this.contextCache.get(keyEvent.baseTranscriptionToken);\r\n if(transcription) {\r\n // Has there been a context change at any point during the multitap? If so, we need\r\n // to revert it. If not, we assume it's a layer-change multitap, in which case\r\n // no such reset is needed.\r\n if(!isEmptyTransform(transcription.transform) || !transcription.preInput.isEqual(Mock.from(outputTarget))) {\r\n // Restores full context, including deadkeys in their exact pre-keystroke state.\r\n outputTarget.restoreTo(transcription.preInput);\r\n }\r\n /*\r\n else:\r\n 1. We don't need to restore the original context, as it's already\r\n in-place.\r\n 2. Restoring anyway would obliterate any selected text, which is bad\r\n if this is a purely-layer-switching multitap. (#11230)\r\n */\r\n } else {\r\n console.warn('The base context for the multitap could not be found');\r\n }\r\n }\r\n\r\n return this._processKeyEvent(keyEvent, outputTarget);\r\n } finally {\r\n if(kbdMismatch) {\r\n // Restore our \"current\" activeKeyboard to its setting before the mismatching KeyEvent.\r\n this.keyboardInterface.activeKeyboard = trueActiveKeyboard;\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Acts as the core of `processKeyEvent` once we're comfortable asserting that the incoming\r\n * keystroke matches the current `activeKeyboard`.\r\n * @param keyEvent\r\n * @param outputTarget\r\n * @returns\r\n */\r\n private _processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior {\r\n let formFactor = keyEvent.device.formFactor;\r\n let fromOSK = keyEvent.isSynthetic;\r\n\r\n // The default OSK layout for desktop devices does not include nextlayer info, relying on modifier detection here.\r\n // It's the OSK equivalent to doModifierPress on 'desktop' form factors.\r\n if((formFactor == DeviceSpec.FormFactor.Desktop || !this.activeKeyboard || this.activeKeyboard.usesDesktopLayoutOnDevice(keyEvent.device)) && fromOSK) {\r\n // If it's a desktop OSK style and this triggers a layer change,\r\n // a modifier key was clicked. No output expected, so it's safe to instantly exit.\r\n if(this.keyboardProcessor.selectLayer(keyEvent)) {\r\n return new RuleBehavior();\r\n }\r\n }\r\n\r\n // Will handle keystroke-based non-layer change modifier & state keys, mapping them through the physical keyboard's version\r\n // of state management. `doModifierPress` must always run.\r\n if(this.keyboardProcessor.doModifierPress(keyEvent, outputTarget, !fromOSK)) {\r\n // If run on a desktop platform, we know that modifier & state key presses may not\r\n // produce output, so we may make an immediate return safely.\r\n if(!fromOSK) {\r\n return new RuleBehavior();\r\n }\r\n }\r\n\r\n // If suggestions exist AND space is pressed, accept the suggestion and do not process the keystroke.\r\n // If a suggestion was just accepted AND backspace is pressed, revert the change and do not process the backspace.\r\n // We check the first condition here, while the prediction UI handles the second through the try__() methods below.\r\n if(this.languageProcessor.isActive) {\r\n // The following code relies on JS's logical operator \"short-circuit\" properties to prevent unwanted triggering of the second condition.\r\n\r\n // Can the suggestion UI revert a recent suggestion? If so, do that and swallow the backspace.\r\n if((keyEvent.kName == \"K_BKSP\" || keyEvent.Lcode == Codes.keyCodes[\"K_BKSP\"]) && this.languageProcessor.tryRevertSuggestion()) {\r\n return new RuleBehavior();\r\n // Can the suggestion UI accept an existing suggestion? If so, do that and swallow the space character.\r\n } else if((keyEvent.kName == \"K_SPACE\" || keyEvent.Lcode == Codes.keyCodes[\"K_SPACE\"]) && this.languageProcessor.tryAcceptSuggestion('space')) {\r\n return new RuleBehavior();\r\n }\r\n }\r\n\r\n // // ...end I3363 (Build 301)\r\n\r\n // Create a \"mock\" backup of the current outputTarget in its pre-input state.\r\n // Current, long-existing assumption - it's DOM-backed.\r\n let preInputMock = Mock.from(outputTarget, true);\r\n\r\n const startingLayerId = this.keyboardProcessor.layerId;\r\n\r\n // We presently need the true keystroke to run on the FULL context. That index is still\r\n // needed for some indexing operations when comparing two different output targets.\r\n let ruleBehavior = this.keyboardProcessor.processKeystroke(keyEvent, outputTarget);\r\n\r\n // Swap layer as appropriate.\r\n if(keyEvent.kNextLayer) {\r\n this.keyboardProcessor.selectLayer(keyEvent);\r\n }\r\n\r\n // If it's a key that we 'optimize out' of our fat-finger correction algorithm,\r\n // we MUST NOT trigger it for this keystroke.\r\n let isOnlyLayerSwitchKey = Codes.isFrameKey(keyEvent.kName);\r\n\r\n // Best-guess stopgap for possible custom modifier keys.\r\n // If a key (1) does not affect the context and (2) shifts the active layer,\r\n // we assume it's a modifier key. (Touch keyboards may define custom modifier keys.)\r\n //\r\n // Note: this will mean we won't generate alternates in the niche scenario where:\r\n // 1. Keypress does not alter the actual context\r\n // 2. It DOES emit a deadkey with an earlier processing rule.\r\n // 3. The FINAL processing rule does not match.\r\n // 4. The key ALSO signals a layer shift.\r\n // If any of the four above conditions aren't met - no problem!\r\n // So it's a pretty niche scenario.\r\n\r\n if(isEmptyTransform(ruleBehavior?.transcription?.transform) && keyEvent.kNextLayer) {\r\n isOnlyLayerSwitchKey = true;\r\n }\r\n\r\n const keepRuleBehavior = ruleBehavior != null;\r\n // Should we swallow any further processing of keystroke events for this keydown-keypress sequence?\r\n if(keepRuleBehavior) {\r\n // alternates are our fat-finger alternate outputs. We don't build these for keys we detect as\r\n // layer switch keys\r\n let alternates = isOnlyLayerSwitchKey ? null : this.buildAlternates(ruleBehavior, keyEvent, preInputMock);\r\n\r\n // Now that we've done all the keystroke processing needed, ensure any extra effects triggered\r\n // by the actual keystroke occur.\r\n ruleBehavior.finalize(this.keyboardProcessor, outputTarget, false);\r\n\r\n // -- All keystroke (and 'alternate') processing is now complete. Time to finalize everything! --\r\n\r\n // Notify the ModelManager of new input - it's predictive text time!\r\n if(alternates && alternates.length > 0) {\r\n ruleBehavior.transcription.alternates = alternates;\r\n }\r\n } else {\r\n // We need a dummy RuleBehavior for keys which have no output (e.g. Shift)\r\n ruleBehavior = new RuleBehavior();\r\n ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(outputTarget, null, false);\r\n ruleBehavior.triggersDefaultCommand = true;\r\n }\r\n\r\n // Multitaps operate in part by referencing 'committed' Transcriptions to rewind\r\n // the context as necessary.\r\n this.contextCache.save(ruleBehavior.transcription);\r\n\r\n // The keyboard may want to take an action after all other keystroke processing is\r\n // finished, for example to switch layers. This action may not have any output\r\n // but may change system store or variable store values. Given this, we don't need to\r\n // save anything about the post behavior, after finalizing it\r\n\r\n // We need to tell the keyboard if the layer has been changed, either by a keyboard rule itself,\r\n // or by the touch layout 'nextlayer' control.\r\n const hasLayerChanged = ruleBehavior.setStore[SystemStoreIDs.TSS_LAYER] || keyEvent.kNextLayer;\r\n this.keyboardProcessor.newLayerStore.set(hasLayerChanged ? this.keyboardProcessor.layerId : '');\r\n this.keyboardProcessor.oldLayerStore.set(hasLayerChanged ? startingLayerId : '');\r\n\r\n let postRuleBehavior = this.keyboardProcessor.processPostKeystroke(this.contextDevice, outputTarget);\r\n if(postRuleBehavior) {\r\n postRuleBehavior.finalize(this.keyboardProcessor, outputTarget, true);\r\n }\r\n\r\n // Yes, even for ruleBehavior.triggersDefaultCommand. Those tend to change the context.\r\n ruleBehavior.predictionPromise = this.languageProcessor.predict(ruleBehavior.transcription, this.keyboardProcessor.layerId);\r\n\r\n // Text did not change (thus, no text \"input\") if we tabbed or merely moved the caret.\r\n if(!ruleBehavior.triggersDefaultCommand) {\r\n // For DOM-aware targets, this will trigger a DOM event page designers may listen for.\r\n outputTarget.doInputEvent();\r\n }\r\n\r\n return keepRuleBehavior ? ruleBehavior : null;\r\n }\r\n\r\n private buildAlternates(ruleBehavior: RuleBehavior, keyEvent: KeyEvent, preInputMock: Mock): Alternate[] {\r\n let alternates: Alternate[];\r\n\r\n // If we're performing a 'default command', it's not a standard 'typing' event - don't do fat-finger stuff.\r\n // Also, don't do fat-finger stuff if predictive text isn't enabled.\r\n if(this.languageProcessor.isActive && !ruleBehavior.triggersDefaultCommand) {\r\n let keyDistribution = keyEvent.keyDistribution;\r\n\r\n // We don't need to track absolute indexing during alternate-generation;\r\n // only position-relative, so it's better to use a sliding window for context\r\n // when making alternates. (Slightly worse for short text, matters greatly\r\n // for long text.)\r\n let contextWindow = new ContextWindow(preInputMock, ContextWindow.ENGINE_RULE_WINDOW, this.keyboardProcessor.layerId);\r\n let windowedMock = contextWindow.toMock();\r\n\r\n // Note - we don't yet do fat-fingering with longpress keys.\r\n if(this.languageProcessor.isActive && keyDistribution && keyEvent.kbdLayer) {\r\n // Tracks a 'deadline' for fat-finger ops, just in case both context is long enough\r\n // and device is slow enough that the calculation takes too long.\r\n //\r\n // Consider use of https://developer.mozilla.org/en-US/docs/Web/API/Performance/now instead?\r\n // Would allow finer-tuned control.\r\n let TIMEOUT_THRESHOLD: number = Number.MAX_VALUE;\r\n let _globalThis = globalObject();\r\n let timer: () => number;\r\n\r\n // Available by default on `window` in browsers, but _not_ on `global` in Node,\r\n // surprisingly. Since we can't use code dependent on `require` statements\r\n // at present, we have to condition upon it actually existing.\r\n if(_globalThis['performance'] && _globalThis['performance']['now']) {\r\n timer = function() {\r\n return _globalThis['performance']['now']();\r\n };\r\n\r\n TIMEOUT_THRESHOLD = timer() + 16; // + 16ms.\r\n } // else {\r\n // We _could_ just use Date.now() as a backup... but that (probably) only matters\r\n // when unit testing. So... we actually don't _need_ time thresholding when in\r\n // a Node environment.\r\n // }\r\n\r\n // Tracks a minimum probability for keystroke probability. Anything less will not be\r\n // included in alternate calculations.\r\n //\r\n // Seek to match SearchSpace.EDIT_DISTANCE_COST_SCALE from the predictive-text engine.\r\n // Reasoning for the selected value may be seen there. Short version - keystrokes\r\n // that _appear_ very precise may otherwise not even consider directly-neighboring keys.\r\n let KEYSTROKE_EPSILON = Math.exp(-5);\r\n\r\n // Sort the distribution into probability-descending order.\r\n keyDistribution.sort((a, b) => b.p - a.p);\r\n\r\n alternates = [];\r\n\r\n let totalMass = 0; // Tracks sum of non-error probabilities.\r\n for(let pair of keyDistribution) {\r\n if(pair.p < KEYSTROKE_EPSILON) {\r\n totalMass += pair.p;\r\n break;\r\n } else if(timer && timer() >= TIMEOUT_THRESHOLD) {\r\n // Note: it's always possible that the thread _executing_ our JS\r\n // got paused by the OS, even if JS itself is single-threaded.\r\n //\r\n // The case where `alternates` is initialized (line 167) but empty\r\n // (because of net-zero loop iterations) MUST be handled.\r\n break;\r\n }\r\n\r\n let mock = Mock.from(windowedMock, false);\r\n\r\n const altKey = pair.keySpec;\r\n if(!altKey) {\r\n console.warn(\"Internal error: failed to properly filter set of keys for corrections\");\r\n continue;\r\n }\r\n\r\n let altEvent = this.keyboardProcessor.activeKeyboard.constructKeyEvent(altKey, keyEvent.device, this.keyboardProcessor.stateKeys);\r\n let alternateBehavior = this.keyboardProcessor.processKeystroke(altEvent, mock);\r\n\r\n // If alternateBehavior.beep == true, ignore it. It's a disallowed key sequence,\r\n // so we expect users to never intend their use.\r\n //\r\n // Also possible that this set of conditions fail for all evaluated alternates.\r\n if(alternateBehavior && !alternateBehavior.beep && pair.p > 0) {\r\n let transform: Transform = alternateBehavior.transcription.transform;\r\n\r\n // Ensure that the alternate's token id matches that of the current keystroke, as we only\r\n // record the matched rule's context (since they match)\r\n transform.id = ruleBehavior.transcription.token;\r\n alternates.push({sample: transform, 'p': pair.p});\r\n totalMass += pair.p;\r\n }\r\n }\r\n\r\n // Renormalizes the distribution, as any error (beep) results\r\n // will result in a distribution that doesn't sum to 1 otherwise.\r\n // All `.p` values are strictly positive, so totalMass is\r\n // guaranteed to be > 0 if the array has entries.\r\n alternates.forEach(function(alt) {\r\n alt.p /= totalMass;\r\n });\r\n }\r\n }\r\n return alternates;\r\n }\r\n\r\n public resetContext(outputTarget?: OutputTarget) {\r\n // Also handles new-context events, which may modify the layer\r\n this.keyboardProcessor.resetContext(outputTarget);\r\n // With the layer now set, we trigger new predictions.\r\n this.languageProcessor.invalidateContext(outputTarget, this.keyboardProcessor.layerId);\r\n }\r\n}", + "class DomEventTracking {\r\n Pelem: EventTarget;\r\n Peventname: string;\r\n Phandler: (arg0: Object) => boolean;\r\n PuseCapture?: boolean\r\n\r\n constructor(Pelem: EventTarget, Peventname: string, Phandler: (arg0: Object) => boolean, PuseCapture?: boolean) {\r\n this.Pelem = Pelem;\r\n this.Peventname = Peventname.toLowerCase();\r\n this.Phandler = Phandler;\r\n this.PuseCapture = PuseCapture;\r\n }\r\n\r\n equals(other: DomEventTracking): boolean {\r\n return this.Pelem == other.Pelem && this.Peventname == other.Peventname &&\r\n this.Phandler == other.Phandler && this.PuseCapture == other.PuseCapture;\r\n }\r\n};\r\n\r\n/**\r\n * Facilitates adding and removing event listeners to and from DOM elements in a manner\r\n * that allows widespread removal/cleanup of the listeners at a future time if and when needed.\r\n *\r\n * Said \"widespread removal\" helps to prevent separate instances of KeymanWeb from stomping on\r\n * each other during unit tests.\r\n */\r\nexport class DomEventTracker {\r\n private domEvents: DomEventTracking[] = [];\r\n\r\n /**\r\n * Function attachDOMEvent: Note for most browsers, adds an event to a chain, doesn't stop existing events\r\n * Scope Public\r\n * @param {Object} Pelem Element (or IFrame-internal Document) to which event is being attached\r\n * @param {string} Peventname Name of event without 'on' prefix\r\n * @param {function(Object)} Phandler Event handler for event\r\n * @param {boolean=} PuseCapture True only if event to be handled on way to target element\r\n * Description Attaches event handler to element DOM event\r\n */\r\n attachDOMEvent(\r\n Pelem: Window,\r\n Peventname: K,\r\n Phandler: (ev: WindowEventMap[K]) => any,\r\n PuseCapture?: boolean\r\n ): void;\r\n attachDOMEvent(\r\n Pelem: Document,\r\n Peventname: K,\r\n Phandler: (ev: DocumentEventMap[K]) => any,\r\n PuseCapture?: boolean\r\n ): void;\r\n attachDOMEvent(\r\n Pelem: HTMLElement,\r\n Peventname: K,\r\n Phandler: (ev: HTMLElementEventMap[K]) => any,\r\n PuseCapture?: boolean\r\n ): void;\r\n attachDOMEvent(Pelem: EventTarget, Peventname: string, Phandler: (arg0: Object) => boolean, PuseCapture?: boolean): void {\r\n // @ts-ignore // Since the trickery unfortunately don't also clear things up for anything we call within.\r\n // It's possible to fix, but that gets way more complex to spec out completely.\r\n this.detachDOMEvent(Pelem, Peventname, Phandler, PuseCapture);\r\n Pelem.addEventListener(Peventname, Phandler, PuseCapture ? true : false);\r\n\r\n // Since we're attaching to the DOM, these events should be tracked for detachment during shutdown.\r\n var event = new DomEventTracking(Pelem, Peventname, Phandler, PuseCapture);\r\n this.domEvents.push(event);\r\n }\r\n\r\n /**\r\n * Function detachDOMEvent\r\n * Scope Public\r\n * @param {Object} Pelem Element from which event is being detached\r\n * @param {string} Peventname Name of event without 'on' prefix\r\n * @param {function(Object)} Phandler Event handler for event\r\n * @param {boolean=} PuseCapture True if event was being handled on way to target element\r\n * Description Detaches event handler from element [to prevent memory leaks]\r\n */\r\n detachDOMEvent(\r\n Pelem: Window,\r\n Peventname: K,\r\n Phandler: (ev: WindowEventMap[K]) => any,\r\n PuseCapture?: boolean\r\n ): void;\r\n detachDOMEvent(\r\n Pelem: Document,\r\n Peventname: K,\r\n Phandler: (ev: DocumentEventMap[K]) => any,\r\n PuseCapture?: boolean\r\n ): void;\r\n detachDOMEvent(\r\n Pelem: HTMLElement,\r\n Peventname: K,\r\n Phandler: (ev: HTMLElementEventMap[K]) => any,\r\n PuseCapture?: boolean\r\n ): void;\r\n detachDOMEvent(Pelem: EventTarget, Peventname: string, Phandler: (arg0: Object) => boolean, PuseCapture?: boolean): void {\r\n Pelem.removeEventListener(Peventname, Phandler, PuseCapture);\r\n\r\n // Since we're detaching, we should drop the tracking data from the old event.\r\n var event = new DomEventTracking(Pelem, Peventname, Phandler, PuseCapture);\r\n for(var i = 0; i < this.domEvents.length; i++) {\r\n if(this.domEvents[i].equals(event)) {\r\n this.domEvents.splice(i, 1);\r\n break;\r\n }\r\n }\r\n }\r\n\r\n shutdown() {\r\n // Remove all events linking to elements of the original, unaltered page.\r\n // This should sever any still-existing page ties to this instance of KMW,\r\n // allowing browser GC to do its thing.\r\n for(let event of this.domEvents) {\r\n // @ts-ignore // since it's simpler this way and doesn't earn us much to re-check types.\r\n this.detachDOMEvent(event.Pelem, event.Peventname, event.Phandler, event.PuseCapture);\r\n }\r\n }\r\n}", + "import { EventEmitter } from \"eventemitter3\";\r\nimport EventNames = EventEmitter.EventNames;\r\nimport EventListener = EventEmitter.EventListener;\r\n\r\nimport { LegacyEventEmitter } from \"./legacyEventEmitter.js\";\r\n\r\ninterface EventMap {\r\n // Provides IntelliSense suggestions in conditionals based on the parameters!\r\n\r\n /**\r\n * Indicates that a listener for the named event has been registered for the\r\n * EventEmitter being spied upon.\r\n * @param eventName\r\n */\r\n listeneradded(eventName: EventNames): void;\r\n\r\n /**\r\n * Indicates that a listener for the named event has been unregistered from the\r\n * EventEmitter being spied upon.\r\n * @param eventName\r\n */\r\n listenerremoved(eventName: EventNames): void;\r\n}\r\n\r\ntype Emitter = EventEmitter | LegacyEventEmitter;\r\n\r\n/**\r\n * A spy-object that wraps event-emitters in order to listen in on listener addition methods and\r\n * raise events when new listeners are attached.\r\n */\r\nexport class EmitterListenerSpy extends EventEmitter> {\r\n constructor(emitter: Emitter) {\r\n super();\r\n\r\n if(emitter instanceof EventEmitter) {\r\n emitter.on = this.listenerRegistrationSpy('listeneradded', emitter, emitter.on);\r\n emitter.addListener = this.listenerRegistrationSpy('listeneradded', emitter, emitter.addListener);\r\n emitter.off = this.listenerRegistrationSpy('listenerremoved', emitter, emitter.off);\r\n emitter.removeListener = this.listenerRegistrationSpy('listenerremoved', emitter, emitter.off);\r\n } else {\r\n // TS gets really fussy about how the legacy event typing is a bit more\r\n // restrictive (due to less-restricted event name types in EventEmitter)\r\n // It's not worth the effort to make this 100% perfect at the moment.\r\n //\r\n // @ts-ignore\r\n emitter.addEventListener = this.listenerRegistrationSpy('listeneradded', emitter, emitter.addEventListener);\r\n // @ts-ignore\r\n emitter.removeEventListener = this.listenerRegistrationSpy('listenerremoved', emitter, emitter.removeEventListener);\r\n }\r\n }\r\n\r\n /**\r\n * Given an event emitter and one of its methods used to register or unregister associated events,\r\n * this method will construct a replacement method that calls the original AND raises the specified\r\n * corresponding listener event provided by this class afterward. The replacement method\r\n * should be assigned to the emitter afterward, overwriting the original version.\r\n *\r\n * Refer to https://stackoverflow.com/a/10057969.\r\n */\r\n private listenerRegistrationSpy(\r\n spyEventName: EventNames>,\r\n emitter: Emitter,\r\n method: (\r\n eventName: EventNames,\r\n listener: EventListener>,\r\n ) => any\r\n ): ( // returns a method of the same signature as the original implementation.\r\n eventName: EventNames,\r\n listener: EventListener>,\r\n ) => any {\r\n return (eventName, listener) => {\r\n const retVal = method.apply(emitter, [eventName, listener]);\r\n this.emit(spyEventName, eventName);\r\n return retVal;\r\n }\r\n }\r\n}\r\n\r\n/**** A code block for verifying that typing, etc checks out: ****/\r\n\r\n// interface TestMap {\r\n// 'a': (str: string) => void;\r\n// 'b': (num: number, str: string) => void;\r\n// }\r\n\r\n// const emitter = new LegacyEventEmitter; // or `new EventEmitter`.\r\n// const emitterSpy = new EmitterListenerSpy(emitter);\r\n// emitterSpy.on('listeneradded', (eventName) => {\r\n// // eventName = 'c'; // will error; there is no event 'c' in the event map.\r\n// if(eventName == 'a') {\r\n// // stuff\r\n// }\r\n// })", + "// Most of the typing below is derived from that of EventEmitter, but customized for\r\n// modeling legacy KMW events. Including the heavy typing gets us event Intellisense\r\n// and compile-time errors if and when types don't match expectations.\r\n\r\n/**\r\n * Can define the set of events as follows:\r\n * ```\r\n * interface Test extends EventMap {\r\n * 'event': (param: {'prop': any}) => boolean;\r\n * }\r\n * ```\r\n *\r\n * Each event may have either no parameters or a single parameter of type object.\r\n * The type definition of the parameter will be utilized by TS's type-inference engine\r\n * for type checking on handlers and for raising the event.\r\n *\r\n * Note: the `extends EventMap` part is actually important for TS type inference here.\r\n */\r\nexport type LegacyEventMap = object;\r\n\r\n/**\r\n * Matches the name of any single event defined within the specified event-map definition.\r\n */\r\nexport type EventNames = Exclude;\r\n\r\n/**\r\n * Builds a type-array of the arguments for each named event, indexed by that name.\r\n */\r\ntype ArgumentMap = {\r\n [K in Exclude]: T[K] extends (arg: any) => void\r\n ? Parameters[0]\r\n : (\r\n T[K] extends Function\r\n ? never\r\n : T[K]\r\n );\r\n};\r\n\r\n/**\r\n * Provides the type signature of event listeners able to handle defined events.\r\n */\r\nexport type EventListener<\r\n T extends LegacyEventMap,\r\n K extends EventNames\r\n > = ( // argumentMap[eventName] - retrieves the specific parameter typing for the event.\r\n args: ArgumentMap[Extract]\r\n ) => any;\r\n\r\n/**\r\n * Provides fairly strong typing for all legacy KMW events. Note that all events\r\n * assume a handler receiving up to one object, though that object's properties will\r\n * vary from event to event.\r\n *\r\n * Note that the behavior differs from EventEmitter events on a few points:\r\n * 1. Event functions are expected to return a boolean value - generally, `true`.\r\n * If 'false' or `undefined` is returned, no further listeners will receive the event.\r\n * 2. These events receive up to one parameter, always of an object type.\r\n * 3. These events proactively prevent accidental event-handler recursion. Should an event's\r\n * handler retrigger the event, the newly-triggered event will be prevented entirely.\r\n */\r\nexport class LegacyEventEmitter {\r\n // An object mapping event names to individual event lists. Maps strings to arrays.\r\n private events: { [name: string]: ((arg0: Object) => boolean)[];} = {};\r\n private currentEvents: string[] = []; // The event messaging call stack.\r\n\r\n /**\r\n * Function addEventListener\r\n * Scope Private\r\n * @param {string} event name of event prefixed by module, e.g. osk.touchmove\r\n * @param {function(Object)} func event handler\r\n * @return {boolean}\r\n * Description Add (or replace) an event listener for this component\r\n */\r\n addEventListener> (\r\n event: T,\r\n func: EventListener\r\n ): boolean {\r\n this._removeEventListener(event, func);\r\n // TS gets hung up on the type info here because we can potentially store\r\n // different types of listeners for different events.\r\n this.events[event].push(func as unknown as any);\r\n return true;\r\n }\r\n\r\n /**\r\n * Function removeEventListener\r\n * Scope Private\r\n * @param {string} event name of event prefixed by module, e.g. osk.touchmove\r\n * @param {function(Object)} func event handler\r\n * @return {boolean}\r\n * Description Remove the specified function from the listeners for this event\r\n */\r\n public removeEventListener> (\r\n event: T,\r\n func: EventListener\r\n ): boolean {\r\n return this._removeEventListener(event, func);\r\n }\r\n\r\n // Separate, in order to prevent `addEventListener` from sending 'listenerremoved' events with\r\n // EmitterListenerSpy.\r\n private _removeEventListener> (\r\n event: T,\r\n func: EventListener\r\n ): boolean {\r\n if(typeof this.events[event] == 'undefined') {\r\n this.events[event] = [];\r\n }\r\n\r\n for(var i=0; i> (\r\n event: T,\r\n params: ArgumentMap[T]\r\n ): boolean {\r\n if(typeof this.events[event] == 'undefined') {\r\n return true;\r\n }\r\n\r\n if(this.currentEvents.indexOf(event) != -1) {\r\n return false; // Avoid event messaging recursion!\r\n }\r\n\r\n this.currentEvents.push(event);\r\n\r\n for(var i=0; i, result=false;\r\n try {\r\n result=func(params as any);\r\n } catch(strExcept) {\r\n console.error(strExcept);\r\n result=false;\r\n } //don't know whether to use true or false here\r\n if(result === false) {\r\n this.currentEvents.pop();\r\n return false;\r\n }\r\n }\r\n this.currentEvents.pop();\r\n return true;\r\n }\r\n\r\n listenerCount>(event: T) {\r\n const listeners = this.events[event];\r\n return listeners ? listeners.length : 0;\r\n }\r\n\r\n shutdown() {\r\n // Remove all event-handler references rooted in KMW events.\r\n this.events = {};\r\n }\r\n}\r\n", + "import { ManagedPromise } from 'keyman/engine/keyboard';\r\nimport CloudRequesterInterface from './cloud/requesterInterface.js';\r\nimport { CLOUD_MALFORMED_OBJECT_ERR, CLOUD_TIMEOUT_ERR, CLOUD_STUB_REGISTRATION_ERR } from './cloud/queryEngine.js';\r\n\r\nexport default class DOMCloudRequester implements CloudRequesterInterface {\r\n private readonly fileLocal: boolean;\r\n\r\n constructor(fileLocal: boolean = false) {\r\n this.fileLocal = fileLocal;\r\n }\r\n\r\n request(query: string) {\r\n let promise = new ManagedPromise();\r\n\r\n // Set callback timer\r\n const timeoutID = window.setTimeout(() => {\r\n promise.reject(new Error(CLOUD_TIMEOUT_ERR));\r\n }, 10000);\r\n\r\n const tFlag='&timerid='+ timeoutID;\r\n const fullRef = query + tFlag;\r\n\r\n const Lscript: HTMLScriptElement = document.createElement('script');\r\n Lscript.onload = (event: Event) => {\r\n window.clearTimeout(timeoutID);\r\n\r\n // This case should only happen if a returned, otherwise-valid keyboard\r\n // script does not ever call `register`. Also provides default handling\r\n // should `register` fail to report results/failure correctly.\r\n if(!promise.isResolved) {\r\n promise.reject(new Error(CLOUD_STUB_REGISTRATION_ERR));\r\n }\r\n };\r\n\r\n // Note: at this time (24 May 2021), this is also happens for \"successful\"\r\n // API calls where there is no matching keyboard ID.\r\n //\r\n // The returned 'error' JSON object is sent with an HTML error code (404)\r\n // and does not call `keyman.register`. Even if it did the latter, the\r\n // 404 code would likely prevent the returned script's call.\r\n Lscript.onerror = (event: string | Event, source?: string,\r\n lineno?: number, colno?: number, error?: Error) => {\r\n window.clearTimeout(timeoutID);\r\n\r\n let msg = CLOUD_MALFORMED_OBJECT_ERR;\r\n if(error) {\r\n msg = msg + \": \" + error.message;\r\n }\r\n\r\n promise.reject(new Error(msg));\r\n }\r\n\r\n if(this.fileLocal) {\r\n Lscript.src = query;\r\n } else {\r\n Lscript.src = fullRef;\r\n }\r\n\r\n try {\r\n document.body.appendChild(Lscript);\r\n } catch(ex) {\r\n document.getElementsByTagName('head')[0].appendChild(Lscript);\r\n }\r\n\r\n promise.finally(() => {\r\n clearTimeout(timeoutID);\r\n });\r\n\r\n return {\r\n promise: promise,\r\n queryId: timeoutID\r\n };\r\n }\r\n}", + "import { type KeyEvent, type Keyboard, KeyboardKeymanGlobal } from \"keyman/engine/keyboard\";\r\nimport { OutputTarget, ProcessorInitOptions, RuleBehavior } from 'keyman/engine/js-processor';\r\nimport { DOMKeyboardLoader as KeyboardLoader } from \"keyman/engine/keyboard/dom-keyboard-loader\";\r\nimport { InputProcessor } from './headless/inputProcessor.js';\r\nimport { OSKView } from \"keyman/engine/osk\";\r\nimport { KeyboardRequisitioner, ModelCache, toUnprefixedKeyboardId as unprefixed } from \"keyman/engine/keyboard-storage\";\r\nimport { ModelSpec, PredictionContext } from \"keyman/engine/interfaces\";\r\n\r\nimport { EngineConfiguration, InitOptionSpec } from \"./engineConfiguration.js\";\r\nimport KeyboardInterface from \"./keyboardInterface.js\";\r\nimport { ContextManagerBase } from \"./contextManagerBase.js\";\r\nimport HardKeyboardBase from \"./hardKeyboard.js\";\r\nimport { LegacyAPIEvents } from \"./legacyAPIEvents.js\";\r\nimport { EventNames, EventListener, LegacyEventEmitter } from \"keyman/engine/events\";\r\nimport DOMCloudRequester from \"keyman/engine/keyboard-storage/dom-requester\";\r\nimport KEYMAN_VERSION from \"@keymanapp/keyman-version\";\r\n\r\n// From https://stackoverflow.com/a/69328045\r\ntype WithRequired = T & { [P in K]-?: T[P] };\r\n// Sets two parts non-optional at this level, while they were at lower levels.\r\ntype ProcessorConfiguration = WithRequired, 'defaultOutputRules'>;\r\n\r\nfunction determineBaseLayout(): string {\r\n // @ts-ignore\r\n if(typeof(window['KeymanWeb_BaseLayout']) !== 'undefined') {\r\n // @ts-ignore\r\n return window['KeymanWeb_BaseLayout'];\r\n } else {\r\n return 'us';\r\n }\r\n}\r\n\r\nexport type KeyEventFullResultCallback = (result: RuleBehavior, error?: Error) => void;\r\nexport type KeyEventFullHandler = (event: KeyEvent, callback?: KeyEventFullResultCallback) => void;\r\n\r\nexport default class KeymanEngine<\r\n Configuration extends EngineConfiguration,\r\n ContextManager extends ContextManagerBase,\r\n HardKeyboard extends HardKeyboardBase\r\n> implements KeyboardKeymanGlobal {\r\n readonly config: Configuration;\r\n contextManager: ContextManager;\r\n interface: KeyboardInterface;\r\n readonly core: InputProcessor;\r\n keyboardRequisitioner: KeyboardRequisitioner;\r\n modelCache: ModelCache;\r\n\r\n protected legacyAPIEvents = new LegacyEventEmitter();\r\n private _hardKeyboard: HardKeyboard;\r\n private _osk: OSKView;\r\n\r\n protected keyEventRefocus?: () => void;\r\n\r\n private keyEventListener: KeyEventFullHandler = (event, callback) => {\r\n const outputTarget = this.contextManager.activeTarget;\r\n\r\n if(!this.contextManager.activeKeyboard || !outputTarget) {\r\n if(callback) {\r\n callback(null, null);\r\n }\r\n return;\r\n }\r\n\r\n if(!this.core.languageProcessor.mayCorrect) {\r\n event.keyDistribution = [];\r\n }\r\n\r\n if(this.keyEventRefocus) {\r\n // Do anything needed to guarantee that the outputTarget stays active (`app/browser`: maintains focus).\r\n // (Interaction with the OSK may have de-focused the element providing active context;\r\n // we want to restore it in case the user swaps back to the hardware keyboard afterward.)\r\n this.keyEventRefocus();\r\n }\r\n\r\n // Clear any cached codepoint data; we can rebuild it if it's unchanged.\r\n outputTarget.invalidateSelection();\r\n // Deadkey matching continues to be troublesome.\r\n // Deleting matched deadkeys here seems to correct some of the issues. (JD 6/6/14)\r\n outputTarget.deadkeys().deleteMatched(); // Delete any matched deadkeys before continuing\r\n\r\n if(event.isSynthetic) {\r\n const oskLayer = this.osk.vkbd.layerId;\r\n\r\n // In case of modipresses.\r\n if(oskLayer && oskLayer != this.core.keyboardProcessor.layerId) {\r\n this.core.keyboardProcessor.layerId = oskLayer;\r\n }\r\n }\r\n const result = this.core.processKeyEvent(event, outputTarget);\r\n\r\n if(result && result.transcription?.transform) {\r\n this.config.onRuleFinalization(result, this.contextManager.activeTarget);\r\n }\r\n\r\n if(callback) {\r\n callback(result, null);\r\n }\r\n\r\n // No try-catch here because we don't want to mask any errors that occur during keystroke\r\n // processing - silent failures are far harder to diagnose.\r\n };\r\n\r\n /**\r\n * @param worker A configured WebWorker to serve as the predictive-text engine's main thread.\r\n * Available in the following variants:\r\n * - sourcemapped, unminified (debug)\r\n * - non-sourcemapped + minified (release)\r\n * @param config\r\n * @param contextManager\r\n * @param processorConfigInitializer A one-time use closure used to initialize certain critical components reliant\r\n * upon the class instance, configured by the derived class, but needed during\r\n * the superclass constructor.\r\n */\r\n constructor(\r\n worker: Worker,\r\n config: Configuration,\r\n contextManager: ContextManager,\r\n processorConfigInitializer: (engine: KeymanEngine) => ProcessorConfiguration\r\n ) {\r\n this.config = config;\r\n this.contextManager = contextManager;\r\n\r\n const processorConfiguration = processorConfigInitializer(this);\r\n processorConfiguration.baseLayout = determineBaseLayout();\r\n this.interface = processorConfiguration.keyboardInterface as KeyboardInterface;\r\n this.core = new InputProcessor(config.hostDevice, worker, processorConfiguration);\r\n\r\n this.core.languageProcessor.on('statechange', (state) => {\r\n // The banner controller cannot directly trigger a layout-refresh at this time,\r\n // so we handle that here.\r\n this.osk?.bannerController.selectBanner(state);\r\n this.osk?.refreshLayout();\r\n });\r\n\r\n // The OSK does not possess a direct connection to the KeyboardProcessor's state-key\r\n // management object; this event + handler allow us to keep the OSK's related states\r\n // in sync.\r\n this.core.keyboardProcessor.on('statekeychange', (stateKeys) => {\r\n this.osk?.vkbd?.updateStateKeys(stateKeys);\r\n })\r\n\r\n this.contextManager.on('beforekeyboardchange', (metadata) => {\r\n this.legacyAPIEvents.callEvent('beforekeyboardchange', {\r\n internalName: metadata?.id,\r\n languageCode: metadata?.langId\r\n });\r\n });\r\n\r\n this.contextManager.on('keyboardchange', (kbd) => {\r\n // Hide OSK and do not update keyboard list if using internal keyboard (desktops).\r\n // Condition will not be met for touch form-factors; they force selection of a\r\n // default keyboard.\r\n if(!kbd) {\r\n this.osk.startHide(false);\r\n }\r\n\r\n const prepareKeyboardSwap = () => {\r\n this.refreshModel();\r\n // Triggers context resets that can trigger layout stuff.\r\n // It's not the final such context-reset, though.\r\n this.core.activeKeyboard = kbd?.keyboard;\r\n\r\n this.legacyAPIEvents.callEvent('keyboardchange', {\r\n internalName: kbd?.metadata.id ?? '',\r\n languageCode: kbd?.metadata.langId ?? ''\r\n });\r\n }\r\n\r\n /*\r\n This pattern is designed to minimize layout reflow during the keyboard-swap process.\r\n The 'default' layer is loaded by default, but some keyboards will start on different\r\n layers depending on the current state of the context.\r\n\r\n If possible, we want to only perform layout operations once the correct layer is\r\n set to active.\r\n */\r\n if(this.osk) {\r\n this.osk.batchLayoutAfter(() => {\r\n prepareKeyboardSwap();\r\n this.osk.activeKeyboard = kbd;\r\n // Note: when embedded within the mobile apps, the keyboard will still be visible\r\n // at this time.\r\n\r\n /*\r\n Needed to ensure the correct layer is displayed AND that deadkeys from\r\n the old keyboard have been wiped.\r\n\r\n Needs to be after the OSK has loaded for the keyboard in case the default\r\n layer should be something other than \"default\" for the current context.\r\n */\r\n this.contextManager.resetContext();\r\n this.osk.present();\r\n });\r\n } else {\r\n prepareKeyboardSwap();\r\n this.contextManager.resetContext();\r\n }\r\n });\r\n\r\n this.contextManager.on('keyboardasyncload', (metadata) => {\r\n /* Original implementation pre-modularization:\r\n *\r\n * > Force OSK display for CJK keyboards (keyboards using a pick list)\r\n *\r\n * A matching subcondition in the block below will ensure that the OSK activates pre-load\r\n * for CJK keyboards. Yes, even before a CJK picker could ever show. We should be fine\r\n * without the CJK check so long as a picker keyboard's OSK is kept activated post-load,\r\n * when the picker actually needs to be kept persistently-active.\r\n * `metadata` would be relevant a the CJK-check, which was based on language codes.\r\n *\r\n * Of course, as mobile devices don't have guaranteed physical keyboards... we need to\r\n * keep the OSK visible for them, hence the actual block below.\r\n */\r\n if(this.config.hostDevice.touchable && this.osk?.activationModel) {\r\n this.osk.activationModel.enabled = true;\r\n // Also note: the OSKView.mayDisable method returns false when hostDevice.touchable = false.\r\n // The .startHide() call below will check that method before actually starting an OSK hide.\r\n }\r\n\r\n // Always (temporarily) hide the OSK when loading a new keyboard, to ensure\r\n // that a failure to load doesn't leave the current OSK displayed\r\n this.osk?.startHide(false);\r\n });\r\n }\r\n\r\n public async init(optionSpec: Required){\r\n // There may be some valid mutations possible even on repeated calls?\r\n // The original seems to allow it.\r\n\r\n const config = this.config;\r\n if(config.deferForInitialization.isResolved) {\r\n // abort! Maybe throw an error, too.\r\n return Promise.resolve();\r\n }\r\n\r\n config.initialize(optionSpec);\r\n\r\n // Initialize supplementary plane string extensions\r\n String.kmwEnableSupplementaryPlane(true);\r\n\r\n // Since we're not sandboxing keyboard loads yet, we just use `window` as the jsGlobal object.\r\n // All components initialized below require a properly-configured `config.paths` or similar.\r\n const keyboardLoader = new KeyboardLoader(this.interface, config.applyCacheBusting);\r\n this.keyboardRequisitioner = new KeyboardRequisitioner(keyboardLoader, new DOMCloudRequester(), this.config.paths);\r\n this.modelCache = new ModelCache();\r\n const kbdCache = this.keyboardRequisitioner.cache;\r\n\r\n const keyboardProcessor = this.core.keyboardProcessor;\r\n const predictionContext = new PredictionContext(this.core.languageProcessor, () => keyboardProcessor.layerId);\r\n this.contextManager.configure({\r\n resetContext: (target) => {\r\n // Could reset the target's deadkeys here, but it's really more of a 'core' task.\r\n // So we delegate that to keyboard.\r\n if(this.osk) {\r\n this.osk.batchLayoutAfter(() => {\r\n this.core.resetContext(target);\r\n })\r\n } else {\r\n this.core.resetContext(target);\r\n }\r\n },\r\n predictionContext: predictionContext,\r\n keyboardCache: this.keyboardRequisitioner.cache\r\n });\r\n\r\n /*\r\n * Handler for post-processing once a suggestion has been applied.\r\n *\r\n * This is called after the suggestion is applied but _before_ new\r\n * predictions are requested based on the resulting context.\r\n */\r\n this.core.languageProcessor.on('suggestionapplied', () => {\r\n // Tell the keyboard that the current layer has not changed\r\n keyboardProcessor.newLayerStore.set('');\r\n keyboardProcessor.oldLayerStore.set('');\r\n // Call the keyboard's entry point.\r\n keyboardProcessor.processPostKeystroke(keyboardProcessor.contextDevice, predictionContext.currentTarget as OutputTarget)\r\n // If we have a RuleBehavior as a result, run it on the target. This should\r\n // only change system store and variable store values.\r\n ?.finalize(keyboardProcessor, predictionContext.currentTarget as OutputTarget, true);\r\n });\r\n\r\n // #region Event handler wiring\r\n this.config.on('spacebartext', () => {\r\n // On change of spacebar-text mode, we currently need a layout refresh to update the\r\n // spacebar's text.\r\n this.osk?.refreshLayout();\r\n });\r\n\r\n kbdCache.on('stubadded', (stub) => {\r\n let eventRaiser = () => {\r\n // The corresponding event is needed in order to update UI modules as new keyboard stubs \"come online\".\r\n this.legacyAPIEvents.callEvent('keyboardregistered', {\r\n internalName: stub.KI,\r\n language: stub.KL,\r\n keyboardName: stub.KN,\r\n languageCode: stub.KLC,\r\n package: stub.KP\r\n });\r\n\r\n // If this is the first stub loaded, set it as active.\r\n if(this.config.activateFirstKeyboard && this.keyboardRequisitioner.cache.defaultStub == stub) {\r\n // Note: leaving this out is super-useful for debugging issues that occur when no keyboard is active.\r\n this.contextManager.activateKeyboard(stub.id, stub.langId, true);\r\n }\r\n }\r\n\r\n if(this.config.deferForInitialization.isResolved) {\r\n eventRaiser();\r\n } else {\r\n this.config.deferForInitialization.then(eventRaiser);\r\n }\r\n });\r\n\r\n kbdCache.on('keyboardadded', (keyboard) => {\r\n let eventRaiser = () => {\r\n // Execute any external (UI) code needed after loading keyboard\r\n this.legacyAPIEvents.callEvent('keyboardloaded', {\r\n keyboardName: keyboard.id\r\n });\r\n }\r\n\r\n if(this.config.deferForInitialization.isResolved) {\r\n eventRaiser();\r\n } else {\r\n this.config.deferForInitialization.then(eventRaiser);\r\n }\r\n });\r\n\r\n this.keyboardRequisitioner.cache.on('keyboardadded', (keyboard) => {\r\n this.legacyAPIEvents.callEvent('keyboardloaded', { keyboardName: keyboard.id });\r\n });\r\n //\r\n // #endregion\r\n }\r\n\r\n /**\r\n * Public API: Denotes the 'patch' component of the version of the current engine.\r\n *\r\n * https://help.keyman.com/developer/engine/web/current-version/reference/core/build\r\n */\r\n public get build(): number {\r\n return Number.parseInt(KEYMAN_VERSION.VERSION_PATCH, 10);\r\n }\r\n\r\n /**\r\n * Public API: Denotes the major & minor components of the version of the current engine.\r\n *\r\n * https://help.keyman.com/developer/engine/web/current-version/reference/core/version\r\n */\r\n public get version(): string {\r\n return KEYMAN_VERSION.VERSION_RELEASE;\r\n }\r\n\r\n public get hardKeyboard(): HardKeyboard {\r\n return this._hardKeyboard;\r\n }\r\n\r\n protected set hardKeyboard(keyboard: HardKeyboard) {\r\n if(this._hardKeyboard) {\r\n this._hardKeyboard.off('keyevent', this.keyEventListener);\r\n }\r\n this._hardKeyboard = keyboard;\r\n keyboard.on('keyevent', this.keyEventListener);\r\n }\r\n\r\n public get osk(): OSKView {\r\n return this._osk;\r\n }\r\n\r\n public set osk(value: OSKView) {\r\n if(this._osk) {\r\n this._osk.off('keyevent', this.keyEventListener);\r\n this.core.keyboardProcessor.layerStore.handler = this.osk.layerChangeHandler;\r\n }\r\n this._osk = value;\r\n // As the `new context` ruleset is designed to facilitate OSK layer-change updates\r\n // based on the context being entered, we want the keyboard processor's current\r\n // contextDevice to match that of the active OSK. See #11740.\r\n this.core.keyboardProcessor.contextDevice = value?.targetDevice ?? this.config.softDevice;\r\n if(value) {\r\n // Don't build an OSK if no keyboard is available yet; avoid the extra flash.\r\n if(this.contextManager.activeKeyboard) {\r\n value.activeKeyboard = this.contextManager.activeKeyboard;\r\n }\r\n value.on('keyevent', this.keyEventListener);\r\n this.core.keyboardProcessor.layerStore.handler = value.layerChangeHandler;\r\n }\r\n }\r\n\r\n public getDebugInfo(): Record {\r\n const activeKbd = this.contextManager?.activeKeyboard;\r\n\r\n const report = {\r\n configReport: this.config?.debugReport(),\r\n keyboard: {\r\n id: unprefixed(activeKbd?.metadata?.id ?? ''),\r\n langId: activeKbd?.metadata?.langId || '',\r\n version: activeKbd?.keyboard?.version ?? ''\r\n },\r\n model: {\r\n id: this.core?.activeModel?.id || ''\r\n },\r\n osk: {\r\n banner: this.osk?.banner?.banner.type ?? '',\r\n layer: this.osk?.vkbd?.layerId || ''\r\n }\r\n };\r\n\r\n return report;\r\n }\r\n\r\n // Returned Promise: gives the model-spec object. Only resolves when any model loading or unloading\r\n // is fully complete.\r\n private refreshModel(): Promise {\r\n const kbd = this.contextManager.activeKeyboard;\r\n const model = this.modelCache.modelForLanguage(kbd?.metadata.langId);\r\n\r\n if(this.core.activeModel != model) {\r\n if(this.core.activeModel) {\r\n this.core.languageProcessor.unloadModel();\r\n }\r\n\r\n // Semi-hacky management of banner display state.\r\n if(model) {\r\n return this.core.languageProcessor.loadModel(model).then(() => {\r\n return model;\r\n });\r\n }\r\n }\r\n\r\n return Promise.resolve(model);\r\n }\r\n\r\n /**\r\n * Subscribe to Keyman Engine events documented at\r\n * https://help.keyman.com/developer/engine/web/current-version/reference/events. Note that any OSK-related\r\n * events should instead register on `keyman.osk.addEventListener`, not on this method.\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/core/addEventListener\r\n */\r\n public addEventListener>(event: Name, listener: EventListener) {\r\n this.legacyAPIEvents.addEventListener(event, listener);\r\n }\r\n\r\n /**\r\n * Public API: Unsubscribe from Keyman Engine events documented at\r\n * https://help.keyman.com/developer/engine/web/current-version/reference/events.\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/core/removeEventListener\r\n */\r\n public removeEventListener>(event: Name, listener: EventListener) {\r\n this.legacyAPIEvents.removeEventListener(event, listener);\r\n }\r\n\r\n shutdown() {\r\n this.legacyAPIEvents.shutdown();\r\n this.osk?.shutdown();\r\n }\r\n\r\n // API methods\r\n\r\n // 17.0: new! Only used by apps utilizing app/webview and one app/browser test page.\r\n // Is not part of our 'published' API.\r\n\r\n /**\r\n * Registers the specified lexical model within Keyman Engine. If a keyboard with a\r\n * matching language code is currently activated, it will also activate the model.\r\n *\r\n * @param model An object defining model ID, associated language IDs, and either the\r\n * model's definition or a path to a file containing it.\r\n */\r\n addModel(model: ModelSpec): Promise {\r\n this.modelCache.register(model);\r\n\r\n const activeStub = this.contextManager.activeKeyboard?.metadata;\r\n if(activeStub && model.languages.indexOf(activeStub.langId) != -1) {\r\n return this.refreshModel().then(() => { return; });\r\n } else {\r\n return Promise.resolve();\r\n }\r\n }\r\n\r\n // 17.0: new! Only used by apps utilizing app/webview and one app/browser test page.\r\n\r\n /**\r\n * Unregisters any previously-registered lexical model with a matching ID from Keyman Engine.\r\n * If a keyboard with a matching language code is currently activated, it will also\r\n * deactivate the model.\r\n *\r\n * @param modelId The ID for the model to be deregistered and forgotten by Keyman Engine.\r\n */\r\n removeModel(modelId: string) {\r\n this.modelCache.unregister(modelId);\r\n\r\n // Is it the active model?\r\n if(this.core.activeModel && this.core.activeModel.id == modelId) {\r\n this.core.languageProcessor.unloadModel();\r\n }\r\n }\r\n\r\n /**\r\n * Allow to change active keyboard by (internal) keyboard name\r\n *\r\n * @param {string} PInternalName Internal name\r\n * @param {string} PLgCode Language code\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/core/setActiveKeyboard\r\n */\r\n public async setActiveKeyboard(keyboardId: string, languageCode?: string): Promise {\r\n return this.contextManager.activateKeyboard(keyboardId, languageCode, true);\r\n }\r\n\r\n /**\r\n * Function getActiveKeyboard\r\n * Scope Public\r\n * @return {string} Name of active keyboard\r\n * Description Return internal name of currently active keyboard\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/core/getActiveKeyboard\r\n */\r\n public getActiveKeyboard(): string {\r\n return this.contextManager.activeKeyboard?.metadata.id ?? '';\r\n }\r\n\r\n /**\r\n * Function getActiveLanguage\r\n * Scope Public\r\n * @param {boolean=} true to retrieve full language name, false/undefined to retrieve code.\r\n * @return {string} language code\r\n * Description Return language code for currently selected language\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/core/getActiveLanguage\r\n */\r\n public getActiveLanguage(fullName?: boolean): string {\r\n // In short... the activeStub.\r\n const metadata = this.contextManager.activeKeyboard?.metadata;\r\n\r\n if(!fullName) {\r\n return metadata?.langId ?? '';\r\n } else {\r\n return metadata?.langName ?? '';\r\n }\r\n }\r\n\r\n /**\r\n * Function isChiral\r\n * Scope Public\r\n * @param {string|Object=} k0\r\n * @return {boolean}\r\n * Description Tests if the active keyboard (or optional argument) uses chiral modifiers.\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/core/isChiral\r\n */\r\n public isChiral(k0?: string | Keyboard) {\r\n let kbd: Keyboard;\r\n if(k0) {\r\n if(typeof k0 == 'string') {\r\n const kbdObj = this.keyboardRequisitioner.cache.getKeyboard(k0);\r\n if(!kbdObj) {\r\n throw new Error(`Keyboard '${k0}' has not been loaded.`);\r\n } else {\r\n k0 = kbdObj;\r\n }\r\n }\r\n\r\n kbd = k0;\r\n } else {\r\n kbd = this.core.activeKeyboard;\r\n }\r\n return kbd.isChiral;\r\n }\r\n\r\n /**\r\n * Function resetContext\r\n * Scope Public\r\n * Description Reverts the OSK to the default layer, clears any processing caches and modifier states,\r\n * and clears deadkeys and prediction-processing states on the active element (if it exists)\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/core/resetContext\r\n */\r\n public resetContext() {\r\n this.contextManager.resetContext();\r\n };\r\n\r\n /**\r\n * Function setNumericLayer\r\n * Scope Public\r\n * Description Set OSK to numeric layer if it exists\r\n */\r\n setNumericLayer() {\r\n this.core.keyboardProcessor.setNumericLayer(this.config.softDevice);\r\n };\r\n}\r\n\r\n// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed.", + "import { Keyboard, KeyboardProperties } from 'keyman/engine/keyboard';\r\nimport { createUnselectableElement } from 'keyman/engine/dom-utils';\r\n\r\n// Base class for a banner above the keyboard in the OSK\r\n\r\nexport abstract class Banner {\r\n private _height: number; // pixels\r\n private _width: number; // pixels\r\n private div: HTMLDivElement;\r\n\r\n public static DEFAULT_HEIGHT: number = 37; // pixels; embedded apps can modify\r\n\r\n public static readonly BANNER_CLASS: string = 'kmw-banner-bar';\r\n public static readonly BANNER_ID: string = 'kmw-banner-bar';\r\n\r\n /**\r\n * Function height\r\n * Scope Public\r\n * @returns {number} height in pixels\r\n * Description Returns the height of the banner in pixels\r\n */\r\n public get height(): number {\r\n return this._height;\r\n }\r\n\r\n /**\r\n * Function height\r\n * Scope Public\r\n * @param {number} height the height in pixels\r\n * Description Sets the height of the banner in pixels. If a negative\r\n * height is given, set height to 0 pixels.\r\n * Also updates the banner styling.\r\n */\r\n public set height(height: number) {\r\n this._height = (height > 0) ? height : 0;\r\n this.update();\r\n }\r\n\r\n public get width(): number {\r\n return this._width;\r\n }\r\n\r\n public set width(width: number) {\r\n this._width = width;\r\n this.update();\r\n }\r\n\r\n /**\r\n * Function update\r\n * @return {boolean} true if the banner styling changed\r\n * Description Update the height and display styling of the banner\r\n */\r\n protected update() : boolean {\r\n let ds = this.div.style;\r\n let currentHeightStyle = ds.height;\r\n let currentDisplayStyle = ds.display;\r\n\r\n if (this._height > 0) {\r\n ds.height = this._height + 'px';\r\n ds.display = 'block';\r\n } else {\r\n ds.height = '0px';\r\n ds.display = 'none';\r\n }\r\n\r\n return (!(currentHeightStyle === ds.height) ||\r\n !(currentDisplayStyle === ds.display));\r\n }\r\n\r\n public constructor(height?: number) {\r\n let d = createUnselectableElement('div');\r\n d.id = Banner.BANNER_ID;\r\n d.className = Banner.BANNER_CLASS;\r\n this.div = d;\r\n\r\n this.height = height;\r\n this.update();\r\n }\r\n\r\n public appendStyleSheet() {\r\n // TODO: add stylesheets\r\n // See VisualKeyboard's method + 'addFontStyle' for current handling.\r\n }\r\n\r\n /**\r\n * Function getDiv\r\n * Scope Public\r\n * @returns {HTMLElement} Base element of the banner\r\n * Description Returns the HTMLElement of the banner\r\n */\r\n public getDiv(): HTMLElement {\r\n return this.div;\r\n }\r\n\r\n /**\r\n * Allows banners to adapt based on the active keyboard and related properties, such as\r\n * associated fonts.\r\n * @param keyboard\r\n * @param keyboardProperties\r\n */\r\n public configureForKeyboard(keyboard: Keyboard, keyboardProperties: KeyboardProperties) { }\r\n\r\n public readonly refreshLayout?: () => void;\r\n\r\n abstract get type(): string;\r\n\r\n public shutdown() { };\r\n}\r\n", + "export interface LengthStyle {\r\n val: number,\r\n absolute: boolean,\r\n special?: 'em' | 'rem';\r\n};\r\n\r\nexport class ParsedLengthStyle implements LengthStyle {\r\n public readonly val: number;\r\n public readonly absolute: boolean;\r\n public readonly special: 'em' | 'rem';\r\n\r\n public constructor(style: LengthStyle | string) {\r\n let parsed: LengthStyle = (typeof style == 'string') ? ParsedLengthStyle.parseLengthStyle(style) : style;\r\n\r\n // While Object.assign would be nice (and previously, was used), it will break\r\n // on old but still supported versions of Android if their Chrome isn't updated.\r\n // Requires mobile Chrome 45+, but API 21 (5.0) launches with an older browser.\r\n\r\n // Object.assign(this, parsed);\r\n this.val = parsed.val;\r\n this.absolute = parsed.absolute;\r\n if(parsed.special) {\r\n this.special = parsed.special;\r\n }\r\n }\r\n\r\n public get styleString(): string {\r\n if(this.absolute) {\r\n return this.val + 'px';\r\n } else if(this.special) {\r\n // Only 'em' and 'rem' are allowed, and both may be treated similarly.\r\n // Both relate to font sizes, though the path to the reference element\r\n // differs between them.\r\n return this.val + this.special;\r\n } else {\r\n return (this.val * 100) + '%';\r\n }\r\n }\r\n\r\n public scaledBy(scalar: number): ParsedLengthStyle {\r\n return new ParsedLengthStyle({\r\n val: scalar * this.val,\r\n absolute: this.absolute\r\n });\r\n }\r\n\r\n public static inPixels(val: number): ParsedLengthStyle {\r\n return new ParsedLengthStyle({val: val, absolute: true});\r\n }\r\n\r\n public static inPercent(val: number): ParsedLengthStyle {\r\n return new ParsedLengthStyle({val: val/100, absolute: false});\r\n }\r\n\r\n public static forScalar(val: number): ParsedLengthStyle {\r\n return new ParsedLengthStyle({val: val, absolute: false});\r\n }\r\n\r\n public static special(val: number, suffix: 'em' | 'rem'): ParsedLengthStyle {\r\n return new ParsedLengthStyle({val: val, absolute: false, special: suffix});\r\n }\r\n\r\n private static parseLengthStyle(spec: string): LengthStyle {\r\n if(spec == '') {\r\n return CONSTANT_STYLE;\r\n }\r\n\r\n const val = parseFloat(spec);\r\n\r\n if(isNaN(val)) {\r\n // Cannot parse.\r\n console.error(\"Could not properly parse specified length style info: '\" + spec + \"'.\");\r\n return CONSTANT_STYLE;\r\n }\r\n\r\n return spec.indexOf('px') != -1 ? {val: val, absolute: true} :\r\n // 16 px ~= 12 pt.\r\n // Reference: https://kyleschaeffer.com/css-font-size-em-vs-px-vs-pt-vs-percent\r\n spec.indexOf('pt') != -1 ? {val: (4 * val / 3), absolute: true} :\r\n spec.indexOf('%') != -1 ? {val: val/100, absolute: false} :\r\n spec.indexOf('rem') != -1 ? {val: val, absolute: false, special: 'rem'} :\r\n spec.indexOf('em') != -1 ? {val: val, absolute: false, special: 'em'} :\r\n // At this point, assuming either Number or number in a string without units\r\n // Note: this one is NOT natively handled by browsers!\r\n // We'll treat it as if it were 'pt', since that's likely the user's\r\n // most familiar font size unit.\r\n {val: (4 * val / 3), absolute: true};\r\n }\r\n}\r\n\r\nconst CONSTANT_STYLE = new ParsedLengthStyle('1em');", + "import { Banner } from \"./banner.js\";\r\n\r\n/**\r\n * Function BlankBanner\r\n * Description A banner of height 0 that should not be shown\r\n */\r\nexport class BlankBanner extends Banner {\r\n readonly type = 'blank';\r\n\r\n constructor() {\r\n super(0);\r\n }\r\n}", + "import { EventEmitter } from 'eventemitter3';\r\n\r\nimport { createUnselectableElement } from 'keyman/engine/dom-utils';\r\n\r\nimport { Banner } from './banner.js';\r\nimport OSKViewComponent from '../components/oskViewComponent.interface.js';\r\nimport { ParsedLengthStyle } from '../lengthStyle.js';\r\nimport { BlankBanner } from './blankBanner.js';\r\n\r\n/**\r\n * This object is used to specify options by both `BannerManager.getOptions`\r\n * and `BannerManager.setOptions`. Refer to the latter for specification of\r\n * each field.\r\n */\r\nexport interface BannerOptions {\r\n alwaysShow?: boolean;\r\n imagePath?: string;\r\n}\r\n\r\nexport type BannerType = \"blank\" | \"image\" | \"suggestion\" | \"html\";\r\n\r\ninterface BannerViewEventMap {\r\n 'bannerchange': () => void;\r\n}\r\n\r\n/**\r\n * The `BannerView` module is designed to serve as the hot-swap container for the\r\n * different `Banner` types, helping KMW to avoid needless DOM element shuffling.\r\n */\r\nexport class BannerView implements OSKViewComponent {\r\n private bannerContainer: HTMLDivElement;\r\n\r\n /**\r\n * The currently active banner.\r\n */\r\n private currentBanner: Banner;\r\n private _activeBannerHeight: number = Banner.DEFAULT_HEIGHT;\r\n\r\n public readonly events = new EventEmitter();\r\n\r\n constructor() {\r\n // Step 1 - establish the container element. Must come before this.setOptions.\r\n this.constructContainer();\r\n }\r\n\r\n /**\r\n * Constructs the
element used to contain hot-swapped `Banner` instances.\r\n */\r\n private constructContainer(): HTMLDivElement {\r\n let d = createUnselectableElement('div');\r\n d.id = \"keymanweb_banner_container\";\r\n d.className = \"kmw-banner-container\";\r\n return this.bannerContainer = d;\r\n }\r\n\r\n /**\r\n * Returns the `Banner`-containing div element used to facilitate hot-swapping.\r\n */\r\n public get element(): HTMLDivElement {\r\n return this.bannerContainer;\r\n }\r\n\r\n /**\r\n * Applies any stylesheets needed by specific `Banner` instances.\r\n */\r\n public appendStyles() {\r\n if(this.currentBanner) {\r\n this.currentBanner.appendStyleSheet();\r\n }\r\n }\r\n\r\n public get banner(): Banner {\r\n return this.currentBanner;\r\n }\r\n\r\n /**\r\n * The `Banner` actively being displayed to the user in the OSK's current state,\r\n * whether a `SuggestionBanner` (with predictive-text active) or a different\r\n * type for use when the predictive-text engine is inactive.\r\n */\r\n public set banner(banner: Banner) {\r\n if(this.currentBanner) {\r\n if(banner == this.currentBanner) {\r\n return;\r\n } else {\r\n let prevBanner = this.currentBanner;\r\n this.currentBanner = banner;\r\n this.bannerContainer.replaceChild(banner.getDiv(), prevBanner.getDiv());\r\n prevBanner.shutdown();\r\n }\r\n } else {\r\n this.currentBanner = banner;\r\n if(banner) {\r\n this.bannerContainer.appendChild(banner.getDiv());\r\n }\r\n }\r\n\r\n if(!(banner instanceof BlankBanner)) {\r\n banner.height = this.activeBannerHeight;\r\n }\r\n\r\n this.events.emit('bannerchange');\r\n }\r\n\r\n /**\r\n * Gets the height (in pixels) of the active `Banner` instance.\r\n */\r\n public get height(): number {\r\n if(this.currentBanner) {\r\n return this.currentBanner.height;\r\n } else {\r\n return 0;\r\n }\r\n }\r\n\r\n public get activeBannerHeight(): number {\r\n return this._activeBannerHeight;\r\n }\r\n\r\n /**\r\n * Sets the height (in pixels) of the active 'Banner' instance.\r\n */\r\n public set activeBannerHeight(h: number) {\r\n this._activeBannerHeight = h;\r\n\r\n if (this.currentBanner && !(this.currentBanner instanceof BlankBanner)) {\r\n this.currentBanner.height = h;\r\n }\r\n }\r\n\r\n public get layoutHeight(): ParsedLengthStyle {\r\n return ParsedLengthStyle.inPixels(this.height);\r\n }\r\n\r\n public get width(): number | undefined {\r\n return this.currentBanner?.width;\r\n }\r\n\r\n public set width(w: number) {\r\n if(this.currentBanner) {\r\n this.currentBanner.width = w;\r\n }\r\n }\r\n\r\n public refreshLayout() {\r\n this.currentBanner.refreshLayout?.();\r\n }\r\n}", + "import { Banner } from \"./banner.js\";\r\n\r\n/**\r\n * Function ImageBanner\r\n * @param {string} imagePath Path of image to display in the banner\r\n * @param {number} height If provided, the height of the banner in pixels\r\n * Description Display an image in the banner\r\n */\r\nexport class ImageBanner extends Banner {\r\n private img: HTMLElement;\r\n readonly type;\r\n\r\n constructor(imagePath: string, height?: number) {\r\n if (imagePath.length > 0) {\r\n super();\r\n if (height) {\r\n this.height = height;\r\n }\r\n } else {\r\n super(0);\r\n }\r\n\r\n this.type = 'image';\r\n\r\n if(imagePath.indexOf('base64') >=0) {\r\n console.log(\"Loading img from base64 data\");\r\n } else {\r\n console.log(\"Loading img with src '\" + imagePath + \"'\");\r\n }\r\n this.img = document.createElement('img');\r\n this.img.setAttribute('src', imagePath);\r\n let ds = this.img.style;\r\n\r\n // We may want to eliminate the width-spec in the future, once we're sure of\r\n // no unintended side-effects for iOS's use of this banner.\r\n //\r\n // Maybe if/when we also add a style=\"background-color: #xxx\" option.\r\n ds.width = '100%';\r\n ds.height = '100%';\r\n this.getDiv().appendChild(this.img);\r\n console.log(\"Image loaded.\");\r\n }\r\n\r\n /**\r\n * Function setImagePath\r\n * Scope Public\r\n * @param {string} imagePath Path of image to display in the banner\r\n * Description Update the image in the banner\r\n */\r\n public setImagePath(imagePath: string) {\r\n if (this.img) {\r\n this.img.setAttribute('src', imagePath);\r\n }\r\n }\r\n}", + "export function reportError(baseMsg: string, err: Error) {\r\n // Our mobile-app Sentry logging will listen for this and log it.\r\n if(err instanceof Error) {\r\n console.error(`${baseMsg}: ${err.message}\\n\\n${err.stack}`);\r\n } else {\r\n console.error(baseMsg);\r\n console.error(err);\r\n }\r\n}", + "import { timedPromise } from \"@keymanapp/web-utils\";\r\nimport { reportError } from \"../reportError.js\";\r\n\r\nexport type QueueClosure = () => (Promise | void);\r\n\r\n/**\r\n This class is modeled somewhat after Swift's `DispatchQueue` class, but with\r\n the twist that each closure may return a `Promise` (in Swift: a `Future`) to\r\n lock out further closure processing until the `Promise` resolves.\r\n*/\r\nexport class AsyncClosureDispatchQueue {\r\n private queue: QueueClosure[];\r\n private waitLock: Promise;\r\n private defaultWaitFactory: () => Promise;\r\n\r\n /**\r\n *\r\n * @param defaultWaitFactory A factory returning Promises to use for default\r\n * delays between tasks. If not specified, Promises corresponding to\r\n * setTimeout(0) will be used, allowing the microqueue task to flush between\r\n * tasks.\r\n */\r\n constructor(defaultWaitFactory?: () => Promise) {\r\n // We only need to trigger events if the queue has no prior entries and there isn't an\r\n // active wait-lock; for the latter, we'll auto-trigger the next function when it unlocks.\r\n this.queue = [];\r\n\r\n this.defaultWaitFactory = defaultWaitFactory || (() => { return timedPromise(0) });\r\n }\r\n\r\n get defaultWait() {\r\n return this.defaultWaitFactory();\r\n }\r\n\r\n get ready() {\r\n return this.queue.length == 0 && !this.waitLock;\r\n }\r\n\r\n private async triggerNextClosure() {\r\n if(this.queue.length == 0) {\r\n return;\r\n }\r\n\r\n const functor = this.queue.shift();\r\n\r\n // A stand-in so that `ready` doesn't report true while the closure runs.\r\n this.waitLock = Promise.resolve();\r\n\r\n /*\r\n It is imperative that any errors triggered by the functor do not prevent this method from setting\r\n the wait lock that will trigger the following event (if it exists). Failure to do so will\r\n result in all further queued closures never getting the opportunity to run!\r\n */\r\n let result: undefined | Promise;\r\n try {\r\n // Is either undefined (return type: void) or is a Promise.\r\n result = functor() as undefined | Promise;\r\n /* c8 ignore start */\r\n } catch (err) {\r\n reportError('Error from queued closure', err);\r\n }\r\n /* c8 ignore end */\r\n\r\n /*\r\n Replace the stand-in with the _true_ post-closure wait.\r\n\r\n If the closure returns a Promise, the implication is that the further processing of queued\r\n functions should be blocked until that Promise is fulfilled.\r\n\r\n If not, we just add a default delay.\r\n */\r\n result = result ?? this.defaultWaitFactory();\r\n this.waitLock = result;\r\n\r\n try {\r\n await result;\r\n } catch(err) {\r\n reportError('Async error from queued closure', err);\r\n }\r\n\r\n this.waitLock = null;\r\n // if queue is length zero, auto-returns.\r\n this.triggerNextClosure();\r\n }\r\n\r\n runAsync(closure: QueueClosure) {\r\n // Check before putting the closure on the internal queue; the check is based in part\r\n // upon the existing queue length.\r\n const isReady = this.ready;\r\n\r\n this.queue.push(closure);\r\n\r\n // If `!isReady`, the next closure will automatically be triggered when possible.\r\n if(isReady) {\r\n this.triggerNextClosure();\r\n }\r\n }\r\n}", + "/**\r\n * We want these to be readily and safely converted to and from\r\n * JSON (for unit test use and development)\r\n */\r\nexport interface InputSample {\r\n /**\r\n * Represents the x-coordinate of the input sample\r\n * in 'client' / viewport coordinates.\r\n */\r\n readonly clientX?: number;\r\n\r\n /**\r\n * Represents the x-coordinate of the input sample in\r\n * coordinates relative to the recognizer's `targetRoot`.\r\n */\r\n readonly targetX: number;\r\n\r\n /**\r\n * Represents the y-coordinate of the input sample\r\n * in 'client' / viewport coordinates.\r\n */\r\n readonly clientY?: number;\r\n\r\n /**\r\n * Represents the y-coordinate of the input sample in\r\n * coordinates relative to the recognizer's `targetRoot`.\r\n */\r\n readonly targetY: number;\r\n\r\n /**\r\n * Represents the timestamp at which the input was observed\r\n * (in ms)\r\n */\r\n readonly t: number;\r\n\r\n // The following two are intentionally _not_ readonly; `stateToken`, in particular,\r\n // may need modification by specific gesture-model implementations.\r\n\r\n /**\r\n * The UI/UX 'item' underneath the touchpoint for this sample.\r\n */\r\n item?: Type;\r\n\r\n /**\r\n * A token identifying the state of the consuming system associated\r\n * with this sample's `GestureSource`, if any such association exists.\r\n */\r\n stateToken?: StateToken\r\n}\r\n\r\nexport type InputSampleSequence = InputSample[];\r\n\r\nexport function isAnInputSample(obj: any): obj is InputSample {\r\n return 'targetX' in obj && 'targetY' in obj && 't' in obj;\r\n}", + "import { InputSample, isAnInputSample } from \"./inputSample.js\";\r\n\r\n/**\r\n * Denotes one dimension utilized by touchpath input coordinates - 'x' and y' for space,\r\n * 't' for time.\r\n */\r\nexport type PathCoordAxis = 'x' | 'y' | 't';\r\n\r\n/**\r\n * Denotes a pair of dimensions utilized by touchpath input coordinates. The two axes\r\n * (see `PathCoordAxis`) must be specified in alphabetical order.\r\n */\r\nexport type PathCoordAxisPair = 'tx' | 'ty' | 'xy';\r\n\r\n/**\r\n * Denotes one dimension or feature (velocity) that this class tracks statistics for.\r\n *\r\n * Sine and Cosine stats are currently excluded due to their necessary lack of statistical\r\n * independence.\r\n */\r\ntype StatAxis = PathCoordAxis;\r\n\r\n/**\r\n * As the name suggests, this class facilitates tracking of cumulative mathematical values, etc\r\n * useful for interpretation of a contact point's path as it relates to gestures.\r\n *\r\n * Instances of this class may be considered immutable externally.\r\n *\r\n * A subclass with properties useful for path segmentation: `RegressiblePathStats`.\r\n */\r\nexport class CumulativePathStats {\r\n protected rawLinearSums = {'x': 0, 'y': 0, 't': 0};\r\n\r\n // Handles raw-distance stuff.\r\n private coordArcSum: number = 0;\r\n\r\n /**\r\n * The base sample used to transpose all other received samples. Use of this helps\r\n * avoid potential \"catastrophic cancellation\" effects that can occur when diffing\r\n * two numbers far from the sample-space's mathematical origin.\r\n *\r\n * Refer to https://en.wikipedia.org/wiki/Catastrophic_cancellation.\r\n */\r\n protected baseSample?: InputSample;\r\n\r\n /**\r\n * The initial sample included by this instance's computed stats. Needed for\r\n * the 'directness' properties.\r\n */\r\n private _initialSample?: InputSample;\r\n\r\n private _lastSample?: InputSample;\r\n protected followingSample?: InputSample;\r\n private _sampleCount = 0;\r\n\r\n constructor();\r\n constructor(sample: InputSample);\r\n constructor(instance: CumulativePathStats);\r\n constructor(obj?: InputSample | CumulativePathStats)\r\n constructor(obj?: InputSample | CumulativePathStats) {\r\n if(!obj) {\r\n return;\r\n }\r\n\r\n // Will worry about JSON form later.\r\n if(obj instanceof CumulativePathStats) {\r\n Object.assign(this, obj);\r\n\r\n this.rawLinearSums = {...obj.rawLinearSums};\r\n } else if(isAnInputSample(obj)) {\r\n Object.assign(this, this.extend(obj));\r\n /* c8 ignore next 3 */\r\n } else {\r\n throw new Error(\"A constructor for this input pattern has not yet been implemented\");\r\n }\r\n }\r\n\r\n /**\r\n * Statistically \"observes\" a new sample point on the touchpath, accumulating values\r\n * useful for provision of relevant statistical properties.\r\n * @param sample A newly-sampled point on the touchpath.\r\n * @returns A new, separate instance for the cumulative properties up to the\r\n * newly-sampled point.\r\n */\r\n public extend(sample: InputSample): CumulativePathStats {\r\n return this._extend(new CumulativePathStats(this), sample);\r\n }\r\n\r\n // Pattern exists to facilitate subclasses if needed in the future: see #11079 and #11080.\r\n protected _extend(result: CumulativePathStats, sample: InputSample) {\r\n if(!result._initialSample) {\r\n result._initialSample = sample;\r\n result.baseSample = sample;\r\n }\r\n\r\n const baseSample = result.baseSample;\r\n\r\n // Set _after_ deep-copying this for the result.\r\n this.followingSample = sample;\r\n\r\n // Helps prevent \"catastrophic cancellation\" issues from floating-point computation\r\n // for these statistical properties and properties based upon them.\r\n const x = sample.targetX - baseSample.targetX;\r\n const y = sample.targetY - baseSample.targetY;\r\n const t = sample.t - baseSample.t;\r\n\r\n result.rawLinearSums.x += x;\r\n result.rawLinearSums.y += y;\r\n result.rawLinearSums.t += t;\r\n\r\n if(this.lastSample) {\r\n // arc length stuff!\r\n const xDelta = sample.targetX - this.lastSample.targetX;\r\n const yDelta = sample.targetY - this.lastSample.targetY;\r\n\r\n const coordArcDeltaSq = xDelta * xDelta + yDelta * yDelta;\r\n const coordArcDelta = Math.sqrt(coordArcDeltaSq);\r\n\r\n result.coordArcSum += coordArcDelta;\r\n }\r\n\r\n result._lastSample = sample;\r\n result.sampleCount = this.sampleCount + 1;\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * \"De-accumulates\" currently-accumulated values corresponding to the specified\r\n * subset, which should represent an earlier, previously-observed part of the path.\r\n * @param subsetStats The accumulated stats for the part of the path being removed\r\n * from this instance's current accumulation.\r\n * @returns\r\n */\r\n public deaccumulate(subsetStats?: CumulativePathStats): CumulativePathStats {\r\n const result = new CumulativePathStats(this);\r\n\r\n return this._deaccumulate(result, subsetStats);\r\n }\r\n\r\n protected _deaccumulate(result: CumulativePathStats, subsetStats?: CumulativePathStats): CumulativePathStats {\r\n // Possible addition: use `this.buildRenormalized` on the returned version\r\n // if catastrophic cancellation effects (random, small floating point errors)\r\n // are not sufficiently mitigated & handled by the measures currently in place.\r\n //\r\n // Even then, we'd need to apply such generated objects carefully - we can't\r\n // re-merge the accumulated values or remap them to their old coordinate system\r\n // afterward.`buildRenormalize`'s remapping maneuver is a one-way stats-abuse trick.\r\n //\r\n // Hint: we'd need to pay attention to the \"lingering segments\" aspects in which\r\n // detected sub-segments might be \"re-merged\".\r\n // - Whenever they're merged & cleared, we should be clear to recentralize\r\n // the cumulative stats that follow. If any are still active, we can't\r\n // recentralize.\r\n\r\n // We actually WILL accept a `null` argument; makes some of the segmentation\r\n // logic simpler.\r\n if(!subsetStats) {\r\n return result;\r\n }\r\n\r\n /* c8 ignore next 3 */\r\n if(!subsetStats.followingSample || !subsetStats.lastSample) {\r\n throw 'Invalid argument: stats missing necessary tracking variable.';\r\n }\r\n\r\n for(let dim in result.rawLinearSums) {\r\n // TS refuses to infer beyond 'string' in a `let... in` construct; we can't\r\n // even assert it directly on `dim` via declaring it early!\r\n const d = dim as PathCoordAxis;\r\n result.rawLinearSums[d] -= subsetStats.rawLinearSums[d];\r\n }\r\n\r\n // arc length stuff!\r\n if(subsetStats.followingSample && subsetStats.lastSample) {\r\n const xDelta = subsetStats.followingSample.targetX - subsetStats.lastSample.targetX;\r\n const yDelta = subsetStats.followingSample.targetY - subsetStats.lastSample.targetY;\r\n\r\n const coordArcDeltaSq = xDelta * xDelta + yDelta * yDelta;\r\n const coordArcDelta = Math.sqrt(coordArcDeltaSq);\r\n\r\n // Due to how arc length stuff gets segmented.\r\n // There's the arc length within the prefix subset (operand 2 below) AND the part connecting it to the\r\n // 'remaining' subset (operand 1 below) before the portion wholly within what remains (the result)\r\n result.coordArcSum -= coordArcDelta;\r\n result.coordArcSum -= subsetStats.coordArcSum;\r\n }\r\n\r\n result.sampleCount -= subsetStats.sampleCount;\r\n\r\n // NOTE: baseSample MUST REMAIN THE SAME. All math is based on the corresponding diff.\r\n // Though... very long touchpoint interactions could start being affected by that \"catastrophic\r\n // cancellation\" effect without further adjustment. (If it matters, we'll get to that later.)\r\n // But _probably_ not; we don't go far beyond a couple of orders of magnitude from the origin in\r\n // ANY case except the timestamp (.t) - and even then, not far from the baseSample's timestamp value.\r\n\r\n // initialSample, though, we need to update b/c of the 'directness' properties.\r\n result._initialSample = subsetStats.followingSample;\r\n\r\n return result;\r\n }\r\n\r\n public translateCoordSystem(functor: (sample: InputSample) => InputSample): CumulativePathStats {\r\n const result = new CumulativePathStats(this);\r\n\r\n return this._translateCoordSystem(result, functor);\r\n }\r\n\r\n protected _translateCoordSystem(result: CumulativePathStats, functor: (sample: InputSample) => InputSample): CumulativePathStats {\r\n if(this.sampleCount == 0) {\r\n return result;\r\n }\r\n\r\n const singleSample = result.initialSample == result.lastSample;\r\n\r\n result._initialSample = functor(result.initialSample);\r\n result.baseSample = functor(result.baseSample);\r\n result._lastSample = singleSample ? result._initialSample : functor(result.lastSample);\r\n\r\n return result;\r\n }\r\n\r\n public replaceInitialSample(sample: InputSample): CumulativePathStats {\r\n let result = new CumulativePathStats(this);\r\n\r\n return this._replaceInitialSample(result, sample);\r\n }\r\n\r\n protected _replaceInitialSample(result: CumulativePathStats, sample: InputSample) {\r\n // if stats length == 0 or length == 1, is ezpz. Could 'shortcut' things here.\r\n if(this.sampleCount == 0) {\r\n // Note: if this error actually causes problems, 'silently failing' the call\r\n // by insta-returning should be \"fine\" as far as actual gesture processing goes.\r\n throw new Error(\"no sample available to replace\");\r\n // return;\r\n }\r\n\r\n // Re: the block above... obviously, don't replace if there IS no initial sample yet.\r\n // It'll happen soon enough anyway.\r\n const originalSample = result.initialSample;\r\n result._initialSample = sample;\r\n\r\n if(this.sampleCount > 1) {\r\n // Works fine re: cata-cancellation - `this.baseSample.___` cancels out.\r\n const xDelta = sample.targetX - originalSample.targetX;\r\n const yDelta = sample.targetY - originalSample.targetY;\r\n const tDelta = sample.t - originalSample.t;\r\n\r\n result.rawLinearSums.x += xDelta;\r\n result.rawLinearSums.y += yDelta;\r\n result.rawLinearSums.t += tDelta;\r\n\r\n /*\r\n * `rawDistance` tracking. Note: this is kind of an approximation, as\r\n * we aren't getting the true distance between the new first and the original\r\n * second point. But... it should be \"good enough\".\r\n *\r\n * If need be, we could always track \"second sample\" to be more precise about things\r\n * here, though that would add a bit more logic overhead at low sample counts.\r\n * (Note the logic interactions inherent in firstSample, secondSample, and lastSample.)\r\n *\r\n * This concern should be a low-priority detail for now - at the time of writing,\r\n * rawDistance is currently only used by KeymanWeb for longpress up-flick thresholding,\r\n * and that codepath doesn't do path-start rewriting.\r\n */\r\n const coordArcDeltaSq = xDelta * xDelta + yDelta * yDelta;\r\n const coordArcDelta = Math.sqrt(coordArcDeltaSq);\r\n\r\n result.coordArcSum += coordArcDelta;\r\n } else {\r\n result._lastSample = sample;\r\n }\r\n\r\n // Do NOT change sampleCount; we're replacing the original.\r\n return result;\r\n }\r\n\r\n public get lastSample() {\r\n return this._lastSample;\r\n }\r\n\r\n public get lastTimestamp(): number {\r\n return this.lastSample?.t;\r\n }\r\n\r\n public get sampleCount() {\r\n return this._sampleCount;\r\n }\r\n\r\n private set sampleCount(value: number) {\r\n this._sampleCount = value;\r\n }\r\n\r\n public get initialSample() {\r\n return this._initialSample;\r\n }\r\n\r\n /**\r\n * In order to mitigate the accumulation of small floating-point errors during the\r\n * various accumulations performed by this class, the domain of incoming values\r\n * is remapped near to the origin via axis-specific mapping constants.\r\n * @param dim\r\n * @returns\r\n */\r\n protected mappingConstant(dim: StatAxis) {\r\n if(!this.baseSample) {\r\n return undefined;\r\n }\r\n\r\n if(dim == 't') {\r\n return this.baseSample.t;\r\n } else if(dim == 'x') {\r\n return this.baseSample.targetX;\r\n } else if(dim == 'y') {\r\n return this.baseSample.targetY;\r\n } else {\r\n return 0;\r\n }\r\n }\r\n\r\n /**\r\n * Gets the statistical mean value of the samples observed during the represented\r\n * interval on the specified axis.\r\n * @param dim\r\n * @returns\r\n */\r\n public mean(dim: StatAxis) {\r\n // This external-facing version needs to provide values in 'external'-friendly\r\n // coordinate space.\r\n return this.rawLinearSums[dim] / this.sampleCount + this.mappingConstant(dim);\r\n }\r\n\r\n /**\r\n * Provides the direct Euclidean distance between the start and end points of the segment\r\n * (or curve) of the interval represented by this instance.\r\n *\r\n * This will likely not match the actual pixel distance traveled.\r\n */\r\n public get netDistance() {\r\n // No issue with a net distance of 0 due to a single point.\r\n if(!this.lastSample || !this.initialSample) {\r\n return 0;\r\n }\r\n\r\n const xDelta = this.lastSample.targetX - this.initialSample.targetX;\r\n const yDelta = this.lastSample.targetY - this.initialSample.targetY;\r\n\r\n return Math.sqrt(xDelta * xDelta + yDelta * yDelta);\r\n }\r\n\r\n /**\r\n * Gets the duration of the represented interval in milliseconds.\r\n */\r\n public get duration() {\r\n // no issue with a duration of zero from just one sample.\r\n if(!this.lastSample || !this.initialSample) {\r\n return 0;\r\n }\r\n return (this.lastSample.t - this.initialSample.t);\r\n }\r\n\r\n /**\r\n * Returns the angle (in radians) traveled by the corresponding segment clockwise\r\n * from the unit vector <0, -1> in the DOM (the unit \"upward\" direction).\r\n */\r\n public get angle() {\r\n if(this.sampleCount == 1 || !this.lastSample || !this.initialSample) {\r\n return undefined;\r\n } else if(this.netDistance < 1) {\r\n // < 1 px, thus sub-pixel, means we have nothing relevant enough to base an angle on.\r\n return undefined;\r\n }\r\n\r\n const xDelta = this.lastSample.targetX - this.initialSample.targetX;\r\n const yDelta = this.lastSample.targetY - this.initialSample.targetY;\r\n const yAngleDiff = Math.acos(-yDelta / this.netDistance);\r\n\r\n return xDelta < 0 ? (2 * Math.PI - yAngleDiff) : yAngleDiff;\r\n }\r\n\r\n /**\r\n * Returns the angle (in degrees) traveled by the corresponding segment clockwise\r\n * from the unit vector <0, -1> in the DOM (the unit \"upward\" direction).\r\n */\r\n public get angleInDegrees() {\r\n return this.angle * 180 / Math.PI;\r\n }\r\n\r\n /**\r\n * Returns the cardinal or intercardinal direction on the screen that most\r\n * closely matches the direction of movement represented by the represented\r\n * segment.\r\n *\r\n * @return A string one or two letters in length (e.g: 'n', 'sw'), or\r\n `undefined` if not enough data to determine a direction.\r\n */\r\n public get cardinalDirection() {\r\n if(this.sampleCount == 1 || !this.lastSample || !this.initialSample) {\r\n return undefined;\r\n }\r\n\r\n if(isNaN(this.angle) || this.angle === null || this.angle === undefined) {\r\n return undefined;\r\n }\r\n\r\n const buckets = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'n'];\r\n\r\n // We could be 'more efficient' and use radians here instead, but this\r\n // version helps a bit more with easy maintainability.\r\n const bucketIndex = Math.ceil((this.angleInDegrees - 22.5)/45);\r\n return buckets[bucketIndex];\r\n }\r\n\r\n /**\r\n * Measured in pixels per second.\r\n * @return a speed in pixels per millisecond. May be 0 if no movement was observed\r\n * among the samples.\r\n */\r\n public get speed() {\r\n return this.duration ? this.netDistance / this.duration : 0;\r\n }\r\n\r\n /**\r\n * Provides the actual, pixel-based distance actually traveled by the represented segment.\r\n * May not be an integer (because diagonals are a thing).\r\n */\r\n public get rawDistance() {\r\n return this.coordArcSum;\r\n }\r\n\r\n /* c8 ignore start */\r\n /**\r\n * Provides a JSON.stringify()-friendly object with the properties most useful for\r\n * debugger-based inspection and/or console-logging statements.\r\n */\r\n public toJSON() {\r\n return {\r\n angle: this.angle,\r\n cardinal: this.cardinalDirection,\r\n netDistance: this.netDistance,\r\n duration: this.duration,\r\n sampleCount: this.sampleCount,\r\n rawDistance: this.rawDistance\r\n }\r\n }\r\n /* c8 ignore end */\r\n}", + "import * as gestures from \"../index.js\";\r\n\r\nexport interface GestureModelDefs {\r\n /**\r\n * The full set of gesture models to be utilized by the gesture-recognition engine.\r\n */\r\n gestures: gestures.specs.GestureModel[],\r\n\r\n /**\r\n * Sets _of sets_ of gesture models accessible as initial gesture stages while\r\n * within various states of the gesture-engine.\r\n *\r\n * `'default'` must be specified, as it is the default state.\r\n *\r\n * A 'chain'-type model resolution has the option to specify a `selectionMode` property;\r\n * the value set there will activate a different gesture-recognition mode _for new\r\n * gestures_ corresponding to the sets specified here.\r\n *\r\n * These sets may be defined to either restrict the range of options for new incoming\r\n * gestures or to restrict them. Specifying an empty set will disable all incoming\r\n * gestures while the alternate state is active, allowing one gesture to block any\r\n * further gestures from starting until it is completed.\r\n */\r\n sets: {\r\n default: string[],\r\n } & Record;\r\n}\r\n\r\n\r\nexport function getGestureModel(defs: GestureModelDefs, id: string): gestures.specs.GestureModel {\r\n const result = defs.gestures.find((spec) => spec.id == id);\r\n if(!result) {\r\n throw new Error(`Could not find spec for gesture with id '${id}'`);\r\n }\r\n\r\n return result;\r\n}\r\n\r\nexport function getGestureModelSet(defs: GestureModelDefs, id: string): gestures.specs.GestureModel[] {\r\n let idSet = defs.sets[id];\r\n if(!idSet) {\r\n throw new Error(`Could not find a defined gesture-set with id '${id}'`);\r\n }\r\n\r\n const set = defs.gestures.filter((spec) => !!idSet.find((id) => spec.id == id));\r\n const missing = idSet.filter((id) => !set.find((spec) => spec.id == id));\r\n\r\n if(missing.length > 0) {\r\n throw new Error(`Set '${id}' cannot find definitions for gestures with ids ${missing}`);\r\n }\r\n\r\n return set;\r\n}\r\n\r\nexport const EMPTY_GESTURE_DEFS = {\r\n gestures: [\r\n ],\r\n sets: {\r\n default: []\r\n }\r\n} as GestureModelDefs\r\n", + "import { RecognitionZoneSource } from \"./recognitionZoneSource.js\";\r\n\r\nexport class ViewportZoneSource implements RecognitionZoneSource {\r\n constructor() {}\r\n\r\n getBoundingClientRect(): DOMRect {\r\n // Viewport dimension detection is based on https://stackoverflow.com/a/8876069.\r\n return new DOMRect(\r\n /*x:*/ 0,\r\n /*y:*/ 0,\r\n /*width:*/ Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0),\r\n /*height:*/ Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)\r\n );\r\n }\r\n}", + "import { RecognitionZoneSource } from \"./recognitionZoneSource.js\";\r\nimport { ViewportZoneSource } from \"./viewportZoneSource.js\";\r\n\r\nexport class PaddedZoneSource implements RecognitionZoneSource {\r\n private readonly root: RecognitionZoneSource;\r\n\r\n private _edgePadding: {\r\n x: number,\r\n y: number,\r\n w: number,\r\n h: number\r\n };\r\n\r\n public get edgePadding() {\r\n return this._edgePadding;\r\n }\r\n\r\n /**\r\n * Provides a dynamic 'padded' recognition zone based upon offsetting from the borders\r\n * of the active page's viewport.\r\n *\r\n * Padding is defined using the standard CSS border & padding spec style:\r\n * - [a]: equal and even padding on all sides\r\n * - [a, b]: top & bottom use `a`, left & right use `b`\r\n * - [a, b, c]: top uses `a`, left & right use `b`, bottom uses `c`\r\n * - [a, b, c, d]: top, right, bottom, then left.\r\n *\r\n * Positive padding reduces the size of the resulting zone; negative padding expands it.\r\n *\r\n * @param rootZoneSource The root zone source object/element to be 'padded'\r\n * @param edgePadding A set of 1 to 4 numbers defining padding per the standard CSS border & padding spec style.\r\n */\r\n public constructor(edgePadding: number[]);\r\n /**\r\n * Provides a dynamic 'padded' recognition zone based upon offsetting from the borders\r\n * of another defined zone.\r\n *\r\n * Padding is defined using the standard CSS border & padding spec style:\r\n * - [a]: equal and even padding on all sides\r\n * - [a, b]: top & bottom use `a`, left & right use `b`\r\n * - [a, b, c]: top uses `a`, left & right use `b`, bottom uses `c`\r\n * - [a, b, c, d]: top, right, bottom, then left.\r\n *\r\n * Positive padding reduces the size of the resulting zone; negative padding expands it.\r\n *\r\n * @param rootZoneSource The root zone source object/element to be 'padded'\r\n * @param edgePadding A set of 1 to 4 numbers defining padding per the standard CSS border & padding spec style.\r\n */\r\n public constructor(rootZoneSource: RecognitionZoneSource, edgePadding?: number[]);\r\n public constructor(rootZoneSource: RecognitionZoneSource | number[], edgePadding?: number[]) {\r\n // Disambiguate which constructor style was intended.\r\n if(Array.isArray(rootZoneSource)) {\r\n edgePadding = rootZoneSource;\r\n rootZoneSource = new ViewportZoneSource();\r\n }\r\n\r\n this.root = rootZoneSource;\r\n // In case it isn't yet defined.\r\n edgePadding = edgePadding || [0, 0, 0, 0];\r\n\r\n this.updatePadding(edgePadding);\r\n }\r\n\r\n /**\r\n * Provides a dynamic 'padded' recognition zone based upon offsetting from the borders\r\n * of another defined zone.\r\n *\r\n * Padding is defined using the standard CSS border & padding spec style:\r\n * - [a]: equal and even padding on all sides\r\n * - [a, b]: top & bottom use `a`, left & right use `b`\r\n * - [a, b, c]: top uses `a`, left & right use `b`, bottom uses `c`\r\n * - [a, b, c, d]: top, right, bottom, then left.\r\n *\r\n * Positive padding reduces the size of the resulting zone; negative padding expands it.\r\n *\r\n * @param rootZoneSource The root zone source object/element to be 'padded'\r\n * @param edgePadding A set of 1 to 4 numbers defining padding per the standard CSS border & padding spec style.\r\n */\r\n updatePadding(edgePadding: number[]) {\r\n\r\n // Modeled after CSS styling definitions... just with preprocessed numbers, not strings.\r\n switch(edgePadding.length) {\r\n case 1:\r\n // all sides equal\r\n const val = edgePadding[0];\r\n this._edgePadding = {\r\n x: val,\r\n y: val,\r\n w: 2 * val,\r\n h: 2 * val\r\n };\r\n break;\r\n case 2:\r\n // top & bottom, left & right\r\n this._edgePadding = {\r\n x: edgePadding[1],\r\n y: edgePadding[0],\r\n w: 2 * edgePadding[1],\r\n h: 2 * edgePadding[0]\r\n };\r\n break;\r\n case 3:\r\n // top, left & right, bottom\r\n this._edgePadding = {\r\n x: edgePadding[1],\r\n y: edgePadding[0],\r\n w: 2 * edgePadding[1],\r\n h: edgePadding[0] + edgePadding[2]\r\n };\r\n break;\r\n case 4:\r\n // top, right, bottom, left\r\n this._edgePadding = {\r\n x: edgePadding[3],\r\n y: edgePadding[0],\r\n w: edgePadding[1] + edgePadding[3],\r\n h: edgePadding[0] + edgePadding[2]\r\n }\r\n break;\r\n default:\r\n throw new Error(\"Invalid values for PaddedZoneSource's edgePadding - must be between 1 to 4 `number` values.\");\r\n }\r\n }\r\n\r\n getBoundingClientRect(): DOMRect {\r\n const rootZone = this.root.getBoundingClientRect();\r\n\r\n // Chrome 35: x, y do not exist on the returned rect, but left & top do.\r\n return new DOMRect(\r\n /*x:*/ rootZone.left + this.edgePadding.x,\r\n /*y:*/ rootZone.top + this.edgePadding.y,\r\n /*width:*/ rootZone.width - this.edgePadding.w,\r\n /*height:*/ rootZone.height - this.edgePadding.h\r\n );\r\n }\r\n}", + "// Note: we may add properties in the future that aren't explicitly readonly;\r\n// it's just that the ELEMENTS and zone definitions involved shouldn't be shifting\r\n// after configuration.\r\n\r\nimport { InputSample } from \"../headless/inputSample.js\";\r\nimport { Mutable } from \"../mutable.js\";\r\nimport { Nonoptional } from \"../nonoptional.js\";\r\nimport { PaddedZoneSource } from \"./paddedZoneSource.js\";\r\nimport { RecognitionZoneSource } from \"./recognitionZoneSource.js\";\r\n\r\nexport type ItemIdentifier = (coord: Omit, 'item'>, target: EventTarget) => ItemType;\r\n\r\n// For example, customization of a longpress timer's length need not be readonly.\r\nexport interface GestureRecognizerConfiguration {\r\n /**\r\n * Specifies the element that mouse input listeners should be attached to. If\r\n * not specified, `eventRoot` will be set equal to `targetRoot`.\r\n */\r\n readonly mouseEventRoot?: HTMLElement;\r\n\r\n /**\r\n * Specifies the element that touch input listeners should be attached to. If\r\n * not specified, `eventRoot` will be set equal to `targetRoot`.\r\n */\r\n readonly touchEventRoot?: HTMLElement;\r\n\r\n /**\r\n * Specifies the most specific common ancestor element of any event target\r\n * that the `InputEventEngine` should consider.\r\n */\r\n readonly targetRoot: HTMLElement;\r\n\r\n /**\r\n * A boundary constraining the legal coordinates for supported touchstart and mousedown\r\n * events. If not specified, this will be set to `targetRoot`.\r\n */\r\n readonly inputStartBounds?: RecognitionZoneSource;\r\n\r\n /**\r\n * A boundary constraining the maximum range that an ongoing input may travel before it\r\n * is forceably canceled. If not specified, this will be set to `targetRoot`.\r\n */\r\n readonly maxRoamingBounds?: RecognitionZoneSource;\r\n\r\n /**\r\n * A boundary constraining the \"safe range\" for ongoing touch events. Events that leave a\r\n * safe boundary that did not start outside its respective \"padded\" bound will be canceled.\r\n *\r\n * If not specified, this will be based on the active viewport, padded internally by 2px on\r\n * all sides.\r\n */\r\n readonly safeBounds?: RecognitionZoneSource;\r\n\r\n /**\r\n * Used to define a \"boundary\" slightly more constrained than `safeBounds`. Events that\r\n * start within this pixel range from a safe bound will disable that bound for the duration\r\n * of its corresponding input sequence. May be a number or an array of 1, 2, or 4 numbers,\r\n * as with CSS styling.\r\n *\r\n * If not specified, this will default to a padding of 3px inside the standard safeBounds\r\n * unless `paddedSafeBounds` is defined.\r\n *\r\n * If `paddedSafeBounds` was specified initially, this will be set to `undefined`.\r\n */\r\n readonly safeBoundPadding?: number | number[];\r\n\r\n /**\r\n * Used to define when an input coordinate is \"close\" to `safeBounds` borders via exclusion.\r\n * If this is not defined while `safeBoundPadding` is, this will be built automatically to\r\n * match the spec set by `safeBoundPadding`.\r\n *\r\n * Defining this directly will cause `safeBoundPadding` to be ignored in favor of the bounds\r\n * set here.\r\n */\r\n readonly paddedSafeBounds?: RecognitionZoneSource;\r\n\r\n /**\r\n * Allows the gesture-recognizer client to specify the most relevant, identifying UI \"item\"\r\n * (as perceived by users / relevant for gesture discrimination) underneath the touchpoint's\r\n * current location based when processing input events.\r\n *\r\n * For applications in the DOM, simply returning `target` itself may be sufficient.\r\n * @param coord The current touchpath coordinate; its .targetX and .targetY values should be\r\n * interpreted as offsets from `targetRoot`.\r\n *\r\n * Its `stateToken` will match the most recently set value for its corresponding\r\n * `GestureSource` if continuing one; otherwise, it'll use the one currently set\r\n * at the gesture-engine level.\r\n * @param target The `EventTarget` (`Node` or `Element`) provided by the corresponding input event,\r\n * if available. May be `null/undefined`.\r\n * @returns\r\n */\r\n readonly itemIdentifier?: ItemIdentifier;\r\n\r\n /**\r\n * When `true`, the engine will persistently record all coordinates visited by each `GestureSource`\r\n * during its lifetime. This is useful for debugging and for generating input recordings for\r\n * use in automated testing.\r\n */\r\n readonly recordingMode?: boolean;\r\n\r\n /**\r\n * If greater than zero, preserves this amount of previously-seen touches and gestures before\r\n * permanently clearing them.\r\n */\r\n readonly historyLength?: number;\r\n}\r\n\r\nexport function preprocessRecognizerConfig(\r\n config: GestureRecognizerConfiguration\r\n): Nonoptional> {\r\n // Allows configuration pre-processing during this method.\r\n let processingConfig: Mutable>> = {...config} as\r\n Nonoptional>;\r\n\r\n processingConfig.mouseEventRoot = processingConfig.mouseEventRoot ?? processingConfig.targetRoot;\r\n processingConfig.touchEventRoot = processingConfig.touchEventRoot ?? processingConfig.targetRoot;\r\n\r\n processingConfig.inputStartBounds = processingConfig.inputStartBounds ?? processingConfig.targetRoot;\r\n processingConfig.maxRoamingBounds = processingConfig.maxRoamingBounds ?? processingConfig.targetRoot;\r\n processingConfig.safeBounds = processingConfig.safeBounds ?? new PaddedZoneSource([2]);\r\n\r\n processingConfig.itemIdentifier = processingConfig.itemIdentifier ?? (() => null);\r\n processingConfig.recordingMode = !!processingConfig.recordingMode;\r\n processingConfig.historyLength = (processingConfig.historyLength ?? 0) > 0 ? processingConfig.historyLength : 0;\r\n\r\n if(!config.paddedSafeBounds) {\r\n let paddingArray = config.safeBoundPadding;\r\n if(typeof paddingArray == 'number') {\r\n paddingArray = [ paddingArray ];\r\n }\r\n paddingArray = paddingArray ?? [3];\r\n\r\n processingConfig.paddedSafeBounds = new PaddedZoneSource(processingConfig.safeBounds, paddingArray);\r\n } else {\r\n // processingConfig.paddedSafeBounds is already set via the spread operator above.\r\n delete processingConfig.safeBoundPadding;\r\n }\r\n\r\n return processingConfig;\r\n}", + "import { EventEmitter } from \"eventemitter3\";\r\nimport { InputSample } from \"./inputSample.js\";\r\nimport { CumulativePathStats } from \"./cumulativePathStats.js\";\r\n\r\ninterface EventMap {\r\n 'step': (sample: InputSample) => void,\r\n 'complete': () => void,\r\n 'invalidated': () => void\r\n}\r\n\r\n/**\r\n * Models the path over time through coordinate space taken by a touchpoint during\r\n * its active lifetime.\r\n *\r\n * _Supported events_:\r\n *\r\n * `'step'`: a new Event has been observed for this touchpoint, extending the path.\r\n * - Parameters:\r\n * - `sample: InputSample` - the coordinate & timestamp of the new observation.\r\n *\r\n * `'complete'`: the touchpoint is no longer active; a touch-end has been observed.\r\n * - Provides no parameters.\r\n * - Will be the last event raised by its instance, after any final 'segmentation'\r\n * events.\r\n * - Still precedes resolution Promise fulfillment on the `Segment` provided by\r\n * the most recently-preceding 'segmentation' event.\r\n * - And possibly recognition Promise fulfillment.\r\n *\r\n * `'invalidated'`: the touchpoint is no longer active; the path has crossed\r\n * gesture-recognition boundaries and is no longer considered valid.\r\n * - Provides no parameters.\r\n * - Will precede the final 'segmentation' event for the 'end' segment\r\n * - Will precede resolution Promise fulfillment on the `Segment` provided by\r\n * the most recently-preceding 'segmentation' event.\r\n * - And possibly recognition Promise fulfillment.\r\n */\r\nexport class GesturePath extends EventEmitter> {\r\n protected _isComplete: boolean = false;\r\n protected _wasCancelled?: boolean;\r\n\r\n protected _stats: CumulativePathStats;\r\n\r\n public get stats() {\r\n // Is (practically) immutable, so it's safe to expose the instance directly.\r\n return this._stats;\r\n }\r\n\r\n /**\r\n * Initializes an empty path intended for tracking a newly-activated touchpoint.\r\n */\r\n constructor() {\r\n super();\r\n\r\n this._stats = new CumulativePathStats();\r\n }\r\n\r\n public clone(): GesturePath {\r\n const instance = new GesturePath();\r\n instance._isComplete = this._isComplete;\r\n instance._wasCancelled = this._wasCancelled;\r\n instance._stats = new CumulativePathStats(this._stats);\r\n\r\n return instance;\r\n }\r\n\r\n /**\r\n * Indicates whether or not the corresponding touchpoint is no longer active -\r\n * either due to cancellation or by the user's direct release of the touchpoint.\r\n */\r\n public get isComplete() {\r\n return this._isComplete;\r\n }\r\n\r\n public get wasCancelled() {\r\n return this._wasCancelled;\r\n }\r\n\r\n /**\r\n * Builds a new instance with equal stats and with translated initialSample and\r\n * lastSample coordinates. Further accumulation will be based upon the new\r\n * coordinate system as well.\r\n * @param functor\r\n */\r\n public translateCoordSystem(functor: (sample: InputSample) => InputSample) {\r\n this._stats = this._stats.translateCoordSystem(functor);\r\n }\r\n\r\n /**\r\n * Builds a new instance with its initial sample replaced and stats updated\r\n * to reflect the alternate starting position.\r\n *\r\n * Note that `rawDistance` adjustments are an approximation, not exact. To\r\n * be precise, for stats representing two more more samples, the distance\r\n * between the original and new initial samples is added as a flat amount.\r\n * @param sample\r\n */\r\n public replaceInitialSample(sample: InputSample) {\r\n this._stats = this._stats.replaceInitialSample(sample);\r\n }\r\n\r\n /**\r\n * Extends the path with a newly-observed coordinate.\r\n * @param sample\r\n */\r\n extend(sample: InputSample) {\r\n /* c8 ignore next 3 */\r\n if(this._isComplete) {\r\n throw new Error(\"Invalid state: this GesturePath has already terminated.\");\r\n }\r\n\r\n // The tracked path should emit InputSample events before Segment events and\r\n // resolution of Segment Promises.\r\n this._stats = this._stats.extend(sample);\r\n this.emit('step', sample);\r\n }\r\n\r\n /**\r\n * Finalizes the path.\r\n * @param cancel Whether or not this finalization should trigger cancellation.\r\n */\r\n terminate(cancel: boolean = false) {\r\n /* c8 ignore next 3 */\r\n if(this._isComplete) {\r\n return;\r\n }\r\n\r\n this._wasCancelled = cancel;\r\n this._isComplete = true;\r\n\r\n // If cancelling, do so before finishing segments\r\n if(cancel) {\r\n this.emit('invalidated');\r\n } else {\r\n // If not cancelling, signal completion after finishing segments.\r\n this.emit('complete');\r\n }\r\n\r\n this.removeAllListeners();\r\n }\r\n\r\n public toJSON(): any {\r\n return {\r\n // Replicate array and its entries, but with certain fields of each entry missing.\r\n // No .clientX, no .clientY.\r\n stats: this.stats,\r\n wasCancelled: this.wasCancelled\r\n }\r\n }\r\n}", + "import { InputSample } from \"./inputSample.js\";\r\nimport { CumulativePathStats } from \"./cumulativePathStats.js\";\r\nimport { Mutable } from \"../mutable.js\";\r\nimport { GesturePath } from \"./gesturePath.js\";\r\n\r\n/**\r\n * Documents the expected typing of serialized versions of the `GesturePath` class.\r\n */\r\nexport type SerializedGesturePath = {\r\n coords: Mutable>[]; // ensures type match with public class property.\r\n wasCancelled?: boolean;\r\n stats?: CumulativePathStats\r\n}\r\n\r\n/**\r\n * Models the path over time through coordinate space taken by a touchpoint during\r\n * its active lifetime.\r\n *\r\n * _Supported events_:\r\n *\r\n * `'step'`: a new Event has been observed for this touchpoint, extending the path.\r\n * - Parameters:\r\n * - `sample: InputSample` - the coordinate & timestamp of the new observation.\r\n *\r\n * `'complete'`: the touchpoint is no longer active; a touch-end has been observed.\r\n * - Provides no parameters.\r\n * - Will be the last event raised by its instance, after any final 'segmentation'\r\n * events.\r\n * - Still precedes resolution Promise fulfillment on the `Segment` provided by\r\n * the most recently-preceding 'segmentation' event.\r\n * - And possibly recognition Promise fulfillment.\r\n *\r\n * `'invalidated'`: the touchpoint is no longer active; the path has crossed\r\n * gesture-recognition boundaries and is no longer considered valid.\r\n * - Provides no parameters.\r\n * - Will precede the final 'segmentation' event for the 'end' segment\r\n * - Will precede resolution Promise fulfillment on the `Segment` provided by\r\n * the most recently-preceding 'segmentation' event.\r\n * - And possibly recognition Promise fulfillment.\r\n */\r\nexport class GestureDebugPath extends GesturePath {\r\n private samples: InputSample[] = [];\r\n\r\n public clone(): GestureDebugPath {\r\n const instance = new GestureDebugPath();\r\n instance.samples = [].concat(this.samples);\r\n\r\n instance._isComplete = this._isComplete;\r\n instance._wasCancelled = this._wasCancelled;\r\n instance._stats = new CumulativePathStats(this._stats);\r\n\r\n return instance;\r\n }\r\n\r\n /**\r\n * Deserializes a GesturePath instance from its corresponding JSON.parse() object.\r\n * @param jsonObj\r\n */\r\n static deserialize(jsonObj: SerializedGesturePath): GestureDebugPath {\r\n const instance = new GestureDebugPath();\r\n\r\n instance.samples = [].concat(jsonObj.coords.map((obj) => ({...obj} as InputSample)));\r\n instance._isComplete = true;\r\n instance._wasCancelled = jsonObj.wasCancelled;\r\n\r\n let stats = instance.samples.reduce((stats: CumulativePathStats, sample) => stats.extend(sample), new CumulativePathStats());\r\n instance._stats = stats;\r\n\r\n return instance;\r\n }\r\n\r\n /**\r\n * Extends the path with a newly-observed coordinate.\r\n * @param sample\r\n */\r\n extend(sample: InputSample) {\r\n /* c8 ignore next 3 */\r\n if(this.isComplete) {\r\n throw new Error(\"Invalid state: this GesturePath has already terminated.\");\r\n }\r\n\r\n // The tracked path should emit InputSample events before Segment events and\r\n // resolution of Segment Promises.\r\n this.samples.push(sample);\r\n super.extend(sample);\r\n }\r\n\r\n\r\n public translateCoordSystem(functor: (sample: InputSample) => InputSample) {\r\n super.translateCoordSystem(functor);\r\n\r\n for(let i=0; i < this.samples.length; i++) {\r\n this.samples[i] = functor(this.samples[i]);\r\n }\r\n }\r\n\r\n /**\r\n * Returns all coordinate + timestamp pairings observed for the corresponding\r\n * touchpoint's path over its lifetime thus far.\r\n */\r\n public get coords(): readonly InputSample[] {\r\n return this.samples;\r\n }\r\n\r\n /**\r\n * Creates a serialization-friendly version of this instance for use by\r\n * `JSON.stringify`.\r\n */\r\n toJSON() {\r\n let jsonClone: SerializedGesturePath = {\r\n // Replicate array and its entries, but with certain fields of each entry missing.\r\n // No .clientX, no .clientY.\r\n coords: [].concat(this.samples.map((obj) => ({\r\n targetX: obj.targetX,\r\n targetY: obj.targetY,\r\n t: obj.t,\r\n item: obj.item\r\n }))),\r\n wasCancelled: this.wasCancelled,\r\n stats: this.stats\r\n }\r\n\r\n // Removes components of each sample that we don't want serialized.\r\n for(let sample of jsonClone.coords) {\r\n delete sample.clientX;\r\n delete sample.clientY;\r\n\r\n // No point in serializing an `undefined` 'item' entry.\r\n if(sample.item === undefined) {\r\n delete sample.item;\r\n }\r\n }\r\n\r\n return jsonClone;\r\n }\r\n}", + "import { InputSample } from \"./inputSample.js\";\r\nimport { GesturePath } from \"./gesturePath.js\";\r\nimport { GestureRecognizerConfiguration, preprocessRecognizerConfig } from \"../configuration/gestureRecognizerConfiguration.js\";\r\nimport { Nonoptional } from \"../nonoptional.js\";\r\nimport { MatcherSelector } from \"./gestures/matchers/matcherSelector.js\";\r\nimport { SerializedGesturePath } from \"./gestureDebugPath.js\";\r\n\r\nexport function buildGestureMatchInspector(selector: MatcherSelector) {\r\n return (source: GestureSource) => {\r\n return selector.potentialMatchersForSource(source).map((matcher) => matcher.model.id);\r\n };\r\n}\r\n\r\n/**\r\n * Documents the expected typing of serialized versions of the `GestureSource` class.\r\n */\r\nexport type SerializedGestureSource = {\r\n isFromTouch: boolean;\r\n path: SerializedGesturePath;\r\n stateToken?: StateToken;\r\n identifier?: string;\r\n // identifier is not included b/c it's only needed during live processing.\r\n}\r\n\r\n\r\n/**\r\n * Represents all metadata needed internally for tracking a single \"touch contact point\" / \"touchpoint\"\r\n * involved in a potential / recognized gesture as tracked over time.\r\n *\r\n * Each instance corresponds to one unique contact point as recognized by `Touch.identifier` or to\r\n * one 'cursor-point' as represented by mouse-based motion.\r\n *\r\n * Refer to https://developer.mozilla.org/en-US/docs/Web/API/Touch and\r\n * https://developer.mozilla.org/en-US/docs/Web/API/Navigator/maxTouchPoints re \"touch contact point\".\r\n *\r\n * May be one-to-many with recognized gestures: a keyboard longpress interaction generally only has one\r\n * contact point but will have multiple realized gestures / components:\r\n * - longpress: Enough time has elapsed\r\n * - subkey: Subkey from the longpress subkey menu has been selected.\r\n *\r\n * Thus, it is a \"gesture source\". This is the level needed to model a single contact point, while some\r\n * gestures expect multiple, hence \"simple\".\r\n *\r\n */\r\nexport class GestureSource<\r\n HoveredItemType,\r\n StateToken=any,\r\n /**\r\n * Not intended for non-default types outside of gesture-recognizer internals. Is used to facilitate\r\n * 'debug-mode' / 'recording-mode' paths.\r\n */\r\n PathType extends GesturePath = GesturePath\r\n> {\r\n /**\r\n * Indicates whether or not this tracked point's original source is a DOM `Touch`.\r\n */\r\n public readonly isFromTouch: boolean;\r\n\r\n /**\r\n * The numeric form of this point's identifier as seen in events (or as emulated for mouse events)\r\n */\r\n public readonly rawIdentifier: number;\r\n\r\n // A full, uninterrupted recording of all samples observed during the lifetime of the touchpoint.\r\n protected _path: PathType;\r\n\r\n protected _baseItem: HoveredItemType;\r\n\r\n // Assertion: must always contain an index 0 - the base recognizer config.\r\n protected recognizerConfigStack: Nonoptional>[];\r\n\r\n /**\r\n * Usable by the gesture-recognizer library's consumer to track a token identifying specific states\r\n * of the consuming system if desired.\r\n */\r\n public stateToken: StateToken = null;\r\n\r\n /**\r\n * Tracks the coordinates and timestamps of each update for the lifetime of this `GestureSource`.\r\n */\r\n public get path(): PathType {\r\n return this._path;\r\n }\r\n\r\n /**\r\n * Allows the GestureSource to report on its remaining potential GestureModel matches for the\r\n * current gesture stage.\r\n *\r\n * Would be nice to have it required in the constructor, but that would greatly complicate certain\r\n * automated testing patterns.\r\n */\r\n private _matchInspectionClosure: (source: GestureSource) => string[];\r\n\r\n /**\r\n * For internal gesture-engine use only. Will throw an error if called more than once during the\r\n * GestureSource's lifetime.\r\n */\r\n public setGestureMatchInspector(closure: typeof GestureSource.prototype._matchInspectionClosure) {\r\n if(this._matchInspectionClosure) {\r\n throw new Error(\"Invalid state: the match-inspection closure has already been set\");\r\n }\r\n\r\n this._matchInspectionClosure = closure;\r\n }\r\n\r\n /**\r\n * Constructs a new GestureSource instance for tracking updates to an active input point over time.\r\n * @param identifier The system identifier for the input point's events.\r\n * @param initialHoveredItem The initiating event's original target element\r\n * @param isFromTouch `true` if sourced from a `TouchEvent`; `false` otherwise.\r\n * @param pathConstructor A default, parameterless constructor for the `GesturePath` variant\r\n * specified for the `PathType` generic parameter.\r\n */\r\n constructor(\r\n identifier: number,\r\n recognizerConfig: Nonoptional>\r\n | Nonoptional>[],\r\n isFromTouch: boolean,\r\n // Sadly, `typeof PathType` isn't TS-legal. This is the best way forward as a result.\r\n pathConstructor?: typeof GesturePath\r\n ) {\r\n this.rawIdentifier = identifier;\r\n this.isFromTouch = isFromTouch;\r\n this._path = (pathConstructor ? new pathConstructor() : new GesturePath()) as PathType;\r\n\r\n this.recognizerConfigStack = Array.isArray(recognizerConfig) ? recognizerConfig : [recognizerConfig];\r\n }\r\n\r\n public update(sample: InputSample) {\r\n this.path.extend(sample);\r\n this._baseItem ||= sample.item;\r\n }\r\n\r\n /**\r\n * The 'base item' for the path of this `GestureSource`.\r\n *\r\n * May be set independently after construction for cases where one GestureSource conceptually\r\n * \"succeeds\" another one, as with multitap gestures. (Though, those generally constrain\r\n * new paths to have the same base item.)\r\n */\r\n public get baseItem(): HoveredItemType {\r\n return this._baseItem;\r\n }\r\n\r\n public set baseItem(value: HoveredItemType) {\r\n this._baseItem = value;\r\n }\r\n\r\n /**\r\n * The most recent path sample (coordinate) under consideration for this `GestureSource`.\r\n */\r\n public get currentSample(): InputSample {\r\n return this.path.stats.lastSample;\r\n }\r\n\r\n /**\r\n * Returns an array of IDs for gesture models that are still valid for the `GestureSource`'s\r\n * current state. They will be specified in descending `resolutionPriority` order.\r\n */\r\n public get potentialModelMatchIds(): string[] {\r\n return this._matchInspectionClosure(this);\r\n }\r\n\r\n /**\r\n * Creates a 'subview' of the current GestureSource. It will be updated as the underlying\r\n * source continues to receive updates until disconnected.\r\n *\r\n * @param startAtEnd If `true`, the 'subview' will appear to start at the most recently-observed\r\n * path coordinate. If `false`, it will have full knowledge of the current path.\r\n * @param preserveBaseItem If `true`, the 'subview' will denote its base item as the same\r\n * as its source. If `false`, the base item for the 'subview' will be set to the `item` entry\r\n * from the most recently-observed path coordinate.\r\n * @param stateTokenOverride Setting this to a 'truthy' value will remap all included samples, using that as\r\n * the new state token.\r\n * @returns\r\n */\r\n public constructSubview(\r\n startAtEnd: boolean,\r\n preserveBaseItem: boolean,\r\n stateTokenOverride?: StateToken\r\n ): GestureSourceSubview {\r\n return new GestureSourceSubview(this, this.recognizerConfigStack, startAtEnd, preserveBaseItem, stateTokenOverride);\r\n }\r\n\r\n /**\r\n * Terminates all tracking for the modeled contact point. Passing `true` as a parameter will\r\n * treat the touchpath as if it were cancelled; `false` and `undefined` will treat it as if\r\n * the touchpath has completed its standard lifecycle.\r\n * @param cancel\r\n */\r\n public terminate(cancel?: boolean) {\r\n this.path.terminate(cancel);\r\n }\r\n\r\n /**\r\n * Denotes if the contact point's path either was cancelled or completed its standard\r\n * lifecycle.\r\n */\r\n public get isPathComplete(): boolean {\r\n return this.path.isComplete;\r\n }\r\n\r\n /**\r\n * Gets a fully-unique string-based identifier, even for edge cases where both mouse and touch input\r\n * are received simultaneously.\r\n */\r\n public get identifier(): string {\r\n const prefix = this.isFromTouch ? 'touch' : 'mouse';\r\n return `${prefix}:${this.rawIdentifier}`;\r\n }\r\n\r\n public pushRecognizerConfig(config: Omit, 'touchEventRoot'| 'mouseEventRoot'>) {\r\n const configToProcess = {...config,\r\n mouseEventRoot: this.recognizerConfigStack[0].mouseEventRoot,\r\n touchEventRoot: this.recognizerConfigStack[0].touchEventRoot\r\n }\r\n this.recognizerConfigStack.push(preprocessRecognizerConfig(configToProcess));\r\n }\r\n\r\n public popRecognizerConfig() {\r\n if(this.recognizerConfigStack.length == 1) {\r\n throw new Error(\"Cannot 'pop' the original recognizer-configuration for this GestureSource.\")\r\n }\r\n\r\n return this.recognizerConfigStack.pop();\r\n }\r\n\r\n public get currentRecognizerConfig() {\r\n return this.recognizerConfigStack[this.recognizerConfigStack.length-1];\r\n }\r\n\r\n /**\r\n * Creates a serialization-friendly version of this instance for use by\r\n * `JSON.stringify`.\r\n */\r\n /* c8 ignore start */\r\n toJSON(): SerializedGestureSource {\r\n let jsonClone: SerializedGestureSource = {\r\n identifier: this.identifier,\r\n isFromTouch: this.isFromTouch,\r\n path: this.path.toJSON(),\r\n stateToken: this.stateToken\r\n };\r\n\r\n return jsonClone;\r\n /* c8 ignore stop */\r\n /* c8 ignore next 2 */\r\n // esbuild or tsc seems to mangle the 'ignore stop' if put outside the ending brace.\r\n }\r\n}\r\n\r\nexport class GestureSourceSubview<\r\n HoveredItemType,\r\n StateToken = any,\r\n PathType extends GesturePath = GesturePath\r\n> extends GestureSource {\r\n private _baseSource: GestureSource\r\n private _baseStartIndex: number;\r\n private subviewDisconnector: () => void;\r\n\r\n /**\r\n * Constructs a new \"Subview\" into an existing GestureSource instance. Future updates of the base\r\n * GestureSource will automatically be included until this instance's `disconnect` method is called.\r\n * @param source The \"original\" GestureSource for this \"subview\".\r\n * @param configStack `source.recognizerConfigStack`. Must be separately provided due to TS limitations.\r\n * @param startAtEnd `true` if only the latest sample should be included in the \"subview\".\r\n * `false` includes all samples from `source` instead.\r\n * @param preserveBaseItem `true` if `source`'s base item should be preserved; `false` if it should be reset\r\n * based upon the latest sample.\r\n * @param stateTokenOverride Setting this to a 'truthy' value will remap all included samples, using that as\r\n * the new state token.\r\n */\r\n constructor(\r\n source: GestureSource,\r\n configStack: typeof GestureSource.prototype['recognizerConfigStack'],\r\n startAtEnd: boolean,\r\n preserveBaseItem: boolean,\r\n stateTokenOverride?: StateToken\r\n ) {\r\n let start = 0;\r\n let length = source.path.stats.sampleCount;\r\n if(source instanceof GestureSourceSubview) {\r\n start = source._baseStartIndex;\r\n }\r\n\r\n // While it'd be nice to validate that a previous subview, if used, has all 'current'\r\n // entries, this gets tricky; race conditions are possible in which an extra input event\r\n // occurs before subviews can be spun up when starting a model-matcher in some scenarios.\r\n\r\n super(source.rawIdentifier, configStack, source.isFromTouch, Object.getPrototypeOf(source.path).constructor);\r\n\r\n const baseSource = this._baseSource = source instanceof GestureSourceSubview ? source._baseSource : source;\r\n this.stateToken = stateTokenOverride ?? source.stateToken;\r\n\r\n /**\r\n * Provides a coordinate-system translation for source subviews.\r\n * The base version still needs to use the original coord system, though.\r\n */\r\n const translateSample = (sample: InputSample) => {\r\n const translation = this.recognizerTranslation;\r\n // Provide a coordinate-system translation for source subviews.\r\n // The base version still needs to use the original coord system, though.\r\n const transformedSample = {\r\n ...sample,\r\n targetX: sample.targetX - translation.x,\r\n targetY: sample.targetY - translation.y\r\n };\r\n\r\n if(this.stateToken) {\r\n transformedSample.stateToken = this.stateToken;\r\n }\r\n\r\n // If the subview is operating from the perspective of a different state token than its base source,\r\n // its samples' item fields will need correction.\r\n //\r\n // This can arise during multitap-like scenarios.\r\n if(this.stateToken != baseSource.stateToken || this.stateToken != source.stateToken) {\r\n transformedSample.item = this.currentRecognizerConfig.itemIdentifier(\r\n transformedSample,\r\n null\r\n );\r\n }\r\n\r\n return transformedSample;\r\n }\r\n\r\n // Will hold the last sample _even if_ we don't save every coord that comes through.\r\n const lastSample = source.path.stats.lastSample;\r\n\r\n // Are we 'chop'ping off the existing path or preserving it? This sets the sample-copying\r\n // configuration accordingly.\r\n if(startAtEnd) {\r\n this._baseStartIndex = start = Math.max(start + length - 1, 0);\r\n length = length > 0 ? 1 : 0;\r\n } else {\r\n this._baseStartIndex = start;\r\n }\r\n\r\n // For consistent handling both in and out of 'debugMode' - stats should be built solely\r\n // based on existing stats. Could try to add an assertion that the stats reasonably match\r\n // when in debug mode, though.\r\n if(startAtEnd) {\r\n // The easy case: we don't need to do any fancy stats-object manipulation.\r\n if(source.path.stats.sampleCount) {\r\n // Use the existing, empty one built by the .super() call and start anew.\r\n // Do not pre-translate... for consistency with the translate call below.\r\n this._path.extend(source.path.stats.lastSample);\r\n }\r\n } else {\r\n // Inherit the path.\r\n this._path = source.path.clone() as PathType;\r\n }\r\n this._path.translateCoordSystem(translateSample);\r\n\r\n if(preserveBaseItem) {\r\n // IMPORTANT: inherits the _subview's_ base item, not the baseSource's version thereof.\r\n // This allows gesture models based upon 'sustain timers' to have a different base item\r\n // than concurrent models that aren't sustain-followups.\r\n this._baseItem = source.baseItem;\r\n } else {\r\n this._baseItem = lastSample?.item;\r\n }\r\n\r\n // Ensure that this 'subview' is updated whenever the \"source of truth\" is.\r\n const completeHook = () => this.path.terminate(false);\r\n const invalidatedHook = () => this.path.terminate(true);\r\n const stepHook = (sample: InputSample) => {\r\n super.update(translateSample(sample));\r\n };\r\n baseSource.path.on('complete', completeHook);\r\n baseSource.path.on('invalidated', invalidatedHook);\r\n baseSource.path.on('step', stepHook);\r\n\r\n // But make sure we can \"disconnect\" it later once the gesture being matched\r\n // with the subview has fully matched; it's good to have a snapshot left over.\r\n this.subviewDisconnector = () => {\r\n baseSource.path.off('complete', completeHook);\r\n baseSource.path.off('invalidated', invalidatedHook);\r\n baseSource.path.off('step', stepHook);\r\n }\r\n\r\n // If the path was already completed, that should be reflected here, too.\r\n if(baseSource.isPathComplete) {\r\n this.path.terminate((baseSource.path.wasCancelled));\r\n this.disconnect();\r\n }\r\n }\r\n\r\n private get recognizerTranslation() {\r\n // Allowing a 'null' config greatly simplifies many of our unit-test specs.\r\n if(this.recognizerConfigStack.length == 1 || !this.currentRecognizerConfig) {\r\n return {\r\n x: 0,\r\n y: 0\r\n };\r\n }\r\n\r\n // Could compute all of this a single time & cache the value whenever a recognizer-config is pushed or popped.\r\n const currentRecognizer = this.currentRecognizerConfig;\r\n const currentClientRect = currentRecognizer.targetRoot.getBoundingClientRect();\r\n const baseClientRect = this.recognizerConfigStack[0].targetRoot.getBoundingClientRect();\r\n\r\n return {\r\n // x, y not available in Chrome 35... but left and top are.\r\n x: currentClientRect.left - baseClientRect.left,\r\n y: currentClientRect.top - baseClientRect.top\r\n }\r\n }\r\n\r\n /**\r\n * The original GestureSource this subview is based upon. Note that the coordinate system may\r\n * differ if a gesture stage/component has occurred that triggered a change to the active\r\n * recognizer configuration. (e.g. a subkey menu is being displayed for a longpress interaction)\r\n */\r\n public get baseSource() {\r\n return this._baseSource;\r\n }\r\n\r\n /**\r\n * This disconnects this subview from receiving further updates from the the underlying\r\n * source without causing it to be cancelled or treated as completed.\r\n */\r\n public disconnect() {\r\n if(this.subviewDisconnector) {\r\n this.subviewDisconnector();\r\n this.subviewDisconnector = null;\r\n }\r\n }\r\n\r\n public pushRecognizerConfig(config: Omit, \"touchEventRoot\" | \"mouseEventRoot\">): void {\r\n throw new Error(\"Pushing and popping of recognizer configurations should only be called on the base GestureSource\");\r\n }\r\n\r\n public popRecognizerConfig(): Nonoptional> {\r\n throw new Error(\"Pushing and popping of recognizer configurations should only be called on the base GestureSource\");\r\n }\r\n\r\n public update(sample: InputSample): void {\r\n throw new Error(\"Updates should be provided through the base GestureSource.\")\r\n }\r\n\r\n /**\r\n * Like `disconnect`, but this will also terminate the baseSource and prevent further\r\n * updates for the true, original `GestureSource` instance. If the gesture-model\r\n * and gesture-matching algorithm has determined this should be called, full path-update\r\n * termination is correct, even if called against a subview into the instance.\r\n */\r\n public terminate(cancel?: boolean) {\r\n this.baseSource.terminate(cancel);\r\n }\r\n}", + "import { GestureRecognizerConfiguration } from \"../configuration/gestureRecognizerConfiguration.js\";\r\nimport { Nonoptional } from \"../nonoptional.js\";\r\nimport { GestureDebugPath } from \"./gestureDebugPath.js\";\r\nimport { GestureSource, SerializedGestureSource } from \"./gestureSource.js\";\r\n/**\r\n * Represents all metadata needed internally for tracking a single \"touch contact point\" / \"touchpoint\"\r\n * involved in a potential / recognized gesture as tracked over time.\r\n *\r\n * Each instance corresponds to one unique contact point as recognized by `Touch.identifier` or to\r\n * one 'cursor-point' as represented by mouse-based motion.\r\n *\r\n * Refer to https://developer.mozilla.org/en-US/docs/Web/API/Touch and\r\n * https://developer.mozilla.org/en-US/docs/Web/API/Navigator/maxTouchPoints re \"touch contact point\".\r\n *\r\n * May be one-to-many with recognized gestures: a keyboard longpress interaction generally only has one\r\n * contact point but will have multiple realized gestures / components:\r\n * - longpress: Enough time has elapsed\r\n * - subkey: Subkey from the longpress subkey menu has been selected.\r\n *\r\n * Thus, it is a \"gesture source\". This is the level needed to model a single contact point, while some\r\n * gestures expect multiple, hence \"simple\".\r\n *\r\n */\r\nexport class GestureDebugSource extends GestureSource> {\r\n // Assertion: must always contain an index 0 - the base recognizer config.\r\n private static _jsonIdSeed: -1;\r\n\r\n /**\r\n * Usable by the gesture-recognizer library's consumer to track a token identifying specific states\r\n * of the consuming system if desired.\r\n */\r\n public stateToken: StateToken = null;\r\n\r\n /**\r\n * Constructs a new GestureDebugSource instance for tracking updates to an active input point over time.\r\n * @param identifier The system identifier for the input point's events.\r\n * @param initialHoveredItem The initiating event's original target element\r\n * @param isFromTouch `true` if sourced from a `TouchEvent`; `false` otherwise.\r\n */\r\n constructor(\r\n identifier: number,\r\n recognizerConfig: Nonoptional>\r\n | Nonoptional>[],\r\n isFromTouch: boolean\r\n ) {\r\n super(identifier, recognizerConfig, isFromTouch, GestureDebugPath);\r\n }\r\n\r\n protected initPath(): GestureDebugPath {\r\n return new GestureDebugPath();\r\n }\r\n\r\n /**\r\n * Deserializes a GestureSource instance from its serialized-JSON form.\r\n * @param jsonObj The JSON representation to deserialize.\r\n * @param identifier The unique identifier to assign to this instance.\r\n */\r\n public static deserialize(jsonObj: SerializedGestureSource, identifier: number) {\r\n const id = identifier !== undefined ? identifier : this._jsonIdSeed++;\r\n const isFromTouch = jsonObj.isFromTouch;\r\n const path = GestureDebugPath.deserialize(jsonObj.path);\r\n\r\n const instance = new GestureDebugSource(id, null, isFromTouch);\r\n instance._path = path;\r\n return instance;\r\n }\r\n}", + "import { EventEmitter } from \"eventemitter3\";\r\n\r\nimport { GestureRecognizerConfiguration } from \"../configuration/gestureRecognizerConfiguration.js\";\r\nimport { Nonoptional } from \"../nonoptional.js\";\r\nimport { GestureDebugSource } from \"./gestureDebugSource.js\";\r\nimport { GestureSource } from \"./gestureSource.js\";\r\n\r\ninterface EventMap {\r\n /**\r\n * Indicates that a new, ongoing touchpoint or mouse interaction has begun.\r\n * @param input The instance that tracks all future updates over the lifetime of the touchpoint / mouse interaction.\r\n */\r\n 'pointstart': (input: GestureSource) => void;\r\n\r\n // // idea for line below: to help multitouch gestures keep touchpaths in sync, rather than updated separately\r\n // 'eventcomplete': () => void;\r\n}\r\n\r\n/**\r\n * This serves as an abstract, headless-capable base class for handling incoming touch-path data for\r\n * gesture recognition as it is either generated (in the DOM) or replayed during automated tests\r\n * (headlessly).\r\n */\r\nexport abstract class InputEngineBase extends EventEmitter> {\r\n private _activeTouchpoints: GestureSource[] = [];\r\n\r\n // Touch interactions in the browser actually _re-use_ touch IDs once they lapse; the IDs are not lifetime-unique.\r\n // This gesture-engine desires lifetime-unique IDs, though, so we map them within this engine to remedy that problem.\r\n private readonly identifierMap: Record = {};\r\n private static IDENTIFIER_SEED = 0;\r\n\r\n public stateToken: StateToken;\r\n\r\n protected readonly config: Nonoptional>;\r\n private sourceConstructor: typeof GestureSource;\r\n\r\n public constructor(config: Nonoptional>) {\r\n super();\r\n this.config = config;\r\n this.sourceConstructor = (config?.recordingMode ?? true) ? GestureDebugSource : GestureSource;\r\n }\r\n\r\n createTouchpoint(identifier: number, isFromTouch: boolean) {\r\n // IDs provided to `GestureSource` should be engine-unique. Unfortunately, the base identifier patterns provided by\r\n // browsers don't do this, so we map the browser ID to an engine-internal one.\r\n const unique_id = InputEngineBase.IDENTIFIER_SEED++;\r\n\r\n this.identifierMap[identifier] = unique_id;\r\n\r\n // If debug mode is enabled, will enable persistent coordinate tracking. Is off by default.\r\n const source = new this.sourceConstructor(unique_id, this.config, isFromTouch);\r\n source.stateToken = this.stateToken;\r\n\r\n // Do not add here; it needs special managing for unit tests.\r\n\r\n return source;\r\n }\r\n\r\n public fulfillInputStart(touchpoint: GestureSource) {}\r\n\r\n /**\r\n * Calls to this method will cancel any touchpoints whose internal IDs are _not_ included in the parameter.\r\n * Designed to facilitate recovery from error cases and peculiar states that sometimes arise when debugging.\r\n * @param identifiers\r\n */\r\n maintainTouchpoints(touchpoints: GestureSource[]) {\r\n touchpoints ||= [];\r\n this._activeTouchpoints\r\n .filter((source) => !touchpoints.includes(source))\r\n // Will trigger `.dropTouchpoint` later in the event chain.\r\n .forEach((source) => source.terminate(true));\r\n }\r\n\r\n /**\r\n * @param identifier The identifier number corresponding to the input sequence.\r\n */\r\n hasActiveTouchpoint(identifier: number) {\r\n return this.identifierMap[identifier] !== undefined;\r\n }\r\n\r\n /**\r\n * Retrieves the GestureSource (corresponding to a single touchpoint) corresponding\r\n * to the specified internal identifier. Internal ID -> unique ID mapping is\r\n * performed here.\r\n * @param identifier\r\n * @returns\r\n */\r\n protected getTouchpointWithId(identifier: number) {\r\n const id = this.identifierMap[identifier];\r\n return this._activeTouchpoints.find((point) => point.rawIdentifier == id);\r\n }\r\n\r\n /**\r\n * During the lifetime of a GestureSource (a continuous path for a single touchpoint),\r\n * it is possible that the legal area for the path may change. This function allows\r\n * us to find the appropriate set of constraints for the path if any changes have been\r\n * requested - say, for a subkey menu after a longpress.\r\n * @param identifier\r\n * @returns\r\n */\r\n protected getConfigForId(identifier: number) {\r\n // protected - so, used internally only within the input engines.\r\n // `getTouchpointWithId` will perform the internal -> external ID mapping.\r\n return this.getTouchpointWithId(identifier).currentRecognizerConfig;\r\n }\r\n\r\n protected getStateTokenForId(identifier: number) {\r\n // protected - so, used internally only within the input engines.\r\n // `getTouchpointWithId` will perform the internal -> external ID mapping.\r\n return this.getTouchpointWithId(identifier).stateToken ?? null;\r\n }\r\n\r\n protected dropTouchpoint(point: GestureSource) {\r\n const id = point.rawIdentifier;\r\n\r\n this._activeTouchpoints = this._activeTouchpoints.filter((pt) => point != pt);\r\n for(const key of Object.keys(this.identifierMap)) {\r\n const keyVal = Number.parseInt(key, 10);\r\n if(this.identifierMap[keyVal] == id) {\r\n delete this.identifierMap[keyVal];\r\n }\r\n }\r\n }\r\n\r\n protected addTouchpoint(touchpoint: GestureSource) {\r\n this._activeTouchpoints.push(touchpoint);\r\n }\r\n\r\n public get activeSources(): GestureSource[] {\r\n return [].concat(this._activeTouchpoints);\r\n }\r\n}", + "import { InputEngineBase } from \"./headless/inputEngineBase.js\";\r\nimport { InputSample } from \"./headless/inputSample.js\";\r\nimport { GestureSource } from \"./headless/gestureSource.js\";\r\nimport { GestureRecognizerConfiguration } from \"./index.js\";\r\nimport { reportError } from \"./reportError.js\";\r\n\r\nexport function processSampleClientCoords(config: GestureRecognizerConfiguration, clientX: number, clientY: number) {\r\n const targetRect = config.targetRoot.getBoundingClientRect();\r\n return {\r\n clientX: clientX,\r\n clientY: clientY,\r\n targetX: clientX - targetRect.left,\r\n targetY: clientY - targetRect.top\r\n } as InputSample;\r\n}\r\n\r\nexport abstract class InputEventEngine extends InputEngineBase {\r\n abstract registerEventHandlers(): void;\r\n abstract unregisterEventHandlers(): void;\r\n\r\n protected buildSampleFor(clientX: number, clientY: number, target: EventTarget, timestamp: number, source: GestureSource): InputSample {\r\n const sample: InputSample = {\r\n ...processSampleClientCoords(this.config, clientX, clientY),\r\n t: timestamp,\r\n stateToken: source?.stateToken ?? this.stateToken\r\n };\r\n\r\n const itemIdentifier = source?.currentRecognizerConfig.itemIdentifier ?? this.config.itemIdentifier;\r\n const hoveredItem = itemIdentifier(sample, target);\r\n sample.item = hoveredItem;\r\n\r\n return sample;\r\n }\r\n\r\n protected onInputStart(identifier: number, sample: InputSample, target: EventTarget, isFromTouch: boolean) {\r\n const touchpoint = this.createTouchpoint(identifier, isFromTouch);\r\n touchpoint.update(sample);\r\n\r\n this.addTouchpoint(touchpoint);\r\n\r\n // External objects may desire to directly terminate handling of\r\n // input sequences under specific conditions.\r\n touchpoint.path.on('invalidated', () => {\r\n this.dropTouchpoint(touchpoint);\r\n });\r\n\r\n touchpoint.path.on('complete', () => {\r\n this.dropTouchpoint(touchpoint);\r\n });\r\n\r\n try {\r\n this.emit('pointstart', touchpoint);\r\n } catch(err) {\r\n reportError('Engine-internal error while initializing gesture matching for new source', err);\r\n }\r\n\r\n return touchpoint;\r\n }\r\n\r\n protected onInputMove(touchpoint: GestureSource, sample: InputSample, target: EventTarget) {\r\n if(!touchpoint) {\r\n return;\r\n }\r\n\r\n try {\r\n touchpoint.update(sample);\r\n } catch(err) {\r\n reportError('Error occurred while updating source', err);\r\n }\r\n }\r\n\r\n protected onInputMoveCancel(touchpoint: GestureSource, sample: InputSample, target: EventTarget) {\r\n if(!touchpoint) {\r\n return;\r\n }\r\n\r\n try {\r\n touchpoint.update(sample);\r\n touchpoint.path.terminate(true);\r\n } catch(err) {\r\n reportError('Error occurred while cancelling further input for source', err);\r\n }\r\n }\r\n\r\n protected onInputEnd(touchpoint: GestureSource, target: EventTarget) {\r\n if(!touchpoint) {\r\n return;\r\n }\r\n\r\n try {\r\n touchpoint.path.terminate(false);\r\n } catch(err) {\r\n reportError('Error occurred while finalizing input for source', err);\r\n }\r\n }\r\n}", + "import { GestureRecognizerConfiguration } from \"./gestureRecognizerConfiguration.js\";\r\nimport { InputSample } from \"../headless/inputSample.js\";\r\nimport { Nonoptional } from \"../nonoptional.js\";\r\nimport { RecognitionZoneSource } from \"./recognitionZoneSource.js\";\r\n\r\nexport class ZoneBoundaryChecker {\r\n // This class exists for static methods & fields.\r\n private constructor() { }\r\n\r\n public static readonly FAR_TOP : 0x0008 = 0x0008;\r\n public static readonly FAR_LEFT : 0x0004 = 0x0004;\r\n public static readonly FAR_BOTTOM: 0x0002 = 0x0002;\r\n public static readonly FAR_RIGHT : 0x0001 = 0x0001;\r\n\r\n /**\r\n * Determines the relationship of an input coordinate to one of the gesture engine's\r\n * active recognition zones and returns a bitmask indicating which boundary (or\r\n * boundaries) the input coordinate lies outside of.\r\n *\r\n * @param coord An input coordinate\r\n * @param zone An object defining a 'recognition zone' of the gesture engine.\r\n * @param ignoreBitmask A bitmask indicating select boundaries to ignore for the check.\r\n */\r\n static getCoordZoneBitmask(coord: InputSample, zone: RecognitionZoneSource): number {\r\n const bounds = zone.getBoundingClientRect();\r\n\r\n let bitmask = 0;\r\n bitmask |= (coord.clientX < bounds.left) ? ZoneBoundaryChecker.FAR_LEFT : 0;\r\n bitmask |= (coord.clientX > bounds.right) ? ZoneBoundaryChecker.FAR_RIGHT : 0;\r\n bitmask |= (coord.clientY < bounds.top) ? ZoneBoundaryChecker.FAR_TOP : 0;\r\n bitmask |= (coord.clientY > bounds.bottom) ? ZoneBoundaryChecker.FAR_BOTTOM : 0;\r\n\r\n return bitmask; // returns zero if effectively 'within bounds'.\r\n }\r\n\r\n /**\r\n * Confirms whether or not the input coordinate lies within the accepted coordinate bounds\r\n * for a gesture input sequence's first coordinate.\r\n */\r\n static inputStartOutOfBoundsCheck(coord: InputSample, config: Nonoptional>): boolean {\r\n return !!this.getCoordZoneBitmask(coord, config.inputStartBounds); // true if out of bounds.\r\n }\r\n\r\n /**\r\n * Call this method to determine which safe-boundary edges, if any, the initial coordinate\r\n * indicates should be disabled for its sequence's future updates.\r\n *\r\n * This value should be provided as the third argument to `inputMoveCancellationCheck` for\r\n * updated input coordinates for the current input sequence.\r\n */\r\n static inputStartSafeBoundProximityCheck(coord: InputSample, config: Nonoptional>): number {\r\n return this.getCoordZoneBitmask(coord, config.paddedSafeBounds);\r\n }\r\n\r\n static inputMoveCancellationCheck(coord: InputSample,\r\n config: Nonoptional>,\r\n ignoredSafeBoundFlags?: number): boolean {\r\n ignoredSafeBoundFlags = ignoredSafeBoundFlags || 0;\r\n\r\n // If the coordinate lies outside the maximum supported range, fail the boundary check.\r\n if(!!(this.getCoordZoneBitmask(coord, config.maxRoamingBounds))) {\r\n return true;\r\n }\r\n\r\n let borderProximityBitmask = this.getCoordZoneBitmask(coord, config.safeBounds);\r\n\r\n // If the active input sequence started close enough to a safe zone border, we\r\n // disable that part of the said border for any cancellation checks.\r\n return !!(borderProximityBitmask & ~ignoredSafeBoundFlags);\r\n }\r\n}", + "import { GestureRecognizerConfiguration } from \"./configuration/gestureRecognizerConfiguration.js\";\r\nimport { InputEventEngine } from \"./inputEventEngine.js\";\r\nimport { Nonoptional } from \"./nonoptional.js\";\r\nimport { ZoneBoundaryChecker } from \"./configuration/zoneBoundaryChecker.js\";\r\nimport { GestureSource } from \"./headless/gestureSource.js\";\r\n\r\n// Does NOT use the AsyncClosureDispatchQueue... simply because there can only ever be one mouse touchpoint.\r\nexport class MouseEventEngine extends InputEventEngine {\r\n private readonly _mouseStart: typeof MouseEventEngine.prototype.onMouseStart;\r\n private readonly _mouseMove: typeof MouseEventEngine.prototype.onMouseMove;\r\n private readonly _mouseEnd: typeof MouseEventEngine.prototype.onMouseEnd;\r\n\r\n private hasActiveClick: boolean = false;\r\n private disabledSafeBounds: number = 0;\r\n\r\n private currentSource: GestureSource = null;\r\n private readonly activeIdentifier = 0;\r\n\r\n public constructor(config: Nonoptional>) {\r\n super(config);\r\n\r\n // We use this approach, rather than .bind, because _this_ version allows hook\r\n // insertion for unit tests via prototype manipulation. The .bind version doesn't.\r\n this._mouseStart = (event: MouseEvent) => this.onMouseStart(event);\r\n this._mouseMove = (event: MouseEvent) => this.onMouseMove(event);\r\n this._mouseEnd = (event: MouseEvent) => this.onMouseEnd(event);\r\n }\r\n\r\n private get eventRoot(): HTMLElement {\r\n return this.config.mouseEventRoot;\r\n }\r\n\r\n registerEventHandlers() {\r\n this.eventRoot.addEventListener('mousedown', this._mouseStart, true);\r\n this.eventRoot.addEventListener('mousemove', this._mouseMove, false);\r\n // The listener below fails to capture when performing automated testing checks in Chrome emulation unless 'true'.\r\n this.eventRoot.addEventListener('mouseup', this._mouseEnd, true);\r\n }\r\n\r\n unregisterEventHandlers() {\r\n this.eventRoot.removeEventListener('mousedown', this._mouseStart, true);\r\n this.eventRoot.removeEventListener('mousemove', this._mouseMove, false);\r\n this.eventRoot.removeEventListener('mouseup', this._mouseEnd, true);\r\n }\r\n\r\n private preventPropagation(e: MouseEvent) {\r\n // Standard event maintenance\r\n e.preventDefault();\r\n e.cancelBubble=true;\r\n e.returnValue=false; // I2409 - Avoid focus loss for visual keyboard events\r\n\r\n if(typeof e.stopImmediatePropagation == 'function') {\r\n e.stopImmediatePropagation();\r\n } else if(typeof e.stopPropagation == 'function') {\r\n e.stopPropagation();\r\n }\r\n }\r\n\r\n private buildSampleFromEvent(event: MouseEvent) {\r\n // WILL be null for newly-starting `GestureSource`s / contact points.\r\n return this.buildSampleFor(event.clientX, event.clientY, event.target, performance.now(), this.currentSource);\r\n }\r\n\r\n onMouseStart(event: MouseEvent) {\r\n // If it's not an event we'd consider handling, do not prevent event\r\n // propagation! Just don't process it.\r\n if(!this.config.targetRoot.contains(event.target as Node)) {\r\n return;\r\n }\r\n\r\n this.preventPropagation(event);\r\n\r\n const sample = this.buildSampleFromEvent(event);\r\n\r\n if(!ZoneBoundaryChecker.inputStartOutOfBoundsCheck(sample, this.config)) {\r\n // If we started very close to a safe zone border, remember which one(s).\r\n // This is important for input-sequence cancellation check logic.\r\n this.disabledSafeBounds = ZoneBoundaryChecker.inputStartSafeBoundProximityCheck(sample, this.config);\r\n }\r\n\r\n const touchpoint = this.onInputStart(this.activeIdentifier, sample, event.target, false);\r\n this.currentSource = touchpoint;\r\n\r\n const cleanup = () => {\r\n this.currentSource = null;\r\n }\r\n\r\n touchpoint.path.on('complete', cleanup);\r\n touchpoint.path.on('invalidated', cleanup);\r\n }\r\n\r\n onMouseMove(event: MouseEvent) {\r\n const source = this.currentSource;\r\n if(!source) {\r\n return;\r\n }\r\n\r\n const sample = this.buildSampleFromEvent(event);\r\n\r\n if(!event.buttons) {\r\n if(this.hasActiveClick) {\r\n this.hasActiveClick = false;\r\n this.onInputMoveCancel(source, sample, event.target);\r\n }\r\n return;\r\n }\r\n\r\n this.preventPropagation(event);\r\n const config = source.currentRecognizerConfig;\r\n\r\n if(!ZoneBoundaryChecker.inputMoveCancellationCheck(sample, config, this.disabledSafeBounds)) {\r\n this.onInputMove(source, sample, event.target);\r\n } else {\r\n this.onInputMoveCancel(source, sample, event.target);\r\n }\r\n }\r\n\r\n onMouseEnd(event: MouseEvent) {\r\n const source = this.currentSource;\r\n if(!source) {\r\n return;\r\n }\r\n\r\n if(!event.buttons) {\r\n this.hasActiveClick = false;\r\n }\r\n\r\n this.onInputEnd(source, event.target);\r\n }\r\n}", + "import { GestureRecognizerConfiguration } from \"./configuration/gestureRecognizerConfiguration.js\";\r\nimport { InputEventEngine } from \"./inputEventEngine.js\";\r\nimport { Nonoptional } from \"./nonoptional.js\";\r\nimport { ZoneBoundaryChecker } from \"./configuration/zoneBoundaryChecker.js\";\r\nimport { GestureSource } from \"./headless/gestureSource.js\";\r\nimport { ManagedPromise } from \"@keymanapp/web-utils\";\r\nimport { AsyncClosureDispatchQueue } from \"./headless/asyncClosureDispatchQueue.js\";\r\nimport { GesturePath } from \"./index.js\";\r\n\r\nfunction touchListToArray(list: TouchList) {\r\n const arr: Touch[] = [];\r\n\r\n for(let i=0; i < list.length; i++) {\r\n arr.push(list.item(i));\r\n }\r\n\r\n return arr;\r\n}\r\nexport class TouchEventEngine extends InputEventEngine {\r\n private readonly _touchStart: typeof TouchEventEngine.prototype.onTouchStart;\r\n private readonly _touchMove: typeof TouchEventEngine.prototype.onTouchMove;\r\n private readonly _touchEnd: typeof TouchEventEngine.prototype.onTouchEnd;\r\n\r\n protected readonly eventDispatcher = new AsyncClosureDispatchQueue();\r\n\r\n private safeBoundMaskMap: {[id: number]: number} = {};\r\n // This map works synchronously with the actual event handlers.\r\n private pendingSourcePromises: Map>> = new Map();\r\n private inputStartSignalMap: Map, ManagedPromise> = new Map();\r\n\r\n public constructor(config: Nonoptional>) {\r\n super(config);\r\n\r\n // We use this approach, rather than .bind, because _this_ version allows hook\r\n // insertion for unit tests via prototype manipulation. The .bind version doesn't.\r\n this._touchStart = (event: TouchEvent) => this.onTouchStart(event);\r\n this._touchMove = (event: TouchEvent) => this.onTouchMove(event);\r\n this._touchEnd = (event: TouchEvent) => this.onTouchEnd(event);\r\n }\r\n\r\n private get eventRoot(): HTMLElement {\r\n return this.config.touchEventRoot;\r\n }\r\n\r\n registerEventHandlers() {\r\n // The 'passive' property ensures we can prevent MouseEvent followups from TouchEvents.\r\n // It is only specified during `addEventListener`, not during `removeEventListener`.\r\n this.eventRoot.addEventListener('touchstart', this._touchStart, {capture: true, passive: false});\r\n this.eventRoot.addEventListener('touchmove', this._touchMove, {capture: false, passive: false});\r\n // The listener below fails to capture when performing automated testing checks in Chrome emulation unless 'true'.\r\n this.eventRoot.addEventListener('touchend', this._touchEnd, {capture: true, passive: false});\r\n }\r\n\r\n unregisterEventHandlers() {\r\n this.eventRoot.removeEventListener('touchstart', this._touchStart, true);\r\n this.eventRoot.removeEventListener('touchmove', this._touchMove, false);\r\n this.eventRoot.removeEventListener('touchend', this._touchEnd, true);\r\n }\r\n\r\n private preventPropagation(e: TouchEvent) {\r\n // Standard event maintenance\r\n if(e.cancelable) {\r\n // Chrome generates error-log messages if this is attempted while\r\n // the condition is false.\r\n e.preventDefault();\r\n }\r\n\r\n if(typeof e.stopImmediatePropagation == 'function') {\r\n e.stopImmediatePropagation();\r\n } else if(typeof e.stopPropagation == 'function') {\r\n e.stopPropagation();\r\n }\r\n }\r\n\r\n public dropTouchpoint(source: GestureSource) {\r\n super.dropTouchpoint(source);\r\n\r\n for(const key of Object.keys(this.safeBoundMaskMap)) {\r\n const keyVal = Number.parseInt(key, 10);\r\n if(this.getTouchpointWithId(keyVal) == source) {\r\n delete this.safeBoundMaskMap[keyVal];\r\n }\r\n }\r\n }\r\n\r\n public fulfillInputStart(touchpoint: GestureSource>) {\r\n const lock = this.inputStartSignalMap.get(touchpoint);\r\n if(lock) {\r\n this.inputStartSignalMap.delete(touchpoint);\r\n lock.resolve();\r\n }\r\n };\r\n\r\n public hasActiveTouchpoint(identifier: number): boolean {\r\n const baseResult = super.hasActiveTouchpoint(identifier);\r\n return baseResult || !!this.pendingSourcePromises.has(identifier);\r\n }\r\n\r\n private buildSampleFromTouch(touch: Touch, timestamp: number, source: GestureSource) {\r\n // WILL be null for newly-starting `GestureSource`s / contact points.\r\n return this.buildSampleFor(touch.clientX, touch.clientY, touch.target, timestamp, source);\r\n }\r\n\r\n onTouchStart(event: TouchEvent) {\r\n // If it's not an event we'd consider handling, do not prevent event\r\n // propagation! Just don't process it.\r\n if(!this.config.targetRoot.contains(event.target as Node)) {\r\n return;\r\n }\r\n\r\n this.preventPropagation(event);\r\n\r\n // In case a touch ID is reused, we can pre-emptively filter it for special cases to cancel the old version,\r\n // noting that it's included by a changedTouch. (Only _new_ contact points are included in .changedTouches\r\n // during a touchstart.)\r\n const allTouches = touchListToArray(event.touches);\r\n const newTouches = touchListToArray(event.changedTouches);\r\n const oldTouches = allTouches.filter((touch1) => {\r\n return newTouches.findIndex(touch2 => touch1.identifier == touch2.identifier) == -1;\r\n });\r\n\r\n // Any 'old touches' should have pre-existing entries in our promise-map that are still current, as\r\n // the promise-map is maintained 100% synchronously with incoming events.\r\n const oldSourcePromises = oldTouches.map((touch) => this.pendingSourcePromises.get(touch.identifier));\r\n\r\n this.eventDispatcher.runAsync(async () => {\r\n const oldSources = await Promise.all(oldSourcePromises);\r\n // Maintain all touches in the `.touches` array that are NOT marked as `.changedTouches` (and therefore, new)\r\n this.maintainTouchpoints(oldSources);\r\n\r\n return this.eventDispatcher.defaultWait;\r\n });\r\n\r\n /*\r\n We create Promises that can be set and retrieved synchronously with the actual event handlers\r\n in order to prevent issues from tricky asynchronous identifier-to-source mapping attempts.\r\n\r\n As these Promises are set (and thus, retrievable) synchronously with the actual event handlers,\r\n we can closure-capture them for use in the internally-asynchronous processing closures.\r\n\r\n `capturedSourcePromises` will be useful for closure-capture binding the new Promise(s) to\r\n the closure to be queued. `this.pendingSourcePromises` facilitates similar closure-capture\r\n patterns within the touchMove and touchEnd handlers for their queued closures.\r\n */\r\n const capturedSourcePromises = new Map>>();\r\n for(let i=0; i < event.changedTouches.length; i++) {\r\n const touch = event.changedTouches.item(i);\r\n const promise = new ManagedPromise>();\r\n this.pendingSourcePromises.set(touch.identifier, promise);\r\n capturedSourcePromises.set(touch.identifier, promise);\r\n }\r\n\r\n /*\r\n When multiple touchpoints are active, we need to ensure a specific order of events.\r\n The easiest way to ensure the exact order involves programmatic delay of their\r\n processing, essentially \"sequentializing\" the events into a deterministic order.\r\n\r\n It also helps to ensure that any path updates are only emitted when all listeners\r\n for that path have been prepared - and other parts of the engine cause that to happen\r\n asynchronously in certain situations. Within KMW, one such case is when a simple-tap\r\n with `nextLayer` defined is auto-completed by a new incoming touch, triggering an\r\n instant layer-change.\r\n */\r\n this.eventDispatcher.runAsync(() => {\r\n // Ensure the same timestamp is used for all touches being updated.\r\n const timestamp = performance.now();\r\n let touchpoint: GestureSource = null;\r\n\r\n // During a touch-start, only _new_ touch contact points are listed here;\r\n // we shouldn't signal \"input start\" for any previously-existing touch points,\r\n // so `.changedTouches` is the best way forward.\r\n for(let i=0; i < event.changedTouches.length; i++) {\r\n const touch = event.changedTouches.item(i);\r\n const touchId = touch.identifier;\r\n const sample = this.buildSampleFromTouch(touch, timestamp, null);\r\n\r\n if(!ZoneBoundaryChecker.inputStartOutOfBoundsCheck(sample, this.config)) {\r\n // If we started very close to a safe zone border, remember which one(s).\r\n // This is important for input-sequence cancellation check logic.\r\n this.safeBoundMaskMap[touchId] = ZoneBoundaryChecker.inputStartSafeBoundProximityCheck(sample, this.config);\r\n } else {\r\n // This touchpoint shouldn't be considered; do not signal a touchstart for it.\r\n let sourcePromise = capturedSourcePromises.get(touchId);\r\n sourcePromise.resolve(null);\r\n continue;\r\n }\r\n\r\n touchpoint = this.onInputStart(touchId, sample, event.target, true);\r\n\r\n /*\r\n We use the closure-captured version bound to this specific closure, rather than the\r\n most recent one for the touch-identifier - under heavy rapid typing, it's possible that\r\n the touch-identifier has been reused.\r\n\r\n The resolved Promise may then be used to retrieve the correct source in the other event\r\n handlers' closures.\r\n */\r\n let sourcePromise = capturedSourcePromises.get(touchId);\r\n sourcePromise.resolve(touchpoint);\r\n\r\n /*\r\n Ensure we only do the cleanup if and when it hasn't already been replaced by new events later.\r\n\r\n Must be done for EACH source - we can't risk leaving a lingering entry once we've dismissed\r\n processing for the source. Failure to do so may result in blocking touch events that should\r\n no longer be manipulated by this engine by affecting `hasActiveTouchpoint`.\r\n */\r\n const cleanup = () => {\r\n /*\r\n If delays accumulate significantly, it is possible that when this queued closure is run,\r\n a different touchpoint is reusing the same identifier. Don't delete the entry if our\r\n entry has been replaced.\r\n */\r\n if(this.pendingSourcePromises.get(touchId) == sourcePromise) {\r\n this.pendingSourcePromises.delete(touchId);\r\n }\r\n }\r\n\r\n touchpoint.path.on('complete', cleanup);\r\n touchpoint.path.on('invalidated', cleanup);\r\n }\r\n\r\n if(touchpoint) {\r\n // This 'lock' should only be released when the last simultaneously-registered touch is published via\r\n // gesture-recognizer event.\r\n let eventSignalPromise = new ManagedPromise();\r\n this.inputStartSignalMap.set(touchpoint, eventSignalPromise);\r\n\r\n return eventSignalPromise.corePromise;\r\n } else {\r\n return Promise.resolve();\r\n }\r\n });\r\n }\r\n\r\n onTouchMove(event: TouchEvent) {\r\n for(let i = 0; i < event.touches.length; i++) {\r\n const touch = event.touches.item(i);\r\n if(this.hasActiveTouchpoint(touch.identifier)) {\r\n this.preventPropagation(event);\r\n break;\r\n }\r\n }\r\n\r\n /*\r\n Using the Promise map built in touchStart, we can retrieve a Promise for the source linked\r\n to this event and closure-capture it for the closure queued below.\r\n */\r\n const capturedSourcePromises = new Map>>();\r\n for(let i = 0; i < event.touches.length; i++) {\r\n const touchId = event.touches.item(i).identifier;\r\n // If the source's gesture is finalized or cancelled but touch events are ongoing,\r\n // with no delay between event and its processing, the map entry here will be cleared.\r\n capturedSourcePromises.set(touchId, this.pendingSourcePromises.get(touchId)?.corePromise);\r\n }\r\n\r\n this.eventDispatcher.runAsync(async () => {\r\n const touches = await Promise.all(capturedSourcePromises.values());\r\n this.maintainTouchpoints(touches);\r\n\r\n return this.eventDispatcher.defaultWait;\r\n });\r\n\r\n /*\r\n When multiple touchpoints are active, we need to ensure a specific order of events.\r\n The easiest way to ensure the exact order involves programmatic delay of their\r\n processing, essentially \"sequentializing\" the events into a deterministic order.\r\n\r\n It also helps to ensure that any path updates are only emitted when all listeners\r\n for that path have been prepared - and other parts of the engine cause that to happen\r\n asynchronously in certain situations. Within KMW, one such case is when a simple-tap\r\n with `nextLayer` defined is auto-completed by a new incoming touch, triggering an\r\n instant layer-change.\r\n */\r\n this.eventDispatcher.runAsync(async () => {\r\n // Ensure the same timestamp is used for all touches being updated.\r\n const timestamp = performance.now();\r\n\r\n // Do not change to `changedTouches` - we need a sample for all active touches in order\r\n // to facilitate path-update synchronization for multi-touch gestures.\r\n //\r\n // May be worth doing changedTouches _first_ though.\r\n for(let i=0; i < event.touches.length; i++) {\r\n const touch = event.touches.item(i);\r\n const touchId = touch.identifier;\r\n\r\n\r\n // Only lists touch contact points that have been lifted; touchmove is\r\n // raised separately if any movement occurred.\r\n //\r\n // If the promise object could not be assigned, we `await undefined` -\r\n // which JS converts to `await Promise.resolve(undefined)`. It's safe.\r\n const source = await capturedSourcePromises.get(touchId);\r\n if(!source || source.isPathComplete) {\r\n continue;\r\n }\r\n\r\n const config = source.currentRecognizerConfig;\r\n const sample = this.buildSampleFromTouch(touch, timestamp, source);\r\n\r\n if(!ZoneBoundaryChecker.inputMoveCancellationCheck(sample, config, this.safeBoundMaskMap[touchId])) {\r\n this.onInputMove(source, sample, touch.target);\r\n } else {\r\n this.onInputMoveCancel(source, sample, touch.target);\r\n }\r\n }\r\n\r\n /*\r\n Since we're operating within an async function, a Promise return-type\r\n is implied. That cancels out the default wait, but we want to ensure\r\n that the default wait is applied here.\r\n */\r\n return this.eventDispatcher.defaultWait;\r\n });\r\n }\r\n\r\n onTouchEnd(event: TouchEvent) {\r\n for(let i = 0; i < event.changedTouches.length; i++) {\r\n const touch = event.changedTouches.item(i);\r\n if(this.hasActiveTouchpoint(touch.identifier)) {\r\n this.preventPropagation(event);\r\n break;\r\n }\r\n }\r\n\r\n /*\r\n Using the Promise map built in touchStart, we can retrieve a Promise for the source linked\r\n to this event and closure-capture it for the closure queued below.\r\n */\r\n const capturedSourcePromises = new Map>>();\r\n // Any ending touches don't show up in event.touches - only in event.changedTouches!\r\n for(let i = 0; i < event.changedTouches.length; i++) {\r\n const touchId = event.changedTouches.item(i).identifier;\r\n // If the source's gesture is finalized or cancelled but touch events are ongoing,\r\n // with no delay between event and its processing, the map entry here will be cleared.\r\n const promiseToCapture = this.pendingSourcePromises.get(touchId)?.corePromise;\r\n capturedSourcePromises.set(touchId, promiseToCapture);\r\n }\r\n\r\n this.eventDispatcher.runAsync(async () => {\r\n // Only lists touch contact points that have been lifted; touchmove is\r\n // raised separately if any movement occurred.\r\n //\r\n // If the promise object could not be assigned, we `await undefined` -\r\n // which JS converts to `await Promise.resolve(undefined)`. It's safe.\r\n for(let i=0; i < event.changedTouches.length; i++) {\r\n const touch = event.changedTouches.item(i);\r\n\r\n const source = await capturedSourcePromises.get(touch.identifier);\r\n if(!source || source.isPathComplete) {\r\n continue;\r\n }\r\n\r\n this.onInputEnd(source, event.target);\r\n }\r\n\r\n return this.eventDispatcher.defaultWait;\r\n });\r\n }\r\n}", + "import { CumulativePathStats } from \"../../cumulativePathStats.js\";\r\nimport { GestureSource, GestureSourceSubview } from \"../../gestureSource.js\";\r\nimport { ContactModel } from \"../specs/contactModel.js\";\r\nimport { ManagedPromise, TimeoutPromise } from \"@keymanapp/web-utils\";\r\n\r\nexport type FulfillmentCause = 'path' | 'timer' | 'item' | 'cancelled';\r\n\r\nexport interface PathMatchResolution {\r\n type: 'resolve',\r\n cause: FulfillmentCause\r\n}\r\n\r\nexport interface PathMatchRejection {\r\n type: 'reject'\r\n cause: FulfillmentCause\r\n}\r\n\r\nexport interface PathNotFulfilled {\r\n type: 'continue'\r\n}\r\n\r\ntype PathMatchResult = PathMatchRejection | PathMatchResolution;\r\ntype PathUpdateResult = PathMatchResult | PathNotFulfilled;\r\n\r\nexport class PathMatcher {\r\n private timerPromise?: TimeoutPromise;\r\n public readonly model: ContactModel;\r\n\r\n // During execution, source.path is fine... but once this matcher's role is done,\r\n // `source` will continue to receive edits and may even change the instance\r\n // underlying the `path` field.\r\n public readonly source: GestureSource;\r\n\r\n /**\r\n * Holds the stats for the inherited portion of the path.\r\n */\r\n private readonly inheritedStats: CumulativePathStats;\r\n\r\n /**\r\n * Holds the path's stats at the time of the last `update()` call, as needed\r\n * by PathModel's `evaluate` function.\r\n */\r\n // In regard to KeymanWeb, this exists to enhance flick-resetting behaviors.\r\n private lastStats: CumulativePathStats;\r\n\r\n private readonly publishedPromise: ManagedPromise\r\n private _result: PathMatchResult;\r\n\r\n public get promise() {\r\n return this.publishedPromise.corePromise;\r\n }\r\n\r\n constructor(model: ContactModel, source: GestureSource, basePathStats?: CumulativePathStats) {\r\n /* c8 ignore next 3 */\r\n if(!model || !source) {\r\n throw new Error(\"A gesture-path source and contact-path model must be specified.\");\r\n }\r\n\r\n this.model = model;\r\n this.publishedPromise = new ManagedPromise();\r\n this.source = source;\r\n this.inheritedStats = basePathStats;\r\n this.lastStats = null;\r\n\r\n if(model.timer) {\r\n const offset = model.timer.inheritElapsed ? Math.min(source.path.stats.duration, model.timer.duration) : 0;\r\n this.timerPromise = new TimeoutPromise(model.timer.duration - offset);\r\n\r\n this.publishedPromise.then(() => {\r\n this.timerPromise.resolve(false);\r\n // but make sure that simultaneous path resolution continues even if the timer's is mismatched.\r\n });\r\n\r\n this.timerPromise.then((result) => {\r\n const trueSource = source instanceof GestureSourceSubview ? source.baseSource : source;\r\n const timestamp = performance.now();\r\n\r\n /* It's entirely possible that this will be triggered at a timestamp unaligned with the\r\n * standard timing for input sampling. It's best to ensure that the reported path\r\n * duration (on path.stats) satisfies the timer threshold, so we add an artificial\r\n * sample here that will enforce that desire.\r\n */\r\n if(!trueSource.isPathComplete && trueSource.currentSample.t != timestamp) {\r\n trueSource.path.extend({\r\n ...trueSource.currentSample,\r\n t: timestamp\r\n });\r\n }\r\n\r\n if(result != model.timer.expectedResult) {\r\n this.finalize(false, 'timer');\r\n }\r\n\r\n // Check for validation as needed.\r\n this.finalize(true, 'timer');\r\n });\r\n }\r\n }\r\n\r\n private finalize(result: boolean, cause: FulfillmentCause): PathMatchResult {\r\n if(this.publishedPromise.isFulfilled) {\r\n return this._result;\r\n }\r\n\r\n const model = this.model;\r\n\r\n // Check for validation as needed.\r\n if(model.validateItem && result) {\r\n // If we're finalizing on a positive note but there's an item-validation check, we need\r\n // to obey the results of that check.\r\n result = model.validateItem(this.source.path.stats.lastSample.item, this.baseItem);\r\n }\r\n\r\n let retVal: PathMatchResult;\r\n if(result) {\r\n retVal = {\r\n type: model.pathResolutionAction,\r\n cause: cause\r\n };\r\n } else {\r\n retVal = {\r\n type: 'reject',\r\n cause: cause\r\n };\r\n }\r\n this.publishedPromise.resolve(retVal)\r\n this._result = retVal;\r\n\r\n return retVal;\r\n }\r\n\r\n get stats() {\r\n return this.source.path.stats;\r\n }\r\n\r\n get baseItem() {\r\n return this.source.baseItem;\r\n }\r\n\r\n get lastItem() {\r\n return this.source.currentSample.item;\r\n }\r\n\r\n update(): PathUpdateResult {\r\n const model = this.model;\r\n const source = this.source;\r\n\r\n if(source.path.wasCancelled) {\r\n return this.finalize(false, 'path');\r\n }\r\n\r\n // For certain unit-test setups, we may have a zero-length path when this is called during test init.\r\n // It's best to have that path-coord-length check in place, just in case.\r\n if(model.itemChangeAction && source.path.stats.sampleCount > 0 && source.currentSample.item != source.baseItem) {\r\n const result = model.itemChangeAction == 'resolve';\r\n\r\n return this.finalize(result, 'item');\r\n } else {\r\n // Note: is current path, not 'full path'.\r\n const result = model.pathModel.evaluate(source.path, this.lastStats, source.baseItem, this.inheritedStats) || 'continue';\r\n this.lastStats = source.path.stats;\r\n\r\n if(result != 'continue') {\r\n return this.finalize(result == 'resolve', 'path');\r\n } else if(source.path.isComplete) {\r\n // If the PathModel said to 'continue' but the path is done, we default\r\n // to rejecting the model; there will be no more changes, after all.\r\n return this.finalize(false, 'path');\r\n }\r\n\r\n return {\r\n type: 'continue'\r\n };\r\n }\r\n }\r\n}", + "import { GestureSource, GestureSourceSubview } from \"../../gestureSource.js\";\r\n\r\nimport { GestureModel, GestureResolution, GestureResolutionSpec, RejectionDefault, RejectionReplace, ResolutionItemSpec } from \"../specs/gestureModel.js\";\r\n\r\nimport { ManagedPromise, TimeoutPromise } from \"@keymanapp/web-utils\";\r\nimport { FulfillmentCause, PathMatcher } from \"./pathMatcher.js\";\r\nimport { CumulativePathStats } from \"../../cumulativePathStats.js\";\r\nimport { processSampleClientCoords } from \"../../../inputEventEngine.js\";\r\n\r\n/**\r\n * This interface specifies the minimal data necessary for setting up gesture-selection\r\n * among a set of gesture models that will conceptually follow from the most\r\n * recently-matched gesture-model. The most standard implementation of this is the\r\n * `GestureMatcher` class.\r\n *\r\n * Up until very recently, KeymanWeb would delegate certain gestures to be handled by\r\n * host apps when it was in an embedded state. While that pattern has been dropped,\r\n * the abstraction gained from reaching compatibility with it is useful. Either way,\r\n * for such scenarios, as long as fulfilled gestures can be linked to an implementation\r\n * of this interface, they can be integrated into the gesture-sequence staging system -\r\n * even if not matched directly by the recognizer itself.\r\n */\r\nexport interface PredecessorMatch {\r\n readonly sources: GestureSource[];\r\n readonly allSourceIds: string[];\r\n readonly primaryPath: GestureSource;\r\n readonly result: MatchResult;\r\n readonly model?: GestureModel;\r\n readonly baseItem: Type;\r\n readonly predecessor?: PredecessorMatch;\r\n}\r\n\r\nexport interface MatchResult {\r\n readonly matched: boolean,\r\n readonly action: GestureResolution\r\n}\r\n\r\nexport interface MatchResultSpec {\r\n readonly matched: boolean,\r\n readonly action: GestureResolutionSpec\r\n}\r\n\r\nexport class GestureMatcher implements PredecessorMatch {\r\n private sustainTimerPromise?: TimeoutPromise;\r\n public readonly model: GestureModel;\r\n\r\n private readonly pathMatchers: PathMatcher[];\r\n\r\n public get sources(): GestureSource[] {\r\n return this.pathMatchers.map((pathMatch, index) => {\r\n if(this.model.contacts[index].resetOnInstantFulfill) {\r\n return undefined;\r\n } else {\r\n return pathMatch.source;\r\n }\r\n }).filter((entry) => !!entry);\r\n }\r\n\r\n private _isCancelled: boolean = false;\r\n\r\n readonly predecessor?: PredecessorMatch;\r\n\r\n private readonly publishedPromise: ManagedPromise>; // unsure on the actual typing at the moment.\r\n private _result: MatchResult;\r\n\r\n public get promise() {\r\n return this.publishedPromise.corePromise;\r\n }\r\n\r\n constructor(\r\n model: GestureModel,\r\n sourceObj: GestureSource | PredecessorMatch\r\n ) {\r\n /* c8 ignore next 5 */\r\n if(!model || !sourceObj) {\r\n throw new Error(\"Construction of GestureMatcher requires a gesture-model spec and a source for related contact points.\");\r\n } else if(!model.sustainTimer && !sourceObj) {\r\n throw new Error(\"If the provided gesture-model spec lacks a sustain timer, there must be an active contact point.\");\r\n }\r\n\r\n // We condition on ComplexGestureSource since some unit tests mock the other type without\r\n // instantiating the actual type.\r\n const predecessor = sourceObj instanceof GestureSource ? null : sourceObj;\r\n const source = predecessor ? null : (sourceObj as GestureSource);\r\n\r\n this.predecessor = predecessor;\r\n this.publishedPromise = new ManagedPromise();\r\n\r\n this.model = model;\r\n if(model.sustainTimer) {\r\n this.sustainTimerPromise = new TimeoutPromise(model.sustainTimer.duration);\r\n this.sustainTimerPromise.then((elapsed) => {\r\n const shouldResolve = model.sustainTimer.expectedResult == elapsed;\r\n this.finalize(shouldResolve, 'timer');\r\n });\r\n }\r\n\r\n this.pathMatchers = [];\r\n\r\n const unfilteredSourceTouchpoints: GestureSource[] = source\r\n ? [ source ]\r\n : predecessor.sources;\r\n\r\n const sourceTouchpoints = unfilteredSourceTouchpoints.map((entry) => {\r\n if(source && entry == source) {\r\n // Due to internal delays that can occur when an incoming tap triggers\r\n // completion of a previously-existing gesture but is not included in it\r\n // (`resetOnInstantFulfill` mechanics), it is technically possible for a very\r\n // quick tap to be 'complete' by the time we start trying to match\r\n // against it on some devices. We should still try in such cases.\r\n return source;\r\n } else {\r\n return entry.isPathComplete ? null : entry;\r\n }\r\n }).reduce((cleansed, entry) => {\r\n return entry ? cleansed.concat(entry) : cleansed;\r\n }, [] as GestureSource[]);\r\n\r\n if(model.sustainTimer && sourceTouchpoints.length > 0) {\r\n // If a sustain timer is set, it's because we expect to have NO gesture-source _initially_.\r\n // If we actually have one, that's cause for rejection.\r\n //\r\n this.finalize(false, 'path');\r\n return;\r\n } else if(!model.sustainTimer && sourceTouchpoints.length == 0) {\r\n // If no sustain timer is set, we don't start against the specified set; that'll happen\r\n // once there's an actual source to support the modeled gesture.\r\n this.finalize(false, 'path');\r\n }\r\n\r\n for(let touchpointIndex = 0; touchpointIndex < sourceTouchpoints.length; touchpointIndex++) {\r\n const srcContact = sourceTouchpoints[touchpointIndex];\r\n\r\n if(srcContact instanceof GestureSourceSubview) {\r\n srcContact.disconnect(); // prevent further updates from mangling tracked path info.\r\n }\r\n\r\n // No need to filter out already-matched contact points, and doing so is more trouble\r\n // than its worth.\r\n\r\n const contactSpec = model.contacts[touchpointIndex];\r\n /* c8 ignore next 3 */\r\n if(!contactSpec) {\r\n throw new Error(`No contact model for inherited path: gesture \"${model.id}', entry ${touchpointIndex}`);\r\n }\r\n const inheritancePattern = contactSpec?.model.pathInheritance ?? 'chop';\r\n\r\n let preserveBaseItem: boolean = false;\r\n\r\n let contact: GestureSourceSubview;\r\n switch(inheritancePattern) {\r\n case 'reject':\r\n this.finalize(false, 'path');\r\n return;\r\n case 'full':\r\n contact = srcContact.constructSubview(false, true);\r\n this.addContactInternal(contact, srcContact.path.stats, true);\r\n continue;\r\n case 'partial':\r\n preserveBaseItem = true;\r\n // Intentional fall-through\r\n case 'chop':\r\n contact = srcContact.constructSubview(true, preserveBaseItem);\r\n this.addContactInternal(contact, srcContact.path.stats, true);\r\n break;\r\n }\r\n }\r\n }\r\n\r\n public cancel() {\r\n this._isCancelled = true;\r\n if(!this._result) {\r\n this.finalize(false, 'cancelled');\r\n }\r\n }\r\n\r\n public get isCancelled(): boolean {\r\n return this._isCancelled;\r\n }\r\n\r\n private finalize(matched: boolean, cause: FulfillmentCause): MatchResult {\r\n if(this.publishedPromise.isFulfilled) {\r\n return this._result;\r\n }\r\n\r\n try {\r\n // Determine the correct action-spec that should result from the finalization.\r\n let action: GestureResolutionSpec | ((RejectionDefault | RejectionReplace) & ResolutionItemSpec);\r\n if(matched) {\r\n // Easy peasy - resolutions only need & have the one defined action type.\r\n action = this.model.resolutionAction;\r\n } else {\r\n /*\r\n Some gesture types may wish to restart with a new base item if they fail due to\r\n it changing during its lifetime or due to characteristics of the contact-point's\r\n path.\r\n\r\n If a gesture model match is outright-cancelled, matcher restarts should be completely\r\n blocked. One notable reason: if a model-match is _immediately_ cancelled due to\r\n initial conditions, reattempting it can cause an infinite (async) loop!\r\n */\r\n if(cause != 'cancelled' && this.model.rejectionActions?.[cause]) {\r\n action = this.model.rejectionActions[cause];\r\n action.item = 'none';\r\n }\r\n\r\n // Rejection for other reasons, or if no special action is defined for item rejection:\r\n action = action || {\r\n type: 'none',\r\n item: 'none'\r\n };\r\n }\r\n\r\n // Determine the item source for the item to be reported for this gesture, if any.\r\n let resolutionItem: Type;\r\n const itemSource = action.item ?? 'current';\r\n switch(itemSource) {\r\n case 'none':\r\n resolutionItem = null;\r\n break;\r\n case 'base':\r\n resolutionItem = this.primaryPath.baseItem;\r\n break;\r\n case 'current':\r\n resolutionItem = this.primaryPath.currentSample.item;\r\n break;\r\n }\r\n\r\n // Do actual resolution now that we can convert the spec into a proper resolution object.\r\n let resolveObj: MatchResult = {\r\n matched: matched,\r\n action: {\r\n ...action,\r\n item: resolutionItem\r\n }\r\n };\r\n\r\n this.publishedPromise.resolve(resolveObj);\r\n\r\n this._result = resolveObj;\r\n return resolveObj;\r\n /* c8 ignore next 3 */\r\n } catch(err) {\r\n this.publishedPromise.reject(err);\r\n return {\r\n matched: false,\r\n action: {\r\n type: 'none',\r\n item: null\r\n }\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Applies any source-finalization specified by the model based on whether or not it was matched.\r\n * It is invalid to call this method before model evaluation is complete.\r\n *\r\n * Additionally, this should only be applied for \"selected\" gesture models - those that \"win\"\r\n * and are accepted as part of a GestureSequence.\r\n */\r\n public finalizeSources() {\r\n if(!this._result) {\r\n throw Error(\"Invalid state for source-finalization - the matcher's evaluation of the gesture model is not yet complete\");\r\n }\r\n\r\n const matched = this._result.matched;\r\n for(let i = 0; i < this.pathMatchers.length; i++) {\r\n const matcher = this.pathMatchers[i];\r\n const contactSpec = this.model.contacts[i];\r\n\r\n /* Future TODO:\r\n * This should probably include \"committing\" the state token and items used by the subview,\r\n * should they differ from the base source's original values.\r\n *\r\n * That said, this is only a 'polish' task, as we aren't actually relying on the base source\r\n * once we've started identifying gestures. It'll likely only matter if external users\r\n * desire to utilize the recognizer.\r\n */\r\n\r\n // If the path already terminated, no need to evaluate further for this contact point.\r\n if(matcher.source.isPathComplete) {\r\n continue;\r\n }\r\n\r\n if(matched && contactSpec.endOnResolve) {\r\n matcher.source.terminate(false);\r\n } else if(!matched && contactSpec.endOnReject) {\r\n matcher.source.terminate(false);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Determines the active path-matcher best suited to serve as the \"primary\" path for the gesture.\r\n *\r\n * This is needed for the following logic roles:\r\n * - If accepting a new contact point, `allowsInitialState` needs an existing path's sample for\r\n * comparison.\r\n * - When resolving, the \"primary\" path determines what item (if any) is generated for the gesture.\r\n *\r\n * If no matcher is active, but the currently-evaluating gesture has a direct ancestor, the best\r\n * matcher from the predecessor may be used instead.\r\n */\r\n public get primaryPath(): GestureSource {\r\n let bestMatcher: PathMatcher;\r\n let highestPriority = Number.NEGATIVE_INFINITY;\r\n for(let matcher of this.pathMatchers) {\r\n if(matcher.model.itemPriority > highestPriority) {\r\n highestPriority = matcher.model.itemPriority;\r\n bestMatcher = matcher;\r\n }\r\n }\r\n\r\n // Example case: multitap, with the current stage of the chain having zero active touchpaths...\r\n // but a previous 'link' having had a valid touchpath.\r\n //\r\n // Here, the best answer is to use the 'comparisonPath' from the prior link; it'll contain\r\n // the path-samples we'd intuitively expect to use for comparison, after all.\r\n if(!bestMatcher && this.predecessor) {\r\n return this.predecessor.primaryPath;\r\n }\r\n\r\n return bestMatcher?.source;\r\n }\r\n\r\n public get baseItem(): Type {\r\n return this.primaryPath.baseItem;\r\n }\r\n\r\n public get currentItem(): Type {\r\n return this.primaryPath.currentSample.item;\r\n }\r\n\r\n /*\r\n * Gets the GestureSource identifier corresponding to the gesture being matched\r\n * and all predecessor stages. All are relevant for resolving gesture-selection;\r\n * predecessor IDs become relevant for gesture stages that start without an\r\n * active GestureSource. (One that's not already finished its path)\r\n *\r\n * In theory, just one predecessor previous should be fine, rather than\r\n * 'all'... but that'd take a little extra work.\r\n */\r\n public get allSourceIds(): string[] {\r\n // Do not include any to-be-reset (thus, excluded) sources here.\r\n let currentIds = this.sources.map((entry) => entry.identifier);\r\n const predecessorIds = this.predecessor ? this.predecessor.allSourceIds : [];\r\n\r\n // Each ID should only be listed once, regardless of source.\r\n currentIds = currentIds.filter((entry) => predecessorIds.indexOf(entry) == -1);\r\n\r\n return currentIds.concat(predecessorIds);\r\n }\r\n\r\n mayAddContact(): boolean {\r\n return this.pathMatchers.length < this.model.contacts.length;\r\n }\r\n\r\n // for new incoming GestureSource\r\n addContact(simpleSource: GestureSource) {\r\n const existingContacts = this.pathMatchers.length;\r\n /* c8 ignore next 3 */\r\n if(!this.mayAddContact()) {\r\n throw new Error(`The specified gesture model does not support more than ${existingContacts} contact points.`);\r\n }\r\n\r\n this.addContactInternal(simpleSource.constructSubview(false, true), null);\r\n }\r\n\r\n public get result() {\r\n return this._result;\r\n }\r\n\r\n private addContactInternal(simpleSource: GestureSourceSubview, basePathStats: CumulativePathStats, whileInitializing?: boolean) {\r\n // The number of already-active contacts tracked for this gesture\r\n const existingContacts = this.pathMatchers.length;\r\n\r\n const contactSpec = this.model.contacts[existingContacts];\r\n const contactModel = new PathMatcher(contactSpec.model, simpleSource, new CumulativePathStats(basePathStats));\r\n // Add it early, as we need it to be accessible for reference via .primaryPath stuff below.\r\n this.pathMatchers.push(contactModel);\r\n\r\n let ancestorSource: GestureSource = null;\r\n let baseItem: Type = null;\r\n // If there were no existing contacts but a predecessor exists and a sustain timer\r\n // has been specified, it needs special base-item handling.\r\n if(!existingContacts && this.predecessor && this.model.sustainTimer) {\r\n ancestorSource = this.predecessor.primaryPath;\r\n const baseItemMode = this.model.sustainTimer.baseItem ?? 'result';\r\n let baseStateToken: StateToken;\r\n\r\n switch(baseItemMode) {\r\n case 'none':\r\n baseItem = null;\r\n break;\r\n case 'base':\r\n baseItem = this.predecessor.primaryPath.baseItem;\r\n baseStateToken = this.predecessor.primaryPath.stateToken;\r\n break;\r\n case 'result':\r\n baseItem = this.predecessor.result.action.item;\r\n baseStateToken = this.predecessor.primaryPath.currentSample.stateToken;\r\n break;\r\n }\r\n\r\n // Under 'sustain timer' mode, the concept is that the first new source is the\r\n // continuation and successor to `predecessor.primaryPath`. Its base `item`\r\n // should reflect this.\r\n simpleSource.baseItem = baseItem ?? simpleSource.baseItem;\r\n simpleSource.stateToken = baseStateToken;\r\n simpleSource.currentSample.stateToken = baseStateToken;\r\n\r\n // May be missing during unit tests.\r\n if(simpleSource.currentRecognizerConfig) {\r\n simpleSource.currentSample.item = simpleSource.currentRecognizerConfig.itemIdentifier(\r\n simpleSource.currentSample,\r\n null\r\n );\r\n }\r\n } else {\r\n // just use the highest-priority item source's base item and call it a day.\r\n // There's no need to refer to some previously-existing source for comparison.\r\n baseItem = this.primaryPath.baseItem;\r\n ancestorSource = this.primaryPath;\r\n }\r\n\r\n\r\n if(contactSpec.model.baseCoordReplacer) {\r\n const originalStats = simpleSource.path.stats;\r\n const replacementSampleBase = contactSpec.model.baseCoordReplacer(originalStats, baseItem);\r\n\r\n if(replacementSampleBase) {\r\n // 1. Complete the sample\r\n const replacementSample = {\r\n ...processSampleClientCoords(\r\n simpleSource.currentRecognizerConfig,\r\n replacementSampleBase.clientX,\r\n replacementSampleBase.clientY\r\n ),\r\n t: (replacementSampleBase.t || replacementSampleBase.t === 0) ? replacementSampleBase.t : originalStats.initialSample.t,\r\n item: baseItem,\r\n stateToken: simpleSource.stateToken\r\n };\r\n\r\n // 2. Replace it within the source's path-stats.\r\n simpleSource.path.replaceInitialSample(replacementSample);\r\n }\r\n }\r\n\r\n // Check that initial \"item\" and \"state\" properties are legal for this type of gesture.\r\n if(contactSpec.model.allowsInitialState) {\r\n const initialStateCheck = contactSpec.model.allowsInitialState(\r\n simpleSource.currentSample,\r\n ancestorSource.currentSample,\r\n baseItem,\r\n ancestorSource.stateToken\r\n );\r\n\r\n if(!initialStateCheck) {\r\n // The initial state check failed, and we should not permanently establish a\r\n // pathMatcher for a source that failed to meet initial conditions.\r\n this.pathMatchers.pop();\r\n\r\n /*\r\n To prevent any further retries for the model (via rejectionActions), we list the\r\n cause as 'cancelled'. 'Cancelled' match attempts will never be retried, and we\r\n wish to prevent an infinite (async) loop from retrying something we know will\r\n auto-cancel. (That loop would automatically end upon a different model's match\r\n or upon all possible models failing to match at the same time, but it's still\r\n far from ideal.)\r\n\r\n The rejection-action mechanism in MatcherSelector's `replacer` method (refer to\r\n https://github.com/keymanapp/keyman/blob/be867604e4b2650a60e69dc6bbe0b6115315efff/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/matcherSelector.ts#L559-L575)\r\n already blocks paths that are rejected synchronously by this method. Use of\r\n 'cancelled' is thus not necessary for avoiding the loop-scenario, but it does\r\n add an extra layer of protection. Also, it's more explicit about the fact that\r\n we _are_ permanently cancelling any and all future attempts to match against\r\n it in the future for the affected `GestureSource`(s).\r\n\r\n If we weren't using 'cancelled', 'item' would correspond best with a rejection\r\n here, as the decision is made due to a validation check against the initial item.\r\n */\r\n this.finalize(false, 'cancelled');\r\n\r\n /*\r\n * There's no need to process the gesture-model any further... and the\r\n * invalid state may correspond to assumptions in the path-model that\r\n * will be invalidated if we continue.\r\n */\r\n return;\r\n }\r\n }\r\n\r\n /*\r\n Now that we've done the initial-state check, we can check for instantly-matching and\r\n instantly-rejecting path models... particularly from from causes other than initial-item\r\n and state, such as rejection due to an extra touch.\r\n\r\n KMW example: longpresses cancel when a new touch comes in during the longpress timer;\r\n they should never become valid again for that base touch.\r\n */\r\n const result = contactModel.update();\r\n if(result?.type == 'reject') {\r\n /*\r\n Refer to the earlier comment in this method re: use of 'cancelled'; we\r\n need to prevent any and all further attempts to match against this model\r\n We'd instantly reject it anyway due to its rejected initial state.\r\n Failing to do so can cause an infinite async loop.\r\n\r\n If we weren't using 'cancelled', 'path' would correspond best with a\r\n rejection here, as the decision is made due to the GestureSource's\r\n current path being rejected by one of the `PathModel`s comprising the\r\n `GestureModel`.\r\n\r\n If the model's already been initialized, it's possible that a _new_\r\n incoming touch needs special handling. We'll allow one reset. In the\r\n case that it would try to restart itself, the restarted model will\r\n instantly fail and thus cancel.\r\n */\r\n this.finalize(false, whileInitializing ? 'cancelled' : 'path');\r\n return;\r\n }\r\n\r\n // Standard path: trigger either resolution or rejection when the contact model signals either.\r\n contactModel.promise.then((resolution) => {\r\n this.finalize(resolution.type == 'resolve', resolution.cause);\r\n });\r\n }\r\n\r\n update() {\r\n this.pathMatchers.forEach((matcher) => {\r\n try {\r\n matcher.update();\r\n } catch(err) {\r\n console.error(err);\r\n this.finalize(false, 'cancelled');\r\n }\r\n });\r\n }\r\n}", + "import { EventEmitter } from \"eventemitter3\";\r\n\r\nimport { ManagedPromise, timedPromise } from \"@keymanapp/web-utils\";\r\n\r\nimport { GestureSource, GestureSourceSubview } from \"../../gestureSource.js\";\r\nimport { GestureMatcher, MatchResult, PredecessorMatch } from \"./gestureMatcher.js\";\r\nimport { GestureModel } from \"../specs/gestureModel.js\";\r\n\r\ninterface GestureSourceTracker {\r\n /**\r\n * Should be the actual GestureSource instance, not a subview thereof.\r\n * Each `GestureMatcher` will construct its own 'subview' into the GestureSource\r\n * based on its model's needs.\r\n */\r\n source: GestureSource;\r\n matchPromise: ManagedPromise>;\r\n /**\r\n * Set to `true` during the timeout period needed to complete existing trackers &\r\n * initialize new ones. Once that process is complete, set to false.\r\n *\r\n * This is needed to ensure that failure to extend an existing gesture doesn't\r\n * result in outright selection-failure before attempting to match as a\r\n * newly-started gesture.\r\n */\r\n preserve: boolean;\r\n}\r\n\r\nexport interface MatcherSelection {\r\n matcher: PredecessorMatch,\r\n result: MatchResult\r\n}\r\n\r\ninterface EventMap {\r\n 'rejectionwithaction': (\r\n selection: MatcherSelection,\r\n replaceModelWith: (replacementModel: GestureModel) => void) => void;\r\n}\r\n\r\n/**\r\n * Because returning an unresolved Promise from an await func will await that Promise.\r\n *\r\n * This allows us to bypass that, resolving yet providing a pending Promise.\r\n */\r\ninterface SelectionSetupResults {\r\n selectionPromise: Promise>;\r\n sustainModeWithoutMatch?: boolean;\r\n}\r\n\r\n/**\r\n * This class is used to \"select\" successfully-matched gesture models from among an\r\n * active set of potential GestureMatchers. There may be multiple GestureSources /\r\n * contact-points active; it is able to resolve when they are correlated and how\r\n * resolution should proceed based upon the \"selected\" gesture model.\r\n *\r\n * When at least one \"match\" for a gesture model occurs, this engine ensures that the\r\n * highest-priority one that matched is selected. It will be returned via Promise along\r\n * with the specified match \"action\". If, instead, no model ends up matching a\r\n * GestureSource, the Promise will resolve when the last potential model is rejected,\r\n * providing values indicating match failure and the action to be taken.\r\n */\r\nexport class MatcherSelector extends EventEmitter> {\r\n private _sourceSelector: GestureSourceTracker[] = [];\r\n private potentialMatchers: GestureMatcher[] = [];\r\n\r\n public stateToken: StateToken;\r\n\r\n public readonly baseGestureSetId: string;\r\n\r\n /**\r\n * Used to force synchronization during `matchGesture` setup in case\r\n * of two simultaneous inputs that both require deferral to previously-\r\n * existing matchers that could resolve first.\r\n */\r\n private pendingMatchSetup?: Promise;\r\n\r\n private sustainMode: boolean = false;\r\n\r\n constructor(baseSetId?: string) {\r\n super();\r\n this.baseGestureSetId = baseSetId || 'default';\r\n }\r\n\r\n /**\r\n * Returns all active `GestureMatcher`s that are currently active for the specified `GestureSource`.\r\n * They will be specified in descending `resolutionPriority` order.\r\n * @param source\r\n * @returns\r\n */\r\n public potentialMatchersForSource(source: GestureSource) {\r\n return this.potentialMatchers.filter((matcher) => matcher.allSourceIds.find((id) => id == source.identifier));\r\n }\r\n\r\n /**\r\n *\r\n * @returns An array of all sources that will live on in `sustainWhenNested` mode.\r\n */\r\n public cascadeTermination(): GestureSource[] {\r\n const potentialMatchers = this.potentialMatchers;\r\n const matchersToCancel = potentialMatchers.filter((matcher) => !matcher.model.sustainWhenNested);\r\n\r\n // Leave any matchers for models that specify `sustainWhenNested`.\r\n const matchersToPreserve = potentialMatchers.filter((matcher) => matcher.model.sustainWhenNested);\r\n this.potentialMatchers = matchersToPreserve;\r\n\r\n // Now, we need to clean up any `matchGesture` calls that no longer have valid models to match\r\n // (because none specified `sustainWhenNested`).\r\n //\r\n // Easiest way: first, identify any GestureSources tied to match attempts that DO involve\r\n // a `sustainWhenNested` model.\r\n // 1. Find the source IDs referenced for each case... (map)\r\n // 2. Then flatten + deduplicate the entries of the resulting array. (reduce)\r\n const sourceIdsToPreserve = matchersToPreserve.map((matcher) => matcher.allSourceIds).reduce((compactArray, current) => {\r\n for(const entry of current) {\r\n if(compactArray.indexOf(entry) == -1) {\r\n compactArray.push(entry);\r\n }\r\n }\r\n\r\n return compactArray;\r\n }, [] as string[]);\r\n\r\n // Any source not in the previous array no longer has an active reference; no matches\r\n // can occur for it any longer.\r\n const sourcesToCancel = this._sourceSelector.filter((sourceTracker) => {\r\n return !sourceIdsToPreserve.find((id) => id == sourceTracker.source.identifier);\r\n });\r\n\r\n // Now we can actually trigger proper cancellation - both for the model-match attempts\r\n // and for the `matchGesture` call that referenced the cancelled model-match attempts.\r\n sourcesToCancel.forEach((sourceTracker) => {\r\n sourceTracker.matchPromise.resolve({\r\n matcher: null,\r\n result: {\r\n matched: false,\r\n action: {\r\n type: 'complete',\r\n item: null\r\n }\r\n }\r\n });\r\n\r\n const index = this._sourceSelector.indexOf(sourceTracker);\r\n if(index > -1) {\r\n this._sourceSelector.splice(index, 1);\r\n }\r\n });\r\n\r\n matchersToCancel.forEach((matcher) => matcher.cancel());\r\n this.sustainMode = true;\r\n\r\n return this._sourceSelector.map((data) => data.source);\r\n }\r\n\r\n /**\r\n * Aims to match the gesture-source's path against the specified set of gesture models. The\r\n * returned Promise will resolve either when a match is found or all models have rejected the path.\r\n *\r\n * In order to facilitate state management when an incoming source triggers a match for a\r\n * previously-existing gesture but is not considered part of it, this method involves two levels\r\n * of asynchronicity.\r\n *\r\n * 1. A source must wait for such \"triggered matches\" to fully resolve before new gesture models\r\n * based solely upon it may be built, as stateToken updates may occur as a result.\r\n *\r\n * `await` statements against this method will resolve when all valid model types for the source\r\n * have been initialized.\r\n *\r\n * 2. The object returned via the `await` Promise provides a `.selectionPromise`; this will resolve\r\n * once the best gesture-model match has been determined.\r\n * @param source\r\n * @param gestureModelSet\r\n */\r\n public async matchGesture(\r\n source: GestureSource,\r\n gestureModelSet: GestureModel[]\r\n ): Promise>;\r\n\r\n /**\r\n * Facilitates matching a new stage in an ongoing gesture-stage sequence based on a previously-\r\n * matched stage and the specified models for stages that may follow it.\r\n *\r\n * In order to facilitate state management when an incoming source triggers a match for a\r\n * previously-existing gesture but is not considered part of it, this method involves two levels\r\n * of asynchronicity.\r\n *\r\n * 1. A source must wait for such \"triggered matches\" to fully resolve before new gesture models\r\n * based solely upon it may be built, as stateToken updates may occur as a result.\r\n *\r\n * `await` statements against this method will resolve when all valid model types for the source\r\n * have been initialized.\r\n *\r\n * 2. The object returned via the `await` Promise provides a `.selectionPromise`; this will resolve\r\n * once the best gesture-model match has been determined.\r\n * @param source\r\n * @param gestureModelSet\r\n */\r\n public async matchGesture(\r\n priorStageMatcher: PredecessorMatch,\r\n gestureModelSet: GestureModel[]\r\n ): Promise>;\r\n\r\n public async matchGesture(\r\n source: GestureSource | PredecessorMatch,\r\n gestureModelSet: GestureModel[]\r\n ): Promise> {\r\n /*\r\n * To be clear, this _starts_ the source-tracking process. It's an async process, though.\r\n *\r\n * Operate based upon the actual GestureSource, not a subview. Subviews can get\r\n * 'detached', a state not compatible with the needs of this method.\r\n */\r\n const sourceNotYetStaged = source instanceof GestureSource;\r\n\r\n const determinePredecessorSources = (source: PredecessorMatch): GestureSource[] => {\r\n const directSources = (source.sources as GestureSourceSubview[]).map((source => source.baseSource));\r\n\r\n if(directSources && directSources.length > 0) {\r\n return directSources;\r\n } else if(!source.predecessor) {\r\n return [];\r\n } else {\r\n return determinePredecessorSources(source.predecessor);\r\n }\r\n }\r\n\r\n const sources = sourceNotYetStaged\r\n ? [source instanceof GestureSourceSubview ? source.baseSource : source]\r\n : determinePredecessorSources(source);\r\n\r\n // Defining these as locals helps the TS type-checker better infer types within\r\n // this method; a later assignment to `source` will remove its ability to infer\r\n // `source`'s type at this point.\r\n const unmatchedSource = sourceNotYetStaged ? source : null;\r\n const priorMatcher = sourceNotYetStaged ? null: source;\r\n\r\n // matchGesture calls should be queued and act atomically, in sequence.\r\n if(this.pendingMatchSetup) {\r\n const parentLockPromise = this.pendingMatchSetup;\r\n const childLock = new ManagedPromise();\r\n\r\n this.pendingMatchSetup = childLock.corePromise;\r\n\r\n // If a prior call is still waiting on the `await` below, wait for it to clear\r\n // entirely before proceeding; there could be effects for how the next part below is processed.\r\n\r\n await parentLockPromise;\r\n\r\n if(this.pendingMatchSetup == childLock.corePromise) {\r\n this.pendingMatchSetup = null;\r\n }\r\n childLock.resolve(); // allow the next matchGesture call through.\r\n }\r\n\r\n if(sourceNotYetStaged) {\r\n // Cancellation before a first stage is possible; in this case, there's no sequence\r\n // to trigger cleanup. We can do that here.\r\n unmatchedSource.path.on('invalidated', () => {\r\n this.dropSourcesWithIds([unmatchedSource.identifier]);\r\n })\r\n }\r\n\r\n const matchPromise = new ManagedPromise>();\r\n\r\n /*\r\n * First...\r\n * 1. Verify no duplicate sources (even if subviews)\r\n * 2. Set up source 'trackers' used for synchronization & result-reporting.\r\n */\r\n const sourceTrackers = sources.map((src) => {\r\n // TODO: Assertion check - there's no version of the source currently being actively matched.\r\n\r\n // Even if a component path is already completed, TRACK IT. It's by far the easiest way\r\n // to handle gesture stages that start without active sources - such as multitap stages after\r\n // the initial tap.\r\n\r\n // Sets up source selectors - the object that matches a source against its Promise.\r\n // Promises only resolve once, after all - once called, a \"selection\" has been made.\r\n const sourceSelectors: GestureSourceTracker = {\r\n source: src,\r\n matchPromise: matchPromise,\r\n preserve: true\r\n };\r\n this._sourceSelector.push(sourceSelectors);\r\n\r\n return sourceSelectors;\r\n });\r\n\r\n const synchronizationSet = sourceTrackers.map((track) => track.matchPromise);\r\n\r\n /**\r\n * If we received a single gesture-source on its own that's just starting out, it may be able\r\n * to fulfill secondary `contacts` entries for in-process gesture-models.\r\n *\r\n * If we're following up a previous gesture stage, meaning the contacts are already part of\r\n * an ongoing gesture-sequence and have known associations already... they're not allowed to\r\n * change their committed links; bypass this section.\r\n */\r\n if(sourceNotYetStaged) {\r\n const extendableMatcherSet = this.potentialMatchers.filter((matcher) => matcher.mayAddContact());\r\n extendableMatcherSet.forEach((matcher) => {\r\n // TODO: do we alter the resolution priority in any way, now that there's an extra touchpoint?\r\n // Answer is not yet clear; perhaps work on gesture-staging will help indicate if this would\r\n // be useful... and how it should act, if so.\r\n\r\n matcher.addContact(unmatchedSource);\r\n matcher.promise.then(this.matcherSelectionFilter(matcher, synchronizationSet));\r\n });\r\n\r\n if(extendableMatcherSet.length > 0) {\r\n const originalStateToken = this.stateToken;\r\n\r\n /* We need to wait for any and all pending promises to resolve after the previous loop -\r\n * if any gesture models have resolved, it is possible that our consumer may alter the\r\n * active state token as a consequence... and expect that to be used for the source if it\r\n * corresponds to a newly-starting gesture. See #7173 and compare with the simple-tap\r\n * shortcut in which a new second tap instantly resolves the first. (If the resolved\r\n * tap changes the active layer - the 'state token' here - that's what this addresses.)\r\n *\r\n * The easiest and cleanest way to ensure all Promises that can resolve, do so before\r\n * proceeding: `setTimeout` uses the macrotask queue, while `Promise`s resolve on the\r\n * microtask queue. Thus, awaiting completion of a 0-sec timeout lets everything\r\n * that can fulfill do so before this proceeds.\r\n *\r\n * Reference: https://javascript.info/event-loop\r\n */\r\n\r\n const matchingLock = new ManagedPromise();\r\n this.pendingMatchSetup = matchingLock.corePromise;\r\n\r\n await timedPromise(0);\r\n // A second one, in case of a deferred modipress completion (via awaitNested)\r\n // (which itself needs a macroqueue wait)\r\n await timedPromise(0);\r\n\r\n // Only clear the promise if no extra entries were added to the implied `matchGesture` queue.\r\n if(this.pendingMatchSetup == matchingLock.corePromise) {\r\n this.pendingMatchSetup = null;\r\n }\r\n\r\n matchingLock.resolve();\r\n\r\n // stateToken may have shifted by the time we regain control here.\r\n const incomingStateToken = this.stateToken;\r\n\r\n /* If we've reached this point, we should assume that the incoming source may act\r\n * independently as the start of a new gesture.\r\n *\r\n * Accordingly, if there's a new state token in place, we should ensure the source\r\n * reflects THAT token, rather than the default one it was given.\r\n *\r\n * If it ends up as part of an already-existing gesture, the 'subview' mechanic will\r\n * ensure that it is viewed correctly therein - as the 'subview' will have been\r\n * built before the code below takes effect and since the change below will not\r\n * propagate.\r\n */\r\n\r\n if(originalStateToken != incomingStateToken) {\r\n const currentSample = unmatchedSource.currentSample;\r\n unmatchedSource.stateToken = incomingStateToken;\r\n currentSample.stateToken = incomingStateToken;\r\n\r\n currentSample.item = source.currentRecognizerConfig.itemIdentifier(currentSample, null);\r\n unmatchedSource.baseItem = currentSample.item;\r\n }\r\n\r\n const newlyMatched = extendableMatcherSet.find((entry) => entry.result);\r\n\r\n // If the incoming Source triggered a match AND is included in the model,\r\n // do not build new independent models for it.\r\n if(newlyMatched && newlyMatched.allSourceIds.includes(source.identifier)) {\r\n matchPromise.resolve({\r\n matcher: null,\r\n result: {\r\n matched: false,\r\n action: {\r\n type: 'complete',\r\n item: null\r\n }\r\n }\r\n });\r\n\r\n return {\r\n selectionPromise: matchPromise.corePromise\r\n };\r\n }\r\n }\r\n }\r\n\r\n sourceTrackers.forEach((tracker) => {\r\n tracker.preserve = false;\r\n })\r\n\r\n // If in a sustain mode, no models for new sources may launch;\r\n // only existing sequences are allowed to continue.\r\n if(this.sustainMode && unmatchedSource) {\r\n matchPromise.resolve({\r\n matcher: null,\r\n result: {\r\n matched: false,\r\n action: {\r\n type: 'complete',\r\n item: null\r\n }\r\n }\r\n });\r\n\r\n return { selectionPromise: matchPromise.corePromise, sustainModeWithoutMatch: true };\r\n }\r\n\r\n /**\r\n * In either case, time to spin up gesture models limited to new sources,\r\n * that don't combine with already-active ones. This could be the first\r\n * stage in a sequence or a followup to a prior stage.\r\n */\r\n let newMatchers = gestureModelSet.map((model) => {\r\n try {\r\n /*\r\n Spinning up a new gesture model means running code for that model and\r\n path, which are defined outside of the engine. We should not allow\r\n errors from engine-external code to prevent us from continuing with\r\n unaffected models.\r\n\r\n It's also important to keep the overall flow going; this code is run\r\n during touch-start spinup. An abrupt stop due to an unhandled error\r\n here can lock up the AsyncDispatchQueue for touch events, locking up\r\n the engine!\r\n */\r\n return new GestureMatcher(model, unmatchedSource || priorMatcher)\r\n } catch (err) {\r\n console.error(err);\r\n return null;\r\n }\r\n // Filter out any models that failed to 'spin-up' due to exceptions.\r\n }).filter((entry) => !!entry);\r\n\r\n // If any newly-activating models are disqualified due to initial conditions, don't add them.\r\n newMatchers = newMatchers.filter((matcher) => !matcher.result || matcher.result.matched !== false);\r\n\r\n for(const matcher of newMatchers) {\r\n matcher.promise.then(this.matcherSelectionFilter(matcher, synchronizationSet));\r\n }\r\n\r\n // Were all the new potential models disqualified? If not, add them; if so, instantly say that none\r\n // could be selected.\r\n if(newMatchers.length > 0) {\r\n this.potentialMatchers = this.potentialMatchers.concat(newMatchers);\r\n } else {\r\n matchPromise.resolve({\r\n matcher: null,\r\n result: {\r\n matched: false,\r\n action: {\r\n type: 'complete',\r\n item: null\r\n }\r\n }\r\n });\r\n }\r\n\r\n /*\r\n * Easiest way to ensure resolution priorities are respected: keep 'em sorted in descending order.\r\n * When we iterate through on update-steps, we go sequentially; the first Promise to be marked\r\n * 'resolved' wins.\r\n */\r\n this.potentialMatchers.sort((a, b) => b.model.resolutionPriority - a.model.resolutionPriority);\r\n\r\n // Now that all GestureMatchers are built, reset ALL of our sync-update-check hooks.\r\n this.resetSourceHooks();\r\n\r\n return { selectionPromise: matchPromise.corePromise };\r\n }\r\n\r\n private readonly attemptSynchronousUpdate = () => {\r\n // Determine the most recent timestamp for all active sources. Sources no longer active should be\r\n // ignored, so we filter those out of this array.\r\n //\r\n // We maintain them because they can be relevant for certain 'sustain' scenarios, like for a\r\n // multitap following from a simple tap - referencing that base simple tap is important.\r\n const legalSources = this._sourceSelector.filter((tracker) => !tracker.source.isPathComplete);\r\n\r\n const sourceCurrentTimestamps = legalSources.map((tracker) => tracker.source.currentSample.t);\r\n const t = sourceCurrentTimestamps[0];\r\n\r\n // Ignore timestamps from already-terminated paths; they should not block synchronicity checks.\r\n if(sourceCurrentTimestamps.find((t2) => (t != t2))) {\r\n return;\r\n }\r\n\r\n this.potentialMatchers.forEach((matcher) => matcher.update());\r\n };\r\n\r\n private resetSourceHooks() {\r\n const resetHooks = (gestureSource: GestureSource) => {\r\n // GestureSourceSubviews stay synchronized with their 'base' via event handlers.\r\n // We want GestureMatchers to receive all updates before we attempt a sync'd update.\r\n const baseSource = gestureSource;\r\n\r\n // So, a resetHooks call says to remove the old handler...\r\n baseSource.path.off('step', this.attemptSynchronousUpdate);\r\n baseSource.path.off('complete', this.attemptSynchronousUpdate);\r\n baseSource.path.off('invalidated', this.attemptSynchronousUpdate);\r\n\r\n // And re-add it, but at the end of the handler list.\r\n baseSource.path.on('step', this.attemptSynchronousUpdate);\r\n baseSource.path.on('complete', this.attemptSynchronousUpdate);\r\n baseSource.path.on('invalidated', this.attemptSynchronousUpdate);\r\n }\r\n\r\n // Make sure our source-watching hooks are the last handler for the event;\r\n // matcher-handlers should go first. (Due to how subview synchronization works)\r\n this._sourceSelector.forEach((entry) => resetHooks(entry.source));\r\n }\r\n\r\n public dropSourcesWithIds(idsToClean: string[]) {\r\n for(const id of idsToClean) {\r\n const index = this._sourceSelector.findIndex((entry) => entry.source.identifier == id);\r\n if(index > -1) {\r\n // Ensure that any pending MatcherSelector and/or GestureSequence promises dependent\r\n // on the source fully resolve (with cancellation).\r\n const droppedSelector = this._sourceSelector.splice(index, 1)[0];\r\n droppedSelector.matchPromise.resolve({\r\n matcher: null,\r\n result: {\r\n matched: false,\r\n action: {\r\n type: 'none',\r\n item: null\r\n }\r\n }\r\n });\r\n }\r\n }\r\n }\r\n\r\n private matchersForSource(source: GestureSource) {\r\n return this.potentialMatchers.filter((matcher) => {\r\n return !!matcher.sources.find((src) => src.identifier == source.identifier)\r\n });\r\n }\r\n\r\n private matcherSelectionFilter(matcher: GestureMatcher, matchSynchronizers: ManagedPromise[]) {\r\n // Returns a closure-captured Promise-resolution handler used by individual GestureMatchers managed\r\n // by this class instance.\r\n return async (result: MatchResult) => {\r\n // Note: is only called by GestureMatcher Promises that are resolving.\r\n\r\n // Do not bypass match handling just because a synchronization promise is fulfilled.\r\n // If a source was force-cancelled, cascading to a call of this handler, we still\r\n // need to perform internal state cleanup.\r\n\r\n if(matcher.isCancelled) {\r\n result = {\r\n matched: false,\r\n action: {\r\n type: 'none',\r\n item: null\r\n }\r\n };\r\n } else {\r\n // Since we've selected this matcher, it should apply any model-specified finalization necessary.\r\n matcher.finalizeSources();\r\n }\r\n\r\n // Find ALL associated match-promises for sources matched by the matcher.\r\n const matchedContactIds = matcher.allSourceIds;\r\n\r\n const sourceMetadata = matchedContactIds.map((id) => {\r\n return this._sourceSelector.find((metadata) => metadata.source.identifier == id);\r\n }).filter((entry) => !!entry); // remove `undefined` entries, as they're irrelevant.\r\n\r\n // We have a result for this matcher; go ahead and remove it from the 'potential' list.\r\n const matcherIndex = this.potentialMatchers.indexOf(matcher);\r\n if(matcherIndex == -1) {\r\n // It's already been handled; do not re-attempt.\r\n return;\r\n }\r\n\r\n this.potentialMatchers.splice(matcherIndex, 1);\r\n\r\n /*\r\n * This is the common case for failed gesture matches. It should never be set\r\n * for a successful gesture match. This is a \"didn't match\" signal, so we don't\r\n * do any gesture-staging stuff here or enter a state where we need to ignore\r\n * other matchers.\r\n */\r\n if(result.action.type == 'none') {\r\n this.finalizeMatcherlessTrackers(sourceMetadata);\r\n\r\n /* We allow any other matchers against the represented sources to REMAIN AS THEY ARE.\r\n * Special handling is only needed once none are left, which is what the\r\n * `finalizeMatcherlessTrackers` call represents.\r\n *\r\n * This isn't generally a \"no matches available case; it's a \"_this_ model didn't match\"\r\n * case that only _sometimes_ happens to be the final \"match not available\" case.\r\n */\r\n return;\r\n }\r\n\r\n if(!result.matched) {\r\n // There is an action to be resolved...\r\n // But we didn't actually MATCH a gesture.\r\n const replacer = (replacementModel: GestureModel) => {\r\n if(this.sustainMode && !replacementModel.sustainWhenNested) {\r\n this.finalizeMatcherlessTrackers(sourceMetadata);\r\n return;\r\n }\r\n\r\n const replacementMatcher = new GestureMatcher(replacementModel, matcher);\r\n\r\n /* IMPORTANT: verify that the replacement model is initially valid.\r\n *\r\n * If the model would be 'spun up' for matching in a state where, even initially,\r\n * it cannot match, cancel the replacement. (Otherwise, we could near-instantly\r\n * re-trigger further replacements that will also fail!)\r\n *\r\n * In particular, a multitap operation involves a state with no contact points,\r\n * while a longpress would fail when the state is reached. Longpress models\r\n * will fail when in the state... and should _permanently_ fail for a\r\n * GestureSequence containing a finished GestureSource once said state is reached.\r\n */\r\n if(replacementMatcher.result && replacementMatcher.result.matched == false) {\r\n // If this occurs, and it was the last possible tracker, we need to resolve its\r\n // `matchGesture` promise.\r\n this.finalizeMatcherlessTrackers(sourceMetadata);\r\n return;\r\n }\r\n\r\n replacementMatcher.promise.then(this.matcherSelectionFilter(replacementMatcher, sourceMetadata.map((entry) => entry.matchPromise)));\r\n this.potentialMatchers.push(replacementMatcher);\r\n\r\n this.resetSourceHooks();\r\n };\r\n\r\n // So we emit an event to signal the rejection & allow its replacement via the closure above.\r\n this.emit('rejectionwithaction', {matcher, result}, replacer);\r\n return;\r\n } else /* if(result.matched) */ {\r\n for(const tracker of sourceMetadata) {\r\n // If we have a successful gesture match, we should proactively clear out the matchers\r\n // that (a) didn't win and (b) use at least one source in common with the winner.\r\n const losingMatchers = this.matchersForSource(tracker.source);\r\n this.potentialMatchers = this.potentialMatchers.filter((matcher) => {\r\n return !losingMatchers.find((matcher2) => matcher == matcher2);\r\n });\r\n\r\n /*\r\n * While the 'synchronizer' setup will perfectly handle most cases, we need this block to catch\r\n * a somewhat niche case: if a second source was added to the matcher at a later point in time,\r\n * there are two separate Promise handlers - with separate synchronization sets. We use the\r\n * `cancel` method to ensure that cancellation from one set propagates to the other handler.\r\n * (It seems the simplest & most straightforward approach to do ensure localized, per-matcher\r\n * consistency without mangling any matchers that shouldn't be affected.)\r\n *\r\n * This can arise if a modipress is triggered at the same time a new touchpoint begins, which\r\n * could trigger a simple-tap.\r\n */\r\n losingMatchers.forEach((matcher) => {\r\n // Triggers resolution of remaining matchers for the model-match, but that's\r\n // asynchronous.\r\n matcher.cancel();\r\n });\r\n\r\n // Drop the newly-cancelled trackers.\r\n this._sourceSelector = this._sourceSelector.filter((a) => !sourceMetadata.find((b) => a == b));\r\n\r\n // And now for one line with some \"heavy lifting\":\r\n\r\n /*\r\n * Fulfills the contract set by `matchGesture`.\r\n */\r\n tracker.matchPromise.resolve({matcher, result});\r\n }\r\n\r\n // No use of `finalizeMatcherlessTrackers` here; this is the path where we DO get\r\n // and signal (that last resolve above) a successful gesture-model match.\r\n }\r\n };\r\n }\r\n\r\n /**\r\n * This internal method provides common-case finalization for cases in which\r\n * all available gesture models for at least one source have resolved with none\r\n * matching it. This includes triggering resolution of `Promise`s returned by the\r\n * `matchGesture` call(s) corresponding to the now-unmatchable source(s).\r\n *\r\n * In short, this method should be called at any point where the Selector\r\n * may go from having one or more potential active matchers to zero for at\r\n * least one GestureSource.\r\n * @param trackers\r\n * @returns\r\n */\r\n private finalizeMatcherlessTrackers(trackers: GestureSourceTracker[]) {\r\n // Check - are there any remaining matchers compatible with the rejected matcher's sources?\r\n const remainingMatcherStats = trackers.map((tracker) => {\r\n return {\r\n tracker: tracker,\r\n // We need to inspect each matcher's `contacts` entries for references to the source.\r\n pendingCount: this.potentialMatchers.filter((matcher) => {\r\n return !!matcher.allSourceIds.find((id) => tracker.source.identifier == id);\r\n }).length // and tally up a count at the end.\r\n };\r\n });\r\n\r\n // If we just rejected the last possible matcher for a tracked gesture-source...\r\n // then, for each such affected source...\r\n for(const stat of remainingMatcherStats) {\r\n if(stat.pendingCount == 0 && !stat.tracker.preserve) {\r\n // ... report the failure and signal to close-out that source / stop tracking it.\r\n stat.tracker.matchPromise.resolve({\r\n matcher: null,\r\n result: {\r\n matched: false,\r\n action: {\r\n type: 'complete',\r\n item: null\r\n }\r\n }\r\n });\r\n }\r\n }\r\n }\r\n}", + "import { EventEmitter } from \"eventemitter3\";\r\n\r\nimport { GestureModelDefs, getGestureModel } from \"../specs/gestureModelDefs.js\";\r\nimport { GestureSourceSubview } from \"../../gestureSource.js\";\r\nimport { MatchResult } from \"./gestureMatcher.js\";\r\nimport { GestureModel, GestureResolution } from \"../specs/gestureModel.js\";\r\nimport { MatcherSelection, MatcherSelector } from \"./matcherSelector.js\";\r\nimport { GestureRecognizerConfiguration, TouchpointCoordinator } from \"../../../index.js\";\r\nimport { ManagedPromise, timedPromise } from \"@keymanapp/web-utils\";\r\n\r\nexport class GestureStageReport {\r\n /**\r\n * The id of the GestureModel spec that was matched at this stage of the\r\n * GestureSequence.\r\n */\r\n public readonly matchedId: string;\r\n\r\n /**\r\n * The set id of gesture models that were allowed for this stage of the\r\n * GestureSequence.\r\n */\r\n public readonly gestureSetId: string;\r\n\r\n public readonly linkType: MatchResult['action']['type'];\r\n /**\r\n * The `item`, if any, specified for selection by the matched gesture model.\r\n */\r\n public readonly item: Type;\r\n /**\r\n * The set of GestureSource contact points matched to this stage of the GestureSequence.\r\n * The first one listed (index 0) will be the entry responsible for selection of the\r\n * `item` field.\r\n */\r\n public readonly sources: GestureSourceSubview[];\r\n\r\n public readonly allSourceIds: string[];\r\n\r\n constructor(selection: MatcherSelection, gestureSetId: string) {\r\n const { matcher, result } = selection;\r\n this.gestureSetId = gestureSetId;\r\n this.matchedId = matcher?.model.id;\r\n this.linkType = result.action.type;\r\n this.item = result.action.item;\r\n\r\n // Assumption: GestureMatcher always builds the Subview type when constructing each PathMatcher.\r\n // This assumption currently holds, though we could always do a quick instanceof-check to build a\r\n // subview if it isn't already one.\r\n //\r\n // Each entry has a .baseSource property that may be used to refer to the non-snapshotted version\r\n // of the source by consumers of this object.\r\n this.sources = matcher?.sources as GestureSourceSubview[];\r\n\r\n // Just to be extra-sure they don't continue to update.\r\n // Alternatively, we could just make an extra copy and then instantly \"disconnect\" the new instance.\r\n this.sources?.forEach((source) => source.disconnect());\r\n // Make sure that the `primaryPath` source ends up as the first entry.\r\n this.sources?.sort((a, b) => {\r\n if(matcher?.primaryPath == a) {\r\n return -1;\r\n } else if(matcher?.primaryPath == b) {\r\n return 1;\r\n } else {\r\n return 0;\r\n }\r\n })\r\n\r\n this.allSourceIds = matcher?.allSourceIds || [];\r\n }\r\n}\r\n\r\ninterface PushConfig {\r\n type: 'push',\r\n config: GestureRecognizerConfiguration\r\n}\r\n\r\n// I don't think we currently need this option, but it fits as part of the overall conceptual\r\n// model and is good for generality.\r\ninterface PopConfig {\r\n type: 'pop',\r\n count: number\r\n}\r\n\r\nexport type ConfigChangeClosure = (configStackCommand: PushConfig | PopConfig) => void;\r\n\r\ninterface EventMap {\r\n stage: (\r\n stageReport: GestureStageReport,\r\n changeConfiguration: ConfigChangeClosure\r\n ) => void;\r\n complete: () => void;\r\n}\r\n\r\nexport class GestureSequence extends EventEmitter> {\r\n public stageReports: GestureStageReport[];\r\n\r\n // It's not specific to just this sequence... but it does have access to\r\n // the potential next stages.\r\n private selector: MatcherSelector;\r\n\r\n // We need this reference in order to properly handle 'setchange' resolution actions when staging.\r\n private touchpointCoordinator: TouchpointCoordinator;\r\n // Selectors have locked-in 'base gesture sets'; this is only non-null if\r\n // in a 'setchange' action.\r\n private pushedSelector?: MatcherSelector;\r\n\r\n private gestureConfig: GestureModelDefs;\r\n private markedComplete: boolean = false;\r\n\r\n // Note: the first stage will be available under `stageReports` after awaiting a simple Promise.resolve().\r\n constructor(\r\n firstSelectionMatch: MatcherSelection,\r\n gestureModelDefinitions: GestureModelDefs,\r\n selector: MatcherSelector,\r\n touchpointCoordinator: TouchpointCoordinator\r\n ) {\r\n super();\r\n\r\n this.stageReports = [];\r\n this.selector = selector;\r\n this.selector.on('rejectionwithaction', this.modelResetHandler);\r\n this.once('complete', () => {\r\n if(this.pushedSelector) {\r\n // The `popSelector` method is responsible for triggering cascading cancellations if\r\n // there are nested GestureSequences.\r\n //\r\n // As this tends to affect which gestures are permitted, it's important this is done\r\n // any time the GestureSequence is cancelled or completed, for any reason.\r\n this.touchpointCoordinator?.popSelector(this.pushedSelector);\r\n this.pushedSelector = null;\r\n }\r\n\r\n this.selector.off('rejectionwithaction', this.modelResetHandler);\r\n this.selector.dropSourcesWithIds(this.allSourceIds);\r\n\r\n // Dropping the reference here gives us two benefits:\r\n // 1. Allows garbage collection to do its thing; this might be the last reference left to the selector instance.\r\n // 2. Acts as an obvious flag / indicator of sequence completion.\r\n this.selector = null;\r\n });\r\n this.gestureConfig = gestureModelDefinitions;\r\n\r\n // So that we can...\r\n // 1. push a different selector as active (and restore it later) - say, for modipress\r\n // - 'push' & corresponding pop-like resolution behaviors\r\n // 2. push a different default gesture set ID (and restore it later)\r\n this.touchpointCoordinator = touchpointCoordinator;\r\n\r\n // Adds a slight delay; a constructed Sequence will provide a brief window of time -\r\n // until the event queue next 'ticks' - to receive data about the base stage via the\r\n // same 'stage' event raised for all subsequent stages.\r\n Promise.resolve().then(() => this.selectionHandler(firstSelectionMatch));\r\n }\r\n\r\n public get allSourceIds(): string[] {\r\n // Note: there is a brief window of time - between construction & the deferred first\r\n // 'stage' event - during which this array may be of length 0.\r\n return this.stageReports[this.stageReports.length - 1]?.allSourceIds ?? [];\r\n }\r\n\r\n private get baseGestureSetId(): string {\r\n return this.selector?.baseGestureSetId ?? null;\r\n }\r\n\r\n /**\r\n * Returns an array of IDs for gesture models that are still valid for the `GestureSource`'s\r\n * current state. They will be specified in descending `resolutionPriority` order.\r\n */\r\n public get potentialModelMatchIds(): string[] {\r\n // If `this.selector` is null, it's because no further matches are possible.\r\n // We've already emitted the 'complete' event as well.\r\n if(!this.selector) {\r\n return [];\r\n }\r\n\r\n const selectors = [ this.selector ];\r\n if(this.pushedSelector) {\r\n selectors.push(this.pushedSelector);\r\n }\r\n\r\n // The new round of model-matching is based on the sources used by the previous round.\r\n // This is important; 'sustainTimer' gesture models may rely on a now-terminated source\r\n // from that previous round (like with multitaps).\r\n const lastStageReport = this.stageReports[this.stageReports.length-1];\r\n const trackedSources = lastStageReport.sources;\r\n\r\n const potentialMatches = trackedSources.map((source) => {\r\n return selectors.map((selector) => selector.potentialMatchersForSource(source)\r\n .map((matcher) => matcher.model.id)\r\n )\r\n }).reduce((flattened, arr) => flattened.concat(arr))\r\n .reduce((deduplicated, arr) => {\r\n for(let entry of arr) {\r\n if(deduplicated.indexOf(entry) == -1) {\r\n deduplicated.push(entry);\r\n }\r\n }\r\n return deduplicated;\r\n }, [] as string[]);\r\n\r\n return potentialMatches;\r\n }\r\n\r\n private readonly selectionHandler = async (selection: MatcherSelection) => {\r\n const gestureSet = this.pushedSelector?.baseGestureSetId || this.selector?.baseGestureSetId;\r\n const matchReport = new GestureStageReport(selection, gestureSet);\r\n if(selection.matcher) {\r\n this.stageReports.push(matchReport);\r\n }\r\n\r\n const sourceTracker = selection.matcher ?? this.stageReports[this.stageReports.length-1];\r\n const sources = sourceTracker?.sources.map((matchSource) => {\r\n return matchSource instanceof GestureSourceSubview ? matchSource.baseSource : matchSource;\r\n }) ?? [];\r\n\r\n const actionType = selection.result.action.type;\r\n if(actionType == 'complete' || actionType == 'none') {\r\n sources.forEach((source) => {\r\n if(!source.isPathComplete) {\r\n source.terminate(actionType == 'none');\r\n }\r\n });\r\n\r\n if(!selection.result.matched) {\r\n if(!this.markedComplete) {\r\n this.markedComplete = true;\r\n this.emit('complete');\r\n }\r\n return;\r\n }\r\n }\r\n\r\n if(actionType == 'complete' && this.touchpointCoordinator && this.pushedSelector) {\r\n // Cascade-terminade all nested selectors, but don't remove / pop them yet.\r\n // Their selection mode remains valid while their gestures are sustained.\r\n const sustainedSources = this.touchpointCoordinator?.sustainSelectorSubstack(this.pushedSelector);\r\n\r\n const sustainCompletionPromises = sustainedSources.map((source) => {\r\n const promise = new ManagedPromise();\r\n source.path.on('invalidated', () => promise.resolve());\r\n source.path.on('complete', () => promise.resolve());\r\n return promise.corePromise;\r\n });\r\n\r\n if(sustainCompletionPromises.length > 0 && selection.result.action.awaitNested) {\r\n await Promise.all(sustainCompletionPromises);\r\n // Ensure all nested gestures finish resolving first before continuing by\r\n // waiting against the macroqueue.\r\n await timedPromise(0);\r\n }\r\n\r\n // Actually drops the selection-mode state once all is complete.\r\n // The drop MUST come after the `await` above.\r\n this.touchpointCoordinator?.popSelector(this.pushedSelector);\r\n\r\n // May still need it active?\r\n // this.pushedSelector.off('rejectionwithaction', this.modelResetHandler);\r\n this.pushedSelector = null;\r\n }\r\n\r\n // Raise the event, providing a functor that allows the listener to specify an alt config for the next stage.\r\n // Example case: longpress => subkey selection - the subkey menu has different boundary conditions.\r\n this.emit('stage', matchReport, (command) => {\r\n // Assertion: each Source may only be part of one GestureSequence.\r\n // As such, pushed and popped configs may only come from one influence - the GestureSequence's\r\n // staging transitions.\r\n if(command.type == 'pop') {\r\n sources.forEach((source) => source.popRecognizerConfig());\r\n } else /* if(command.type == 'push') */ {\r\n sources.forEach((source) => source.pushRecognizerConfig(command.config));\r\n }\r\n });\r\n\r\n // ... right, the gesture-definitions.\r\n\r\n // In some automated tests, `this.touchpointCoordinator` may be `null`.\r\n let selectorNotCurrent = false;\r\n if(this.touchpointCoordinator) {\r\n selectorNotCurrent = !this.touchpointCoordinator.selectorStackIncludes(this.selector);\r\n }\r\n\r\n let nextModels = modelSetForAction(selection.result.action, this.gestureConfig, this.baseGestureSetId);\r\n if(selectorNotCurrent) {\r\n // If this sequence's selector isn't current, we're in an unrooted state; the parent, base gesture\r\n // whose state we were in when the gesture began has ended.)\r\n //\r\n // Example: we're a gesture that was triggered under a modipress state, but the modipress itself\r\n // has ended. Subkey selection should be allowed to continue, but not much else.\r\n nextModels = nextModels.filter((model) => model.sustainWhenNested);\r\n }\r\n\r\n if(nextModels.length > 0) {\r\n // Note: resolve selection-mode changes FIRST, before building the next GestureModel in the sequence.\r\n // If a selection-mode change is triggered, any openings for new contacts on the next model can only\r\n // be fulfilled if handled by the corresponding (pushed) selector, rather than the sequence's base selector.\r\n\r\n // Handling 'setchange' resolution actions (where one gesture enables a different gesture set for others\r\n // while active. Example case: modipress.)\r\n if(actionType == 'chain' && selection.result.action.selectionMode == this.pushedSelector?.baseGestureSetId) {\r\n // do nothing; maintain the existing 'selectionMode' behavior\r\n } else {\r\n // pop the old one, if it exists - if it matches our expectations for a current one.\r\n if(this.pushedSelector) {\r\n this.pushedSelector.off('rejectionwithaction', this.modelResetHandler);\r\n this.touchpointCoordinator?.popSelector(this.pushedSelector);\r\n this.pushedSelector = null;\r\n }\r\n\r\n /* Note: we do not change the instance held by this class - it gets to maintain access\r\n * to its original selector regardless.\r\n *\r\n * Example use-case: during subkey selection, which is the intended followup for a longpress,\r\n * either...\r\n *\r\n * 1. No other gestures (new touch contact points) should be allowed and/or trigger interactions\r\n * 2. OR such attempts should automatically cancel the subkey-selection process.\r\n *\r\n * For approach 1, we 'allow' an empty set of gestures, disabling all of them.\r\n *\r\n * For approach 2, we permit a single type of new gesture; when triggered, the gesture consumer\r\n * can then use that to trigger cancellation of the subkey-selection mode.\r\n */\r\n\r\n if(actionType == 'chain') {\r\n const targetSet = selection.result.action.selectionMode;\r\n if(targetSet) {\r\n // push the new one.\r\n const changedSetSelector = new MatcherSelector(targetSet);\r\n changedSetSelector.on('rejectionwithaction', this.modelResetHandler);\r\n this.pushedSelector = changedSetSelector;\r\n this.touchpointCoordinator?.pushSelector(changedSetSelector);\r\n }\r\n }\r\n }\r\n\r\n /* If a selector has been pushed, we need to delegate the next gesture model in the chain\r\n * to it in case it has extra contacts, as those will be processed under the pushed selector.\r\n *\r\n * Example case: a modipress + multitap key should prevent further multitap if a second,\r\n * unrelated key is tapped. Detecting that second tap is only possible via the pushed\r\n * selector.\r\n *\r\n * Future models in the chain are still drawn from the _current_ selector.\r\n */\r\n const nextStageSelector = this.pushedSelector ?? this.selector;\r\n\r\n // Note: if a 'push', that should be handled by an event listener from the main engine driver (or similar)\r\n const modelingSpinupPromise = nextStageSelector.matchGesture(selection.matcher, nextModels);\r\n modelingSpinupPromise.then(async (selectionHost) => this.selectionHandler(await selectionHost.selectionPromise));\r\n } else {\r\n // Any extra finalization stuff should go here, before the event, if needed.\r\n if(!this.markedComplete) {\r\n this.markedComplete = true;\r\n this.emit('complete');\r\n }\r\n }\r\n }\r\n\r\n private readonly modelResetHandler = (\r\n selection: MatcherSelection,\r\n replaceModelWith: (model: GestureModel) => void\r\n ) => {\r\n const sourceIds = selection.matcher.allSourceIds;\r\n\r\n // If none of the sources involved match a source already included in the sequence, bypass\r\n // this handler; it belongs to a different sequence or one that's beginning.\r\n //\r\n // This works even for multitaps because we include the most recent ancestor sources in\r\n // `allSourceIds` - that one will match here.\r\n //\r\n // Also sufficiently handles cases where selection is delegated to the pushedSelector,\r\n // since new gestures under the alternate state won't include a source id from the base\r\n // sequence.\r\n if(this.allSourceIds.find((a) => sourceIds.indexOf(a) == -1)) {\r\n return;\r\n }\r\n\r\n if(selection.result.action.type == 'replace') {\r\n replaceModelWith(getGestureModel(this.gestureConfig, selection.result.action.replace));\r\n } else {\r\n throw new Error(\"Missed a case in implementation!\");\r\n }\r\n };\r\n\r\n public cancel() {\r\n const sources = this.stageReports[this.stageReports.length - 1].sources;\r\n sources.forEach((src) => src.baseSource.isPathComplete || src.baseSource.terminate(true));\r\n if(!this.markedComplete) {\r\n this.markedComplete = true;\r\n this.emit('complete');\r\n }\r\n }\r\n\r\n public toJSON(): any {\r\n return this.stageReports;\r\n }\r\n}\r\n\r\nexport function modelSetForAction(\r\n action: GestureResolution,\r\n gestureModelDefinitions: GestureModelDefs,\r\n activeSetId: string\r\n): GestureModel[] {\r\n switch(action.type) {\r\n case 'none':\r\n case 'complete':\r\n return [];\r\n case 'replace':\r\n return [getGestureModel(gestureModelDefinitions, action.replace)];\r\n case 'chain':\r\n return [getGestureModel(gestureModelDefinitions, action.next)];\r\n default:\r\n throw new Error(\"Unexpected case arose within `processGestureAction` method\");\r\n }\r\n}", + "import { EventEmitter } from \"eventemitter3\";\r\nimport { InputEngineBase } from \"./inputEngineBase.js\";\r\nimport { GestureSource } from \"./gestureSource.js\";\r\nimport { MatcherSelection, MatcherSelector } from \"./gestures/matchers/matcherSelector.js\";\r\nimport { GestureSequence } from \"./gestures/matchers/gestureSequence.js\";\r\nimport { GestureModelDefs, getGestureModel, getGestureModelSet } from \"./gestures/specs/gestureModelDefs.js\";\r\nimport { GestureModel } from \"./gestures/specs/gestureModel.js\";\r\nimport { InputSample } from \"./inputSample.js\";\r\nimport { GestureDebugPath } from \"./gestureDebugPath.js\";\r\nimport { reportError } from \"../reportError.js\";\r\n\r\ninterface EventMap {\r\n /**\r\n * Indicates that a new potential gesture has begun.\r\n * @param input\r\n * @returns\r\n */\r\n 'inputstart': (input: GestureSource) => void;\r\n\r\n 'recognizedgesture': (sequence: GestureSequence) => void;\r\n}\r\n\r\n/**\r\n * This class is responsible for interpreting the output of the various input-engine types\r\n * and facilitating the detection of related gestures. Its role is to serve as a headless\r\n * version of the main `GestureRecognizer` class, avoiding its DOM and DOM-event dependencies.\r\n *\r\n * Of particular note: when a gesture involves multiple touchpoints - like a multitap - this class\r\n * is responsible for linking related touchpoints together for the detection of that gesture.\r\n */\r\nexport class TouchpointCoordinator extends EventEmitter> {\r\n private inputEngines: InputEngineBase[];\r\n private selectorStack: MatcherSelector[] = [new MatcherSelector()];\r\n\r\n private gestureModelDefinitions: GestureModelDefs;\r\n\r\n private _activeSources: GestureSource[] = [];\r\n private _activeGestures: GestureSequence[] = [];\r\n\r\n private _stateToken: StateToken;\r\n\r\n private _history: (GestureSource | GestureSequence)[] = [];\r\n private historyMax: number;\r\n\r\n public constructor(gestureModelDefinitions: GestureModelDefs, inputEngines?: InputEngineBase[], historyLength?: number) {\r\n super();\r\n\r\n this.historyMax = historyLength > 0 ? historyLength : 0;\r\n\r\n this.gestureModelDefinitions = gestureModelDefinitions;\r\n this.inputEngines = [];\r\n if(inputEngines) {\r\n for(let engine of inputEngines) {\r\n this.addEngine(engine);\r\n }\r\n }\r\n\r\n this.selectorStack[0].on('rejectionwithaction', this.modelResetHandler)\r\n }\r\n\r\n private readonly modelResetHandler = (\r\n selection: MatcherSelection,\r\n replaceModelWith: (model: GestureModel) => void\r\n ) => {\r\n const sourceIds = selection.matcher.allSourceIds;\r\n\r\n // If there's an active gesture that uses a source noted in the selection, it's the responsibility\r\n // of an existing GestureSequence to handle this one. The handler should bypass it for this round.\r\n if(this.activeGestures.find((sequence) => {\r\n return sequence.allSourceIds.find((a) => sourceIds.indexOf(a) != -1);\r\n })) {\r\n return;\r\n }\r\n\r\n if(selection.result.action.type == 'replace') {\r\n replaceModelWith(getGestureModel(this.gestureModelDefinitions, selection.result.action.replace));\r\n } else {\r\n throw new Error(\"Missed a case in implementation!\");\r\n }\r\n };\r\n\r\n public pushSelector(selector: MatcherSelector) {\r\n this.selectorStack.push(selector);\r\n selector.on('rejectionwithaction', this.modelResetHandler);\r\n }\r\n\r\n public sustainSelectorSubstack(selector: MatcherSelector) {\r\n if(!selector) {\r\n return [];\r\n }\r\n\r\n // If it's already been popped, just silently return.\r\n const index = this.selectorStack.indexOf(selector);\r\n if(index == -1) {\r\n return [];\r\n }\r\n\r\n /* c8 ignore start */\r\n if(this.selectorStack.length <= 1) {\r\n throw new Error(\"May not force the original, base gesture selector into sustain mode.\");\r\n }\r\n /* c8 ignore end */\r\n\r\n let sustainedSources: GestureSource[] = [];\r\n\r\n for(let i = index; i < this.selectorStack.length; i++) {\r\n selector = this.selectorStack[i];\r\n\r\n // If there are any models active with the `sustainWhenNested` property,\r\n // the following Promise resolves once those are also completed.\r\n sustainedSources = sustainedSources.concat(selector.cascadeTermination());\r\n }\r\n\r\n return sustainedSources;\r\n }\r\n\r\n public popSelector(selector: MatcherSelector) {\r\n if(!selector) {\r\n return;\r\n }\r\n\r\n // If it's already been popped, just silently return.\r\n const index = this.selectorStack.indexOf(selector);\r\n if(index == -1) {\r\n return;\r\n }\r\n\r\n /* c8 ignore start */\r\n if(this.selectorStack.length <= 1) {\r\n throw new Error(\"May not pop the original, base gesture selector.\");\r\n }\r\n /* c8 ignore end */\r\n\r\n while(index < this.selectorStack.length) {\r\n selector = this.selectorStack[index];\r\n selector.off('rejectionwithaction', this.modelResetHandler);\r\n\r\n this.selectorStack.splice(index, 1);\r\n }\r\n\r\n // Should be fine as-is for now b/c modipress is always a base-selector gesture and is\r\n // the only thing modifying stateToken within KeymanWeb. May need an async/await in\r\n // the future if other things become able to manipulate state tokens with this engine.\r\n\r\n // Make sure the current state token is set at this stage.\r\n this.currentSelector.stateToken = this.stateToken;\r\n }\r\n\r\n public selectorStackIncludes(selector: MatcherSelector): boolean {\r\n return this.selectorStack.includes(selector);\r\n }\r\n\r\n public get currentSelector() {\r\n return this.selectorStack[this.selectorStack.length-1];\r\n }\r\n\r\n private buildGestureMatchInspector(selector: MatcherSelector) {\r\n return (source: GestureSource) => {\r\n // Get the selectors at the time of the call, not at the time of the functor's construction.\r\n const selectorIndex = this.selectorStack.indexOf(selector);\r\n const selectors = this.selectorStack.slice(selectorIndex);\r\n\r\n return selectors.map((selector) => selector.potentialMatchersForSource(source).map((matcher) => matcher.model.id))\r\n .reduce((flattened, entry) => flattened.concat(entry));\r\n };\r\n }\r\n\r\n protected addEngine(engine: InputEngineBase) {\r\n engine.on('pointstart', this.onNewTrackedPath);\r\n this.inputEngines.push(engine);\r\n }\r\n\r\n private recordHistory(gesture: typeof this._history[0]) {\r\n const histMax = this.historyMax;\r\n if(histMax > 0) {\r\n if(this._history.length == histMax) {\r\n this._history.shift();\r\n }\r\n this._history.push(gesture);\r\n }\r\n }\r\n\r\n private readonly onNewTrackedPath = async (touchpoint: GestureSource) => {\r\n this.addSimpleSourceHooks(touchpoint);\r\n const modelDefs = this.gestureModelDefinitions;\r\n\r\n let potentialSelector: MatcherSelector;\r\n let selectionPromise: Promise>;\r\n do {\r\n potentialSelector = this.currentSelector;\r\n\r\n /* We wait for the source to fully pass through the gesture-model spin-up phase; there's\r\n * a chance that the new source will complete an existing gesture instantly without being\r\n * locked to it, resulting in activation of a different `stateToken`.\r\n *\r\n * This, in turn, can affect what the initial 'item' for the new gesture will be.\r\n */\r\n const modelingSpinupPromise = potentialSelector.matchGesture(touchpoint, getGestureModelSet(modelDefs, potentialSelector.baseGestureSetId));\r\n const modelingSpinupResults = await modelingSpinupPromise;\r\n\r\n if(modelingSpinupResults.sustainModeWithoutMatch) {\r\n\r\n const correctSample = (sample: InputSample) => {\r\n sample.stateToken = this.stateToken;\r\n sample.item = touchpoint.currentRecognizerConfig.itemIdentifier(sample, null);\r\n };\r\n\r\n /* May need to do a state-token change check & update the item; an `awaitNested` 'complete' action\r\n * may have been pending in the meantime that could have triggered a change.\r\n *\r\n * (The MatcherSelector's state token will not have been updated b/c it will have already been popped,\r\n * and because it's popped, it should not be responsible for managing the new GestureSource -\r\n * including shifts in state token.)\r\n *\r\n * Current actual use-case: deferred modipress due to ongoing flick, auto-completed by new incoming touch.\r\n */\r\n if(touchpoint.path instanceof GestureDebugPath) {\r\n touchpoint.path.coords.forEach(correctSample);\r\n }\r\n\r\n // Don't forget to also correct the `stateToken` and `baseItem`!\r\n touchpoint.stateToken = this.stateToken;\r\n touchpoint.baseItem = touchpoint.path.stats.initialSample.item;\r\n\r\n // Also, in case a contact model's path-eval references data via stats...\r\n correctSample(touchpoint.path.stats.initialSample);\r\n correctSample(touchpoint.path.stats.lastSample);\r\n continue;\r\n } else {\r\n selectionPromise = modelingSpinupResults.selectionPromise;\r\n break;\r\n }\r\n\r\n // Can only happen if a `sustainWhenNested` model state is resolved, nested within\r\n // a gesture whose completion action requests `awaitNested`.\r\n } while(potentialSelector != this.currentSelector);\r\n\r\n const selector = this.currentSelector;\r\n\r\n touchpoint.setGestureMatchInspector(this.buildGestureMatchInspector(selector));\r\n\r\n const preGestureScribe = () => {\r\n this.recordHistory(touchpoint);\r\n }\r\n\r\n /*\r\n If there's an error in code receiving this event, we must not let that break the flow of\r\n event input processing - we may still have a locking Promise corresponding to our active\r\n GestureSource. (See: next comment)\r\n */\r\n try {\r\n touchpoint.path.on('invalidated', preGestureScribe);\r\n this.emit('inputstart', touchpoint);\r\n } catch (err) {\r\n reportError(\"Error from 'inputstart' event listener\", err);\r\n }\r\n\r\n /*\r\n If an `InputEventEngine` internally utilizes the `AsyncClosureDispatchQueue`, this is the point\r\n at which we are now safe to process further events. The correct 'stateToken' has been identified\r\n and all GestureMatcher possibilities for the source have been launched; path updates may resume _and_\r\n new incoming paths may now be safely handled. As such, we can now fulfill any Promise returned by\r\n a closure defined within its `inputStart` method for the `GestureSource` under consideration.\r\n\r\n It is quite important that we _do_ fulfill the `Promise` if it exists - further event processing will\r\n be blocked for such engines until this is done! (Hence the try-catch above)\r\n */\r\n this.inputEngines.forEach((engine) => {\r\n engine.fulfillInputStart(touchpoint);\r\n });\r\n\r\n // ----------------------------------------\r\n\r\n // All gesture-matching is prepared; now we await the source's first gesture model match.\r\n const selection = await selectionPromise;\r\n\r\n // Any related 'push' mechanics that may still be lingering are currently handled by GestureSequence\r\n // during its 'completion' processing. (See `GestureSequence.selectionHandler`.)\r\n if(!selection || selection.result.matched == false) {\r\n return;\r\n }\r\n\r\n // For multitouch gestures, only report the gesture **once**.\r\n const sourceIDs = selection.matcher.allSourceIds;\r\n for(let sequence of this._activeGestures) {\r\n if(!!sequence.allSourceIds.find((id1) => !!sourceIDs.find((id2) => id1 == id2))) {\r\n // We've already established (and thus, already reported) a GestureSequence for this selection.\r\n return;\r\n }\r\n }\r\n\r\n const gestureSequence = new GestureSequence(selection, modelDefs, this.currentSelector, this);\r\n this._activeGestures.push(gestureSequence);\r\n gestureSequence.on('complete', () => {\r\n // When the GestureSequence is fully complete and all related `firstSelectionPromise`s have\r\n // had the chance to resolve, drop the reference; prevent memory leakage.\r\n const index = this._activeGestures.indexOf(gestureSequence);\r\n if(index != -1) {\r\n this._activeGestures.splice(index, 1);\r\n }\r\n });\r\n\r\n // Could track sequences easily enough; the question is how to tell when to 'let go'.\r\n\r\n // No try-catch because only there's no critical code after it.\r\n if(!touchpoint.path.wasCancelled) {\r\n touchpoint.path.off('invalidated', preGestureScribe);\r\n gestureSequence.on('complete', () => this.recordHistory(gestureSequence));\r\n }\r\n this.emit('recognizedgesture', gestureSequence);\r\n }\r\n\r\n public get activeGestures(): GestureSequence[] {\r\n return [].concat(this._activeGestures);\r\n }\r\n\r\n public get activeSources(): GestureSource[] {\r\n return [].concat(this.inputEngines.map((engine) => engine.activeSources).reduce((merged, arr) => merged.concat(arr), []));\r\n }\r\n\r\n public get history() {\r\n return this._history;\r\n }\r\n\r\n public get historyJSON() {\r\n const sanitizingReplacer = function (key: string, value: any) {\r\n if(key == 'item') {\r\n // KMW 'key' elements involve circular refs.\r\n // Just return the key ID. (Assumes use in KMW)\r\n return value?.id;\r\n } else {\r\n return value;\r\n }\r\n }\r\n\r\n return JSON.stringify(this.history, sanitizingReplacer, 2);\r\n }\r\n\r\n /**\r\n * The current 'state token' to be set for newly-starting gestures for use by gesture-recognizer\r\n * consumers, their item-identifier lookup functions, and their gesture model definitions.\r\n *\r\n * Use of this feature is intended to be strictly optional and only used in scenarios where\r\n * the recognizer's consumer needs some sort of system-state to be associated with ongoing gestures.\r\n */\r\n public get stateToken(): StateToken {\r\n return this._stateToken;\r\n }\r\n\r\n public set stateToken(token: StateToken) {\r\n this._stateToken = token;\r\n this.inputEngines.forEach((engine) => engine.stateToken = token);\r\n this.currentSelector.stateToken = token;\r\n }\r\n\r\n private addSimpleSourceHooks(touchpoint: GestureSource) {\r\n\r\n touchpoint.path.on('invalidated', () => {\r\n // GestureSequence _should_ handle any other cleanup internally as fallout\r\n // from the path being cancelled.\r\n //\r\n // That said, it's handled asynchronously... but we can give a synchronous signal\r\n // through the next block of code, allowing cleanup to occur earlier during\r\n // recovery states.\r\n\r\n const owningSequence = this.activeGestures.find((entry) => entry.allSourceIds.includes(touchpoint.identifier));\r\n if(owningSequence) {\r\n owningSequence.cancel();\r\n }\r\n\r\n // To consider: should it specially mark if it 'completed' due to cancellation,\r\n // or is that safe to infer from the tracked GestureSource(s)?\r\n // Currently, we're going with the latter.\r\n\r\n // Also mark the touchpoint as no longer active.\r\n let i = this._activeSources.indexOf(touchpoint);\r\n this._activeSources = this._activeSources.splice(i, 1);\r\n });\r\n touchpoint.path.on('complete', () => {\r\n // Also mark the touchpoint as no longer active.\r\n let i = this._activeSources.indexOf(touchpoint);\r\n this._activeSources = this._activeSources.splice(i, 1);\r\n });\r\n }\r\n}", + "export * from './contactModel.js';\r\nexport * from './gestureModel.js';\r\nexport * from './gestureModelDefs.js';\r\n// Do NOT export from modelDefValidator here. That's top-level only,\r\n// making it far easier to tree-shake.\r\nexport * from './pathModel.js';", + "import { GestureRecognizerConfiguration, preprocessRecognizerConfig } from \"./configuration/gestureRecognizerConfiguration.js\";\r\nimport { MouseEventEngine } from \"./mouseEventEngine.js\";\r\nimport { Nonoptional } from \"./nonoptional.js\";\r\nimport { TouchEventEngine } from \"./touchEventEngine.js\";\r\nimport { TouchpointCoordinator } from \"./headless/touchpointCoordinator.js\";\r\nimport { EMPTY_GESTURE_DEFS, GestureModelDefs } from \"./headless/gestures/specs/index.js\";\r\n\r\nexport class GestureRecognizer extends TouchpointCoordinator {\r\n public readonly config: Nonoptional>;\r\n\r\n private readonly mouseEngine: MouseEventEngine;\r\n private readonly touchEngine: TouchEventEngine;\r\n\r\n public constructor(gestureModelDefinitions: GestureModelDefs, config: GestureRecognizerConfiguration) {\r\n const preprocessedConfig = preprocessRecognizerConfig(config);\r\n\r\n // Possibly just a stop-gap measure... but this provides an empty gesture-spec set definition\r\n // that allows testing the path-constrainment functionality without invoking gesture-processing\r\n // overhead.\r\n gestureModelDefinitions = gestureModelDefinitions || EMPTY_GESTURE_DEFS;\r\n\r\n super(gestureModelDefinitions, null, preprocessedConfig.historyLength);\r\n this.config = preprocessedConfig;\r\n\r\n this.mouseEngine = new MouseEventEngine(this.config);\r\n this.touchEngine = new TouchEventEngine(this.config);\r\n\r\n this.mouseEngine.registerEventHandlers();\r\n this.touchEngine.registerEventHandlers();\r\n\r\n this.addEngine(this.mouseEngine);\r\n this.addEngine(this.touchEngine);\r\n }\r\n\r\n public destroy() {\r\n // When shutting down the gesture engine, we should go ahead and clear out all related\r\n // gesture-source tracking.\r\n this.activeGestures.forEach((sequence) => sequence.cancel());\r\n this.activeSources.forEach((source) => source.terminate(true));\r\n\r\n this.mouseEngine.unregisterEventHandlers();\r\n this.touchEngine.unregisterEventHandlers();\r\n\r\n // Because these two fields are marked readonly, we can't directly delete them.\r\n // Because they're private, we can't apply Mutable to make them deletable.\r\n // So... awkward cast + assignment it is.\r\n (this.mouseEngine as any) = null;\r\n (this.touchEngine as any) = null;\r\n }\r\n}", + "export * as specs from \"./specs/index.js\";\r\nexport * as matchers from \"./matchers/index.js\";", + "export { GestureMatcher } from './gestureMatcher.js';\r\nexport { GestureSequence, GestureStageReport, modelSetForAction } from './gestureSequence.js';\r\nexport { MatcherSelection, MatcherSelector } from './matcherSelector.js';\r\nexport { PathMatcher } from './pathMatcher.js';", + "import { ActiveKeyBase, KeyDistribution } from \"keyman/engine/keyboard\";\r\nimport { CorrectionLayout } from \"./correctionLayout.js\";\r\n\r\n/**\r\n * Computes a squared 'pseudo-distance' for the touch from each key. (Not a proper metric.)\r\n * Intended for use in generating a probability distribution over the keys based on the touch input.\r\n * @param touchCoords A proportional (x, y) coordinate of the touch within the keyboard's geometry.\r\n * Should be within <0, 0> to <1, 1>.\r\n * @param correctiveLayout The corrective-layout mappings for keys under consideration\r\n * by a correction algorithm, also within <0, 0> to <1, 1>.\r\n * @returns A mapping of key IDs to the 'squared pseudo-distance' of the touchpoint to each key.\r\n */\r\nexport function keyTouchDistances(touchCoords: {x: number, y: number}, correctiveLayout: CorrectionLayout): Map {\r\n let keyDists: Map = new Map();\r\n\r\n // This loop computes a pseudo-distance for the touch from each key. Quite useful for\r\n // generating a probability distribution.\r\n correctiveLayout.keys.forEach((entry) => {\r\n // These represent the within-key distance of the touch from the key's center.\r\n // Both should be on the interval [0, 0.5].\r\n let dx = Math.abs(touchCoords.x - entry.centerX);\r\n let dy = Math.abs(touchCoords.y - entry.centerY);\r\n\r\n // If the touch isn't within the key, these store the out-of-key distance\r\n // from the closest point on the key being checked.\r\n let distX: number, distY: number;\r\n\r\n if(dx > 0.5 * entry.width) {\r\n distX = (dx - 0.5 * entry.width);\r\n dx = 0.5;\r\n } else {\r\n distX = 0;\r\n dx /= entry.width;\r\n }\r\n\r\n if(dy > 0.5 * entry.height) {\r\n distY = (dy - 0.5 * entry.height);\r\n dy = 0.5;\r\n } else {\r\n distY = 0;\r\n dy /= entry.height;\r\n }\r\n\r\n // Now that the differentials are computed, it's time to do distance scaling.\r\n //\r\n // For out-of-key distance, we scale the X component by the keyboard's aspect ratio\r\n // to get the actual out-of-key distance rather than proportional.\r\n distX *= correctiveLayout.kbdScaleRatio;\r\n\r\n // While the keys are rarely perfect squares, we map all within-key distance\r\n // to a square shape. (ALT/CMD should seem as close to SPACE as a 'B'.)\r\n //\r\n // For that square, we take the rowHeight as its edge lengths.\r\n distX += dx * entry.height;\r\n distY += dy * entry.height;\r\n\r\n const distance = distX * distX + distY * distY;\r\n keyDists.set(entry.keySpec, distance);\r\n });\r\n\r\n return keyDists;\r\n}\r\n\r\n/**\r\n * @param squaredDistMap A map of key-id to the squared distance of the original touch from each key under\r\n * consideration.\r\n * @returns\r\n */\r\nexport function distributionFromDistanceMaps(squaredDistMaps: Map | Map[]): KeyDistribution {\r\n const keyProbs = new Map();\r\n let totalMass = 0;\r\n\r\n if(!Array.isArray(squaredDistMaps)) {\r\n squaredDistMaps = [squaredDistMaps];\r\n }\r\n\r\n for(let squaredDistMap of squaredDistMaps) {\r\n // Should we wish to allow multiple different transforms for distance -> probability, use a function parameter in place\r\n // of the formula in the loop below.\r\n for(let key of squaredDistMap.keys()) {\r\n // We've found that in practice, dist^-4 seems to work pretty well. (Our input has dist^2.)\r\n // (Note: our rule of thumb here has only been tested for layout-based distances.)\r\n //\r\n // The 3e-5 fudge-factor may seem a bit high, but it has two purposes:\r\n // 1. Prevent div-by-0 errors\r\n // 2. Ensures that the main key's probability doesn't get SO high that we don't\r\n // consider correcting to immediate neighbors, even if perfectly accurate.\r\n const entry = 1 / (Math.pow(squaredDistMap.get(key), 2) + 3e-5);\r\n totalMass += entry;\r\n\r\n // In case of duplicate key IDs; this can occur if multiple sets are specified.\r\n keyProbs.set(key, keyProbs.get(key) ?? 0 + entry);\r\n }\r\n }\r\n\r\n const list: {keySpec: ActiveKeyBase, p: number}[] = [];\r\n\r\n for(let key of keyProbs.keys()) {\r\n list.push({keySpec: key, p: keyProbs.get(key) / totalMass});\r\n }\r\n\r\n return list.sort(function(a, b) {\r\n return b.p - a.p; // Largest probability keys should be listed first.\r\n });\r\n}\r\n", + "import { type KeyElement } from '../../../keyElement.js';\r\nimport VisualKeyboard from '../../../visualKeyboard.js';\r\n\r\nimport { ActiveKey, ActiveKeyBase, ActiveSubKey, KeyDistribution, KeyEvent } from 'keyman/engine/keyboard';\r\nimport { ConfigChangeClosure, CumulativePathStats, GestureRecognizerConfiguration, GestureSequence, GestureSource, GestureSourceSubview, InputSample, RecognitionZoneSource } from '@keymanapp/gesture-recognizer';\r\nimport { GestureHandler } from '../gestureHandler.js';\r\nimport { distributionFromDistanceMaps } from '../../../corrections.js';\r\nimport { GestureParams } from '../specsForLayout.js';\r\nimport { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js';\r\nimport { TouchLayout } from '@keymanapp/common-types';\r\n\r\nexport const OrderedFlickDirections = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const;\r\n\r\nconst PI = Math.PI;\r\n\r\nexport const FlickNameCoordMap = (() => {\r\n const map = new Map();\r\n\r\n const angleIncrement = PI / 4;\r\n for(let i = 0; i < OrderedFlickDirections.length; i++) {\r\n map.set(OrderedFlickDirections[i], [angleIncrement * i, 1]);\r\n }\r\n\r\n return map;\r\n})();\r\n\r\nexport function lockedAngleForDir(lockedDir: typeof OrderedFlickDirections[number]) {\r\n return Math.PI / 4 * OrderedFlickDirections.indexOf(lockedDir);\r\n}\r\n\r\nexport function calcLockedDistance(pathStats: CumulativePathStats, lockedDir: typeof OrderedFlickDirections[number]) {\r\n const lockedAngle = lockedAngleForDir(lockedDir);\r\n\r\n const rootCoord = pathStats.initialSample;\r\n const deltaX = pathStats.lastSample.targetX - rootCoord.targetX;\r\n const deltaY = pathStats.lastSample.targetY - rootCoord.targetY;\r\n\r\n const projY = Math.max(0, -deltaY * Math.cos(lockedAngle));\r\n const projX = Math.max(0, deltaX * Math.sin(lockedAngle));\r\n\r\n // For intercardinals, note that Math.cos and Math.sin essentially result in component factors of sqrt(2);\r\n // essentially, we've already taken the sqrt of distance.\r\n return projX + projY;\r\n}\r\n\r\nexport function buildFlickScroller(\r\n source: GestureSource,\r\n lockedDir: typeof OrderedFlickDirections[number],\r\n previewHost: GesturePreviewHost,\r\n gestureParams: GestureParams\r\n): (coord: InputSample) => void {\r\n return (coord: InputSample) => {\r\n const lockedAngle = lockedAngleForDir(lockedDir);\r\n\r\n const maxProgressDist = gestureParams.flick.triggerDist - gestureParams.flick.dirLockDist;\r\n let progressDist = Math.max(0, calcLockedDistance(source.path.stats, lockedDir) - gestureParams.flick.dirLockDist);\r\n\r\n // Make progress appear slightly less than it really is; 'near complete' slides thus actually are, so\r\n // the user doesn't get aggrevated by 'near misses' in that regard.\r\n //\r\n // With 0.7, a flick-slide that looks 70% complete will actually be 100% complete.\r\n // This is about when the flick's key-cap preview starts becoming \"mostly visible\".\r\n const FUDGE_SCALE_FACTOR = 0.7;\r\n // Prevent overshoot\r\n let slidePc = Math.min(1, FUDGE_SCALE_FACTOR * progressDist / maxProgressDist);\r\n\r\n const previewX = Math.sin(lockedAngle) * slidePc;\r\n const previewY = -Math.cos(lockedAngle) * slidePc;\r\n\r\n previewHost?.scrollFlickPreview(previewX, previewY);\r\n }\r\n}\r\n\r\n/**\r\n * The maximum angle-difference, in radians, allowed before a potential flick\r\n * is to be considered less likely than its base key.\r\n *\r\n * A 60 degree tolerance (Math.PI / 3) + a 'n' flick will consider most angles\r\n * north of the x-axis more likely than the base key - thus including\r\n * 'nw' and 'ne' and some 'w' and 'e' paths.\r\n */\r\nexport const MAX_TOLERANCE_ANGLE_SKEW = Math.PI / 3;\r\n\r\n/**\r\n * Represents a flick gesture's implementation within KeymanWeb, including\r\n * its predictive-text correction aspects.\r\n */\r\nexport default class Flick implements GestureHandler {\r\n readonly directlyEmitsKeys = true;\r\n\r\n private readonly sequence: GestureSequence;\r\n private readonly gestureParams: GestureParams;\r\n\r\n private readonly baseSpec: ActiveKey;\r\n readonly hasModalVisualization: false;\r\n\r\n private baseKeyDistances: Map;\r\n private computedFlickDistribution: KeyDistribution;\r\n private lockedDir: typeof OrderedFlickDirections[number];\r\n private lockedSelectable: ActiveSubKey;\r\n private flickScroller: (coord: InputSample) => void;\r\n\r\n constructor(\r\n sequence: GestureSequence,\r\n configChanger: ConfigChangeClosure,\r\n vkbd: VisualKeyboard,\r\n e: KeyElement,\r\n gestureParams: GestureParams,\r\n previewHost: GesturePreviewHost\r\n ) {\r\n this.sequence = sequence;\r\n this.gestureParams = gestureParams;\r\n this.baseSpec = e.key.spec as ActiveKey;\r\n\r\n // May be worth a temporary alt config: global roaming, rather than auto-canceling.\r\n\r\n this.baseKeyDistances = vkbd.getSimpleTapCorrectionDistances(sequence.stageReports[0].sources[0].path.stats.initialSample, this.baseSpec)\r\n const baseSource = sequence.stageReports[0].sources[0].baseSource;\r\n let source: GestureSource = baseSource;\r\n\r\n sequence.on('complete', () => {\r\n previewHost?.cancel()\r\n });\r\n\r\n this.sequence.on('stage', (result) => {\r\n const pathStats = source.path.stats;\r\n this.computedFlickDistribution = this.flickDistribution(pathStats, true);\r\n\r\n const baseSelection = this.computedFlickDistribution[0].keySpec;\r\n\r\n if(result.matchedId == 'flick-restart') {\r\n // The gesture-engine's already done this, but we need an analogue for it here.\r\n source.path.replaceInitialSample(result.sources[0].path.stats.initialSample);\r\n // Part of the flick-reset process.\r\n return;\r\n } if(result.matchedId == 'flick-reset-centering') {\r\n // Part of the flick-reset process.\r\n source = baseSource.constructSubview(true, true);\r\n return;\r\n } else if(result.matchedId == 'flick-reset-end') {\r\n this.emitKey(vkbd, this.baseSpec, source.path.stats);\r\n return;\r\n } else if(result.matchedId == 'flick-reset') {\r\n // Instant transitions to flick-mid state; entry indicates a lock \"reset\".\r\n // Cancel the flick-viz bit.\r\n if(this.flickScroller) {\r\n this.flickScroller(source.currentSample);\r\n // Clear any previously-set scroller.\r\n source.path.off('step', this.flickScroller);\r\n }\r\n this.lockedDir = null;\r\n this.lockedSelectable = null;\r\n\r\n // Chops off the prior part of the path\r\n if(source instanceof GestureSourceSubview) {\r\n // Clean up the handlers; we're replacing the subview.\r\n source.disconnect();\r\n }\r\n return;\r\n } else if(result.matchedId == 'flick-mid') {\r\n if(baseSelection == this.baseSpec) {\r\n // Do not store a locked direction; the direction we WOULD lock has\r\n // no valid flick available.\r\n return;\r\n }\r\n\r\n const dir = Object.keys(this.baseSpec.flick).find(\r\n (dir) => this.baseSpec.flick[dir as keyof TouchLayout.TouchLayoutFlick] == baseSelection\r\n ) as typeof OrderedFlickDirections[number];\r\n\r\n this.lockedDir = dir;\r\n this.lockedSelectable = baseSelection;\r\n\r\n if(this.flickScroller) {\r\n // Clear any previously-set scroller.\r\n source.path.off('step', this.flickScroller);\r\n }\r\n\r\n this.flickScroller = buildFlickScroller(source, dir, previewHost, this.gestureParams);\r\n this.flickScroller(source.currentSample);\r\n source.path.on('step', this.flickScroller);\r\n\r\n return;\r\n }\r\n\r\n const selection = this.lockedSelectable ?? baseSelection;\r\n this.emitKey(vkbd, selection, pathStats);\r\n });\r\n\r\n // Be sure to extend roaming bounds a bit more than usual for flicks, as they can be quick motions.\r\n const altConfig = this.buildPopupRecognitionConfig(vkbd);\r\n configChanger({\r\n type: 'push',\r\n config: altConfig\r\n });\r\n }\r\n\r\n private emitKey(vkbd: VisualKeyboard, selection: ActiveKeyBase, pathStats: CumulativePathStats) {\r\n let keyEvent: KeyEvent;\r\n const projectedDistance = calcLockedDistance(pathStats, this.lockedDir);\r\n if(projectedDistance > this.gestureParams.flick.triggerDist) {\r\n keyEvent = vkbd.keyEventFromSpec(selection);\r\n } else {\r\n // Even if mid-way between base key and actual key.\r\n keyEvent = vkbd.keyEventFromSpec(this.baseSpec);\r\n }\r\n\r\n keyEvent.keyDistribution = this.currentStageKeyDistribution(this.baseKeyDistances);\r\n\r\n // emit the keystroke\r\n vkbd.raiseKeyEvent(keyEvent, null);\r\n }\r\n\r\n private buildPopupRecognitionConfig(vkbd: VisualKeyboard): GestureRecognizerConfiguration {\r\n const roamBounding: RecognitionZoneSource = {\r\n getBoundingClientRect() {\r\n // We don't want to actually use Number.NEGATIVE_INFINITY or Number.POSITIVE_INFINITY\r\n // because that produces a DOMRect with a few NaN fields, and we don't want _that_.\r\n\r\n // Way larger than any screen resolution should ever be.\r\n const base = Number.MAX_SAFE_INTEGER;\r\n return new DOMRect(-base, -base, 2*base, 2*base);\r\n }\r\n }\r\n\r\n return {\r\n ...vkbd.gestureEngine.config,\r\n maxRoamingBounds: roamBounding,\r\n safeBounds: roamBounding // if embedded, ensure top boundary extends outside the WebView!\r\n }\r\n }\r\n\r\n cancel() {\r\n // Cancel any flick-specific visualization stuff.\r\n }\r\n\r\n /**\r\n * Builds a probability distribution for the likelihood of any key-supported flick\r\n * (or lack thereof) being intended given the path properties specified.\r\n * @param pathStats\r\n * @returns\r\n */\r\n flickDistribution(pathStats: CumulativePathStats, ignoreThreshold?: boolean) {\r\n // NOTE: does not consider flick direction-locking.\r\n const flickSet = this.baseSpec.flick;\r\n\r\n /* Time to compute flick corrections!\r\n *\r\n * The best way to define a \"flick distance\"... the polar coordinate system, which\r\n * uses (angle, dist) instead of (x, y), with dist clamped at the net distance\r\n * threshold. This way, a diagonal flick doesn't have odd effects due to\r\n * \"corner of the square\" positioning if hard-bounding on x & y instead.\r\n *\r\n * The greater the net distance, the less likely that the base key will be selected,\r\n * no matter which flick is actually picked. In the case that only one flick is\r\n * supported, and in the opposite direction from the actual input, both the flick\r\n * and base key will be considered equally likely. (One due to direction, the\r\n * other due to distance.)\r\n *\r\n * We do this even if pred-text is disabled: it's the easiest way to pick a\r\n * 'nearest-neighbor' flick if the direction doesn't fall perfectly within a\r\n * defined bucket. (It lets us 'fudge' the boundaries a bit.)\r\n */\r\n\r\n // Step 1: build the list of supported flicks, including the base key as a fallback.\r\n let keys: {\r\n spec: ActiveKeyBase,\r\n coord: [number, number]\r\n }[] = [{\r\n spec: this.baseSpec,\r\n coord: [NaN, 0]\r\n }];\r\n\r\n keys = keys.concat(Object.keys(flickSet).map((dir) => {\r\n return {\r\n spec: flickSet[dir as typeof OrderedFlickDirections[number]] as ActiveSubKey,\r\n coord: FlickNameCoordMap.get(dir as typeof OrderedFlickDirections[number])\r\n };\r\n }));\r\n\r\n const angle = pathStats.angle;\r\n\r\n // Determine whether or not the flick distance-threshold has been passed...\r\n // and how close it is to being passed if not yet passed.\r\n const TRIGGER_DIST = this.gestureParams.flick.triggerDist;\r\n const baseDist = Math.min(TRIGGER_DIST, ignoreThreshold ? TRIGGER_DIST : pathStats.netDistance);\r\n const distThresholdRatio = baseDist / TRIGGER_DIST;\r\n\r\n let totalMass = 0;\r\n const distribution: KeyDistribution = keys.map((entry) => {\r\n let angleDist = 0;\r\n const coord = entry.coord;\r\n if(!isNaN(coord[0])) {\r\n const angleDelta1 = angle - coord[0];\r\n const angleDelta2 = 2 * PI + coord[0] - angle; // because of angle wrap-around.\r\n\r\n // NOTE: max linear angle dist: PI. (Angles are between 0 and 2*PI.)\r\n angleDist = Math.min(angleDelta1 * angleDelta1, angleDelta2 * angleDelta2);\r\n }\r\n\r\n /*\r\n * Max linear geometric distance: 1. We should weight it for better comparison\r\n * to angleDist.\r\n *\r\n * MAX_TOLERANCE_ANGLE_SKEW is a perfect conversion factor. Being off by a\r\n * dist of 1 then converts into angle-equivalent distance of the skew, making\r\n * it an equal contributor to overall distance.\r\n */\r\n const geoDelta = MAX_TOLERANCE_ANGLE_SKEW * (coord[1] - distThresholdRatio);\r\n\r\n const geoDist = (geoDelta * geoDelta);\r\n const mass = 1 / (angleDist + geoDist + 1e-6); // prevent div-by-zero\r\n totalMass += mass;\r\n\r\n return {\r\n keySpec: entry.spec,\r\n p: mass\r\n }\r\n });\r\n\r\n const normalizer = 1.0 / totalMass;\r\n distribution.forEach((entry) => entry.p *= normalizer);\r\n\r\n // Sort in descending probability order.\r\n return distribution.sort((a, b) => b.p - a.p);\r\n }\r\n\r\n currentStageKeyDistribution(baseDistMap: Map): KeyDistribution {\r\n const baseSpec = this.baseSpec;\r\n const baseDistances = this.baseKeyDistances;\r\n const flickDistrib = this.computedFlickDistribution;\r\n const entry = baseDistances.get(baseSpec);\r\n\r\n if(!entry) {\r\n const best = flickDistrib[0];\r\n return [\r\n {\r\n keySpec: best.keySpec,\r\n p: 1\r\n }\r\n ];\r\n }\r\n\r\n // Corrections are enabled: return a full distribution\r\n const baseKeyFlickProbIndex = flickDistrib.findIndex((entry) => entry.keySpec == baseSpec);\r\n // Remove the base-key entry from the flick distribution but save its probability.\r\n // We'll scale the base distribution down so that its sum equals that value, enabling\r\n // us to merge the distributions while preserving normalization.\r\n const baseKeyFlickProb = flickDistrib.splice(baseKeyFlickProbIndex, 1)[0].p;\r\n\r\n const baseDistribution = distributionFromDistanceMaps(baseDistances);\r\n return flickDistrib.concat(baseDistribution.map((entry) => {\r\n return {\r\n keySpec: entry.keySpec,\r\n // Scale down all base key probabilities by how likely the base key's selection from\r\n // the flick itself is.\r\n p: entry.p * baseKeyFlickProb\r\n }\r\n }));\r\n }\r\n}", + "import {\r\n gestures,\r\n GestureModelDefs,\r\n CumulativePathStats\r\n} from '@keymanapp/gesture-recognizer';\r\n\r\nimport {\r\n TouchLayout\r\n} from '@keymanapp/common-types';\r\nimport ButtonClasses = TouchLayout.TouchLayoutKeySp;\r\n\r\nimport {\r\n ActiveLayout,\r\n deepCopy\r\n} from 'keyman/engine/keyboard';\r\n\r\nimport { type KeyElement } from '../../keyElement.js';\r\n\r\nimport { calcLockedDistance, lockedAngleForDir, MAX_TOLERANCE_ANGLE_SKEW, type OrderedFlickDirections } from './browser/flick.js';\r\n\r\nimport specs = gestures.specs;\r\n\r\nexport interface GestureParams {\r\n readonly longpress: {\r\n /**\r\n * The minimum _net_ distance traveled before a longpress flick-shortcut will cancel any\r\n * conflicting flick models.\r\n */\r\n flickDistStart: number,\r\n\r\n /**\r\n * The minimum _net_ distance traveled before a longpress flick-shortcut will trigger.\r\n */\r\n flickDistFinal: number,\r\n\r\n /**\r\n * The maximum amount of raw-distance movement allowed for a longpress before it is\r\n * aborted in favor of roaming touch and/or a timer reset. Only applied when\r\n * roaming touch behaviors are permitted / when flicks are disabled.\r\n *\r\n * This threshold is not applied if the movement meets all criteria to trigger a\r\n * flick-shortcut but the distance traveled.\r\n */\r\n noiseTolerance: number,\r\n\r\n /**\r\n * The duration (in ms) that the base key must be held before the subkey menu will be\r\n * displayed should the up-flick shortcut not be utilized.\r\n */\r\n waitLength: number\r\n },\r\n readonly multitap: {\r\n /**\r\n * The duration (in ms) permitted between taps. Taps with a greater time interval\r\n * between them will be considered separate.\r\n */\r\n waitLength: number;\r\n\r\n /**\r\n * The duration (in ms) permitted for a tap to be held before it will no longer\r\n * be considered part of a multitap.\r\n */\r\n holdLength: number;\r\n },\r\n readonly flick: {\r\n /**\r\n * The minimum _net_ touch-path distance that must be traversed to \"lock in\" on\r\n * a flick gesture. When keys support both longpresses and flicks, this distance\r\n * must be traversed before the longpress timer elapses.\r\n *\r\n * This distance does _not_ trigger an actual flick keystroke; it is intended to\r\n * ensure that paths meeting this criteria have the chance to meet the full\r\n * distance criteria for a flick even if longpresses are also supported on a key.\r\n */\r\n startDist: number,\r\n\r\n /**\r\n * The minimum _net_ touch-path distance that must be traversed for flicks\r\n * to be triggered.\r\n */\r\n triggerDist: number,\r\n\r\n /**\r\n * The minimum _net_ touch-path distance after which the direction will be locked.\r\n *\r\n * Is currently also used as the max radius for valid flick-reset recentering targets.\r\n */\r\n dirLockDist: number\r\n }\r\n}\r\n\r\nexport interface FullGestureParams extends GestureParams {\r\n readonly longpress: GestureParams[\"longpress\"] & {\r\n /**\r\n * Allows enabling or disabling the longpress up-flick shortcut for\r\n * keyboards that do not include any defined flick gestures.\r\n *\r\n * Will be ignored (in favor of `false`) for keyboards that do have defined\r\n * flicks.\r\n *\r\n * Note: this is automatically overwritten during keyboard initialization\r\n * to match the keyboard's properties.\r\n */\r\n permitsFlick: (item?: Item) => boolean\r\n },\r\n /**\r\n * Indicates whether roaming-touch oriented behaviors should be enabled.\r\n */\r\n roamingEnabled?: boolean;\r\n}\r\n\r\nexport const DEFAULT_GESTURE_PARAMS: GestureParams = {\r\n longpress: {\r\n // Note: actual runtime value is determined at runtime based upon row height.\r\n // See `VisualKeyboard.refreshLayout`, CTRL-F \"Step 3\".\r\n flickDistStart: 8,\r\n flickDistFinal: 40,\r\n waitLength: 500,\r\n noiseTolerance: 10\r\n },\r\n multitap: {\r\n waitLength: 300,\r\n holdLength: 150\r\n },\r\n // Note: all actual runtime values are determined at runtime based upon row height.\r\n // See `VisualKeyboard.refreshLayout`, CTRL-F \"Step 3\".\r\n flick: {\r\n startDist: 10,\r\n dirLockDist: 25,\r\n triggerDist: 40 // should probably be based on row-height?\r\n }\r\n}\r\n\r\n/**\r\n * Gets the centroid (in client coordinates) of a key's element.\r\n *\r\n * Assumes that the key's layer is in the DOM and actively displayed.\r\n * @param key\r\n */\r\nfunction getKeyCentroid(key: KeyElement) {\r\n // We don't layer-shift at present while a flick is active, so it's valid\r\n // for current use-cases. May need extension to closure in something\r\n // to force the layer to be active in the future, though.\r\n const keyRect = key.getBoundingClientRect();\r\n\r\n return {\r\n clientX: keyRect.left + keyRect.width/2,\r\n clientY: keyRect.top + keyRect.height/2\r\n };\r\n}\r\n\r\n// Is kept separate from prior method in case it becomes a closure in the future\r\n// & needs to be passed in as a parameter.\r\nfunction buildDistFromKeyCentroidFunctor(key: KeyElement) {\r\n const keyCentroid = getKeyCentroid(key);\r\n\r\n return (a: CumulativePathStats) => {\r\n const dx = a.lastSample.clientX - keyCentroid.clientX;\r\n const dy = a.lastSample.clientY - keyCentroid.clientY;\r\n return Math.sqrt(dx*dx + dy*dy);\r\n }\r\n}\r\n\r\nexport function keySupportsModipress(key: KeyElement) {\r\n const keySpec = key.key.spec;\r\n\r\n // A key cannot reasonably support both longpresses and modipresses.\r\n // It'd be quite ugly to overlay the subkey menu over the new layer during a modipress.\r\n if(keySpec.sk) {\r\n return false;\r\n }\r\n\r\n const modifierKeyIds = ['K_SHIFT', 'K_ALT', 'K_CTRL', 'K_NUMERALS', 'K_SYMBOLS', 'K_CURRENCIES'];\r\n for(const modKeyId of modifierKeyIds) {\r\n\r\n if(keySpec.id == modKeyId) {\r\n return true;\r\n }\r\n }\r\n\r\n // Allows special-formatted keys with a next-layer property to be modipressable.\r\n if(!keySpec.nextlayer) {\r\n return false;\r\n } else {\r\n switch(keySpec.sp) {\r\n case ButtonClasses.special:\r\n case ButtonClasses.specialActive:\r\n case ButtonClasses.customSpecial:\r\n case ButtonClasses.customSpecialActive:\r\n return true;\r\n default: // .normal, .spacer, .blank, .deadkey\r\n return false;\r\n }\r\n }\r\n}\r\n\r\ninterface LayoutGestureSupportFlags {\r\n hasFlicks: boolean,\r\n hasMultitaps: boolean,\r\n hasLongpresses: boolean\r\n}\r\n\r\n// Simple compile-time validation that OSKLayerGroup's spec object provides the fields expected above.\r\nlet dummy: ActiveLayout;\r\n// @ts-ignore // so that we don't trigger \"unused local\" warnings.\r\nlet dummy2: LayoutGestureSupportFlags = dummy;\r\n\r\n/**\r\n * Defines the set of gestures appropriate for use with the specified Keyman\r\n * keyboard.\r\n * @param layerGroup The active keyboard's layer group\r\n * @param paramObj A set of tweakable gesture parameters. It will be\r\n * closure-captured and referred to by reference; changes to\r\n * its values will take immediate effect during gesture\r\n * processing.\r\n * @returns\r\n */\r\nexport function gestureSetForLayout(flags: LayoutGestureSupportFlags, paramObj: GestureParams): GestureModelDefs {\r\n // To be used among the `allowsInitialState` contact-model specifications as needed.\r\n const gestureKeyFilter = (key: KeyElement, gestureId: string) => {\r\n if(!key) {\r\n return false;\r\n }\r\n\r\n const keySpec = key.key.spec;\r\n switch(gestureId) {\r\n case 'modipress-start':\r\n return keySupportsModipress(key);\r\n case 'special-key-start':\r\n return ['K_LOPT', 'K_ROPT', 'K_BKSP'].indexOf(keySpec.baseKeyID) != -1;\r\n case 'longpress':\r\n // Always allow longpresses to start; we validate them at timer-end.\r\n // This facilitates roaming+longpress interactions.\r\n return true;\r\n case 'multitap-start':\r\n case 'modipress-multitap-start':\r\n if(flags.hasMultitaps) {\r\n return !!keySpec.multitap;\r\n } else {\r\n return false;\r\n }\r\n case 'flick-start':\r\n // This is a gesture-start check; there won't yet be any directional info available.\r\n return !!keySpec.flick;\r\n default:\r\n return true;\r\n }\r\n };\r\n\r\n const params = paramObj as FullGestureParams;\r\n\r\n // Override any prior entries for keyboard-specific configuration.\r\n params.longpress.permitsFlick = (key) => {\r\n const flickSpec = key?.key.spec.flick;\r\n return !flickSpec || !(flickSpec.n || flickSpec.nw || flickSpec.ne);\r\n };\r\n const doRoaming = params.roamingEnabled = !flags.hasFlicks;\r\n\r\n const _initialTapModel: GestureModel = deepCopy(!doRoaming ? initialTapModel(params) : initialTapModelWithReset(params));\r\n const _simpleTapModel: GestureModel = deepCopy(!doRoaming ? simpleTapModel(params) : simpleTapModelWithReset(params));\r\n // Ensure all deep-copy operations for longpress modeling occur before the property-redefining block.\r\n const _longpressModel: GestureModel = withKeySpecFiltering(deepCopy(longpressModel(params, true, doRoaming)), 0);\r\n const _multitapStartModel: GestureModel = withKeySpecFiltering(multitapStartModel(params), 0);\r\n const _modipressMultitapStartModel: GestureModel = withKeySpecFiltering(modipressMultitapStartModel(params), 0);\r\n\r\n // `deepCopy` does not preserve property definitions, instead raw-copying its value.\r\n // We need to re-instate the longpress delay property here.\r\n Object.defineProperty(_longpressModel.contacts[0].model.timer, 'duration', {\r\n get: () => params.longpress.waitLength\r\n });\r\n Object.defineProperty(_multitapStartModel.sustainTimer, 'duration', {\r\n get: () => params.multitap.waitLength\r\n });\r\n Object.defineProperty(_modipressMultitapStartModel.sustainTimer, 'duration', {\r\n get: () => params.multitap.waitLength\r\n });\r\n\r\n // #region Functions for implementing and/or extending path initial-state checks\r\n function withKeySpecFiltering(model: GestureModel, contactIndices: number | number[]) {\r\n // Creates deep copies of the model specifications that are safe to customize to the\r\n // keyboard layout.\r\n model = deepCopy(model);\r\n const modelId = model.id;\r\n\r\n if(typeof contactIndices == 'number') {\r\n contactIndices = [contactIndices];\r\n }\r\n\r\n model.contacts.forEach((contact, index) => {\r\n if((contactIndices as number[]).indexOf(index) != -1) {\r\n const baseInitialStateCheck = contact.model.allowsInitialState ?? (() => true);\r\n\r\n contact.model = {\r\n ...contact.model,\r\n allowsInitialState: (sample, ancestorSample, key) => {\r\n return gestureKeyFilter(key, modelId) && baseInitialStateCheck(sample, ancestorSample, key);\r\n }\r\n };\r\n }\r\n });\r\n\r\n return model;\r\n }\r\n // #endregion\r\n\r\n const specialStartModel = specialKeyStartModel();\r\n const _modipressStartModel = modipressStartModel();\r\n const gestureModels: GestureModel[] = [\r\n _longpressModel,\r\n _multitapStartModel,\r\n multitapEndModel(params),\r\n _initialTapModel,\r\n _simpleTapModel,\r\n withKeySpecFiltering(specialStartModel, 0),\r\n specialKeyEndModel(params),\r\n subkeySelectModel(),\r\n withKeySpecFiltering(_modipressStartModel, 0),\r\n modipressHoldModel(params),\r\n modipressEndModel(),\r\n modipressMultitapTransitionModel(),\r\n _modipressMultitapStartModel,\r\n modipressMultitapEndModel(params),\r\n modipressMultitapLockModel()\r\n ];\r\n\r\n const defaultSet = [\r\n _longpressModel.id, _initialTapModel.id, _modipressStartModel.id, specialStartModel.id\r\n ];\r\n\r\n if(!doRoaming) {\r\n gestureModels.push(withKeySpecFiltering(flickStartModel(params), 0));\r\n gestureModels.push(flickMidModel(params));\r\n gestureModels.push(flickResetModel(params));\r\n gestureModels.push(flickResetCenteringModel(params));\r\n gestureModels.push(flickRestartModel(params));\r\n gestureModels.push(flickResetEndModel());\r\n gestureModels.push(flickEndModel(params));\r\n\r\n defaultSet.push('flick-start');\r\n } else {\r\n // A post-roam version of longpress with the up-flick shortcut disabled but roaming still on.\r\n gestureModels.push(withKeySpecFiltering(longpressModelAfterRoaming(params), 0));\r\n // Allows reactivation of longpress-eval when the base key changes if the timer elapses on\r\n // a subkey-less key.\r\n gestureModels.push(longpressRoamRestoration());\r\n }\r\n\r\n return {\r\n gestures: gestureModels,\r\n sets: {\r\n default: defaultSet,\r\n modipress: defaultSet.filter((entry) => entry != _modipressStartModel.id), // no nested modipressing\r\n none: []\r\n }\r\n }\r\n}\r\n\r\n// #region Definition of models for paths comprising gesture-stage models\r\n\r\n// Note: as specified below, none of the raw specs actually need access to KeyElement typing.\r\n\r\ntype ContactModel = specs.ContactModel;\r\n\r\nexport function instantContactRejectionModel(): ContactModel {\r\n return {\r\n itemPriority: 0,\r\n pathResolutionAction: 'reject',\r\n pathModel: {\r\n evaluate: (path) => 'resolve'\r\n }\r\n };\r\n}\r\n\r\nexport function instantContactResolutionModel(): ContactModel {\r\n return {\r\n itemPriority: 0,\r\n pathResolutionAction: 'resolve',\r\n pathModel: {\r\n evaluate: (path) => 'resolve'\r\n }\r\n };\r\n}\r\n\r\nexport function flickStartContactModel(params: FullGestureParams): gestures.specs.ContactModel {\r\n const flickParams = params.flick;\r\n\r\n return {\r\n itemPriority: 1,\r\n pathModel: {\r\n evaluate: (path, _, item) => {\r\n const stats = path.stats;\r\n const keySpec = item?.key.spec;\r\n\r\n if(keySpec && keySpec.sk) {\r\n const flickSpec = keySpec.flick;\r\n const hasUpFlick = flickSpec.nw || flickSpec.n || flickSpec.ne;\r\n\r\n if(!hasUpFlick) {\r\n // Check for possible conflict with the longpress up-flick shortcut;\r\n // it's supported on this key, as there is no true northish flick.\r\n const baseDistance = stats.netDistance;\r\n const angle = stats.angle; // from <0, -1> (straight up) going clockwise.\r\n const verticalDistance = baseDistance * Math.cos(angle);\r\n if(verticalDistance > params.longpress.flickDistStart) {\r\n return 'reject';\r\n }\r\n }\r\n }\r\n\r\n return stats.netDistance > flickParams.startDist ? 'resolve' : null;\r\n }\r\n },\r\n pathResolutionAction: 'resolve',\r\n pathInheritance: 'partial'\r\n }\r\n}\r\n\r\n/*\r\n * Determines the best direction to use for flick-locking and the total net distance\r\n * traveled in that direction.\r\n */\r\nfunction determineLockFromStats(pathStats: CumulativePathStats, baseItem: KeyElement) {\r\n const flickSpec = baseItem.key.spec.flick;\r\n\r\n const supportedDirs = Object.keys(flickSpec) as (typeof OrderedFlickDirections[number])[];\r\n let bestDir: typeof supportedDirs[number];\r\n let bestLockedDist = 0;\r\n\r\n for(const dir of supportedDirs) {\r\n const lockedDist = calcLockedDistance(pathStats, dir);\r\n if(lockedDist > bestLockedDist) {\r\n bestLockedDist = lockedDist;\r\n bestDir = dir;\r\n }\r\n }\r\n\r\n return {\r\n dir: bestDir,\r\n dist: bestLockedDist\r\n }\r\n}\r\n\r\nexport function flickMidContactModel(params: FullGestureParams): gestures.specs.ContactModel {\r\n return {\r\n itemPriority: 1,\r\n pathModel: {\r\n evaluate: (path, priorStats, baseItem) => {\r\n /*\r\n * Check whether or not there is a valid flick for which the path crosses the flick-dist\r\n * threshold while at a supported angle for flick-locking by the flick handler.\r\n */\r\n const { dir, dist } = determineLockFromStats(path.stats, baseItem);\r\n\r\n // If the best supported flick direction meets the 'direction lock' threshold criteria,\r\n // only then do we allow transitioning to the 'locked flick' state.\r\n if(dist > params.flick.dirLockDist) {\r\n const trueAngle = path.stats.angle;\r\n const lockAngle = lockedAngleForDir(dir);\r\n const dist1 = Math.abs(trueAngle - lockAngle);\r\n const dist2 = Math.abs(2 * Math.PI + lockAngle - trueAngle); // because of angle wrap-around.\r\n\r\n if(dist1 <= MAX_TOLERANCE_ANGLE_SKEW || dist2 <= MAX_TOLERANCE_ANGLE_SKEW) {\r\n return 'resolve';\r\n }\r\n } else if(path.isComplete) {\r\n return 'reject';\r\n }\r\n\r\n return undefined;\r\n }\r\n },\r\n pathResolutionAction: 'resolve',\r\n pathInheritance: 'full'\r\n }\r\n}\r\n\r\n\r\nexport function flickEndContactModel(params: FullGestureParams): ContactModel {\r\n return {\r\n itemPriority: 1,\r\n pathModel: {\r\n evaluate: (path, priorStats, baseItem, baseStats) => {\r\n if(path.isComplete) {\r\n // The Flick handler class will sort out the mess once the path is complete.\r\n // Note: if we wanted auto-triggering once the threshold distance were met,\r\n // we'd need to move its related logic into this method.\r\n return 'resolve';\r\n } else {\r\n const { dir } = determineLockFromStats(baseStats, baseItem);\r\n if(calcLockedDistance(path.stats, dir) < params.flick.dirLockDist) {\r\n return 'reject';\r\n }\r\n }\r\n return undefined;\r\n }\r\n },\r\n pathResolutionAction: 'resolve',\r\n pathInheritance: 'full'\r\n }\r\n}\r\n\r\nexport function longpressContactModel(params: FullGestureParams, enabledFlicks: boolean, resetForRoaming: boolean): ContactModel {\r\n const spec = params.longpress;\r\n\r\n return {\r\n itemPriority: 0,\r\n pathResolutionAction: 'resolve',\r\n timer: {\r\n // Needs to be a getter so that it dynamically updates if the backing value is changed.\r\n get duration() { return spec.waitLength },\r\n expectedResult: true\r\n },\r\n validateItem: (_: KeyElement, baseKey: KeyElement) => !!baseKey?.key.spec.sk,\r\n pathModel: {\r\n evaluate: (path) => {\r\n const stats = path.stats;\r\n\r\n /* The flick-dist threshold may be higher than the noise tolerance,\r\n * so we don't check the latter if we're in the right direction for\r\n * the flick shortcut to trigger.\r\n *\r\n * The 'indexOf' allows 'n', 'nw', and 'ne' - approx 67.5 degrees on\r\n * each side of due N in total.\r\n */\r\n if((enabledFlicks && spec.permitsFlick(stats.lastSample.item)) && (stats.cardinalDirection?.indexOf('n') != -1 ?? false)) {\r\n const baseDistance = stats.netDistance;\r\n const angle = stats.angle; // from <0, -1> (straight up) going clockwise.\r\n const verticalDistance = baseDistance * Math.cos(angle);\r\n if(verticalDistance > spec.flickDistFinal) {\r\n return 'resolve';\r\n }\r\n } else if(resetForRoaming) {\r\n // If roaming, reject if the path has moved significantly (so that we restart)\r\n if(stats.rawDistance > spec.noiseTolerance || stats.lastSample.item != stats.initialSample.item) {\r\n return 'reject';\r\n }\r\n } else {\r\n // If not roaming, reject when the base key changes.\r\n if(stats.lastSample.item != stats.initialSample.item) {\r\n return 'reject';\r\n }\r\n }\r\n\r\n if(path.isComplete) {\r\n return 'reject';\r\n }\r\n\r\n return null;\r\n }\r\n }\r\n }\r\n}\r\n\r\nexport function modipressContactStartModel(): ContactModel {\r\n return {\r\n itemPriority: -1,\r\n pathResolutionAction: 'resolve',\r\n pathModel: {\r\n // Consideration of whether the underlying item supports the corresponding\r\n // gesture will be handled elsewhere.\r\n evaluate: (path) => 'resolve'\r\n }\r\n };\r\n}\r\n\r\nexport function modipressContactHoldModel(): ContactModel {\r\n return {\r\n itemPriority: -1,\r\n itemChangeAction: 'resolve',\r\n pathResolutionAction: 'resolve',\r\n pathModel: {\r\n evaluate: (path) => {\r\n if(path.isComplete) {\r\n return 'reject';\r\n }\r\n return undefined;\r\n }\r\n }\r\n }\r\n}\r\n\r\nexport function modipressContactEndModel(): ContactModel {\r\n return {\r\n itemPriority: -1,\r\n itemChangeAction: 'resolve',\r\n pathResolutionAction: 'resolve',\r\n pathModel: {\r\n evaluate: (path) => {\r\n if(path.isComplete) {\r\n return 'resolve';\r\n }\r\n return undefined;\r\n }\r\n }\r\n };\r\n}\r\n\r\nexport function simpleTapContactModel(params: FullGestureParams, isNotInitial?: boolean): ContactModel {\r\n // Snapshot at model construction; do not update if changed.\r\n const roamingEnabled = params?.roamingEnabled ?? true; // ?? true - used by the banner.\r\n\r\n return {\r\n itemPriority: 0,\r\n itemChangeAction: roamingEnabled ? 'reject' : undefined,\r\n pathResolutionAction: 'resolve',\r\n // if roaming, a tap reset should set the base key.\r\n // if not, block path resets.\r\n pathInheritance: (!roamingEnabled && isNotInitial) ? 'full' : 'chop',\r\n pathModel: {\r\n evaluate: (path) => {\r\n if(path.isComplete && !path.wasCancelled) {\r\n return 'resolve';\r\n }\r\n\r\n return undefined;\r\n }\r\n }\r\n };\r\n}\r\n\r\nexport function subkeySelectContactModel(): ContactModel {\r\n return {\r\n itemPriority: 0,\r\n pathResolutionAction: 'resolve',\r\n pathModel: {\r\n evaluate: (path) => {\r\n if(path.isComplete && !path.wasCancelled) {\r\n return 'resolve';\r\n }\r\n return undefined;\r\n }\r\n }\r\n }\r\n}\r\n// #endregion\r\n\r\n// #region Gesture-stage model definitions\r\n\r\n// Note: as specified below, most of the raw specs actually need access to KeyElement typing.\r\n// That only becomes relevant with some of the modifier functions in the `gestureSetForLayout`\r\n// func at the top.\r\ntype GestureModel = specs.GestureModel;\r\n\r\nexport function specialKeyStartModel(): GestureModel {\r\n return {\r\n id: 'special-key-start',\r\n resolutionPriority: 0,\r\n contacts : [\r\n {\r\n model: {\r\n ...instantContactResolutionModel(),\r\n // Filtering is done via `gestureKeyFilter` as defined within `gestureSetForLayout` above.\r\n // If we've gotten to this point, we're already safe to assume the base key is valid.\r\n },\r\n endOnResolve: false // keyboard-selection longpress - would be nice to not need to lift the finger\r\n // in app/browser form.\r\n }\r\n ],\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'special-key-end',\r\n item: 'current'\r\n }\r\n };\r\n}\r\n\r\nexport function specialKeyEndModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'special-key-end',\r\n resolutionPriority: 0,\r\n contacts : [\r\n {\r\n model: {\r\n ...simpleTapContactModel(params),\r\n itemChangeAction: 'resolve'\r\n },\r\n endOnResolve: true,\r\n }\r\n ],\r\n resolutionAction: {\r\n type: 'complete',\r\n item: 'none'\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * The base model for longpresses, with considerable configurability.\r\n *\r\n * @param params The common gesture configuration object for the gesture set under construction.\r\n * @param allowShortcut If `true` and certain conditions are also met, enables an 'up-flick shortcut' to\r\n * bypass the longpress timer.\r\n *\r\n * Conditions:\r\n * - the key has no northish flicks (nw, n, ne)\r\n * - the common gesture configuration permits the shortcut where supported\r\n * @param allowRoaming Indicates whether \"roaming touch\" mode should be supported.\r\n */\r\nexport function longpressModel(params: FullGestureParams, allowShortcut: boolean, allowRoaming: boolean): GestureModel {\r\n const base: GestureModel = {\r\n id: 'longpress',\r\n // Needs to beat flick-start priority.\r\n resolutionPriority: 4,\r\n contacts: [\r\n {\r\n model: {\r\n ...longpressContactModel(params, allowShortcut, allowRoaming),\r\n itemPriority: 1,\r\n pathInheritance: 'chop'\r\n },\r\n endOnResolve: false\r\n }, {\r\n model: instantContactRejectionModel(),\r\n resetOnInstantFulfill: true\r\n }\r\n ],\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'subkey-select',\r\n selectionMode: 'none',\r\n item: 'none'\r\n },\r\n }\r\n\r\n if(allowRoaming) {\r\n return {\r\n ...base,\r\n rejectionActions: {\r\n path: {\r\n type: 'replace',\r\n replace: 'longpress-roam'\r\n },\r\n // The timer can fail if the key doesn't support subkeys.\r\n // If it legit timed out, the gesture can't be continued anyway.\r\n timer: {\r\n type: 'replace',\r\n replace: 'longpress-roam-restore'\r\n }\r\n }\r\n }\r\n } else {\r\n return base;\r\n }\r\n}\r\n\r\n/**\r\n * For use for transitioning out of roaming-touch.\r\n */\r\nexport function longpressModelAfterRoaming(params: FullGestureParams): GestureModel {\r\n // The longpress-shortcut is always disabled for keys reached by roaming (param 2)\r\n // Only used when roaming is permitted; continued roaming should be allowed. (param 3)\r\n const base = longpressModel(params, false, true);\r\n\r\n return {\r\n ...base,\r\n id: 'longpress-roam'\r\n }\r\n}\r\n\r\n// For reactivating longpress processing after changing base key (during roaming),\r\n// should the timer have elapsed on a key not supporting longpresses.\r\nexport function longpressRoamRestoration(): GestureModel {\r\n return {\r\n id: 'longpress-roam-restore',\r\n contacts: [\r\n {\r\n model: {\r\n pathModel: {\r\n evaluate: (path) => {\r\n // pretty much a placeholder.\r\n return null;\r\n }\r\n },\r\n // The actual trigger.\r\n itemChangeAction: 'reject',\r\n pathInheritance: 'full',\r\n pathResolutionAction: 'reject',\r\n itemPriority: 0\r\n }\r\n }\r\n ],\r\n resolutionPriority: -1,\r\n // We rely on THIS path so it doesn't affect longpress logic, which currently expects the initial\r\n // stage to be a successful longpress.\r\n rejectionActions: {\r\n item: {\r\n type: 'replace',\r\n replace: 'longpress-roam'\r\n }\r\n },\r\n // is required by the type.\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'longpress-roam'\r\n }\r\n }\r\n}\r\n\r\nexport function flickStartModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'flick-start',\r\n resolutionPriority: 3,\r\n contacts: [\r\n {\r\n model: flickStartContactModel(params)\r\n }\r\n ],\r\n resolutionAction: {\r\n type: 'chain',\r\n item: 'none',\r\n next: 'flick-mid',\r\n },\r\n }\r\n}\r\n\r\nexport function flickRestartModel(params: FullGestureParams): GestureModel {\r\n const base = flickStartModel(params);\r\n return {\r\n ...base,\r\n contacts: [\r\n {\r\n ...base.contacts[0],\r\n model: {\r\n ...base.contacts[0].model,\r\n baseCoordReplacer: (stats, key) => {\r\n const keyCentroid = getKeyCentroid(key);\r\n const calcDist = buildDistFromKeyCentroidFunctor(key);\r\n\r\n const coord = stats.initialSample;\r\n const distFromCenter = calcDist(stats);\r\n\r\n // If the current coord is far off key and would trigger a flick, just recenter\r\n // and let the intermediate models 'fall through', displaying the new target flick\r\n // if possible.\r\n if(distFromCenter > params.flick.triggerDist) {\r\n return keyCentroid;\r\n }\r\n\r\n const dirLockDist = params.flick.dirLockDist;\r\n\r\n // If we landed within the distance that'd trigger a direction-lock,\r\n // no need to fully recenter; the current coord is \"good enough\".\r\n if(distFromCenter < dirLockDist) {\r\n return coord;\r\n }\r\n\r\n const projectionScalar = dirLockDist / distFromCenter;\r\n\r\n // If the user didn't land close to the key's center, their \"perceived\"\r\n // center for the gesture is likely different than the 'true' center.\r\n const dx = coord.clientX - keyCentroid.clientX;\r\n const dy = coord.clientY - keyCentroid.clientY;\r\n\r\n // Maps the current coord to a coord on the edge of a circle centered\r\n // on the key centroid at a radius of `dirLockDist` away.\r\n return {\r\n clientX: keyCentroid.clientX + dx * projectionScalar,\r\n clientY: keyCentroid.clientY + dy * projectionScalar\r\n };\r\n }\r\n }\r\n }\r\n ],\r\n id: 'flick-restart',\r\n sustainWhenNested: true,\r\n rejectionActions: {\r\n // Only 'rejects' in this form if the path is completed before direction-locking state.\r\n path: {\r\n type: 'replace',\r\n replace: 'flick-reset-end'\r\n }\r\n },\r\n }\r\n}\r\n\r\nexport function flickMidModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'flick-mid',\r\n resolutionPriority: 0,\r\n contacts: [\r\n {\r\n model: flickMidContactModel(params),\r\n endOnReject: true,\r\n }, {\r\n model: instantContactRejectionModel(),\r\n resetOnInstantFulfill: true,\r\n }\r\n ],\r\n rejectionActions: {\r\n // Only 'rejects' in this form if the path is completed before direction-locking state.\r\n path: {\r\n type: 'replace',\r\n replace: 'flick-reset-end'\r\n }\r\n },\r\n resolutionAction: {\r\n type: 'chain',\r\n item: 'none',\r\n next: 'flick-end'\r\n },\r\n sustainWhenNested: true\r\n }\r\n}\r\n\r\n// Clears existing flick-scrolling & primes the flick-reset recentering mechanism.\r\nexport function flickResetModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'flick-reset',\r\n resolutionPriority: 1,\r\n contacts: [\r\n {\r\n model: {\r\n ...instantContactResolutionModel(),\r\n pathInheritance: 'partial', // keep base item, but reset the path-stats.\r\n },\r\n }\r\n ],\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'flick-reset-centering'\r\n },\r\n sustainWhenNested: true\r\n };\r\n}\r\n\r\nexport function flickResetCenteringModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'flick-reset-centering',\r\n resolutionPriority: 1,\r\n contacts: [\r\n {\r\n model: {\r\n pathModel: {\r\n evaluate(path, priorStats, baseItem) {\r\n priorStats ||= path.stats;\r\n\r\n const calcDist = buildDistFromKeyCentroidFunctor(baseItem);\r\n\r\n const newDist = calcDist(path.stats);\r\n const oldDist = calcDist(priorStats);\r\n\r\n if(oldDist < newDist) {\r\n return 'resolve';\r\n }\r\n\r\n return undefined;\r\n },\r\n },\r\n itemPriority: 0,\r\n pathResolutionAction: 'resolve',\r\n pathInheritance: 'full', // no need to re-reset.\r\n },\r\n }\r\n ],\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'flick-restart'\r\n },\r\n sustainWhenNested: true\r\n };\r\n}\r\n\r\nexport function flickResetEndModel(): GestureModel {\r\n return {\r\n id: 'flick-reset-end',\r\n resolutionPriority: 1,\r\n contacts: [],\r\n sustainTimer: {\r\n duration: 0,\r\n expectedResult: true\r\n },\r\n resolutionAction: {\r\n type: 'complete',\r\n item: 'base'\r\n },\r\n sustainWhenNested: true\r\n };\r\n};\r\n\r\nexport function flickEndModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'flick-end',\r\n resolutionPriority: 0,\r\n contacts: [\r\n {\r\n model: flickEndContactModel(params)\r\n },\r\n {\r\n model: instantContactResolutionModel(),\r\n resetOnInstantFulfill: true\r\n }\r\n ],\r\n rejectionActions: {\r\n path: {\r\n type: 'replace',\r\n replace: 'flick-reset'\r\n }\r\n },\r\n resolutionAction: {\r\n type: 'complete',\r\n item: 'current'\r\n },\r\n sustainWhenNested: true\r\n }\r\n}\r\n\r\nexport function multitapStartModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'multitap-start',\r\n resolutionPriority: 2,\r\n contacts: [\r\n {\r\n model: {\r\n ...instantContactResolutionModel(),\r\n itemPriority: 1,\r\n pathInheritance: 'reject',\r\n allowsInitialState(incomingSample, comparisonSample, baseItem) {\r\n return incomingSample.item == baseItem;\r\n },\r\n },\r\n }\r\n ],\r\n sustainTimer: {\r\n duration: params.multitap.waitLength,\r\n expectedResult: false,\r\n baseItem: 'base'\r\n },\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'multitap-end',\r\n item: 'current'\r\n }\r\n }\r\n}\r\n\r\nexport function multitapEndModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'multitap-end',\r\n resolutionPriority: 2,\r\n contacts: [\r\n {\r\n model: {\r\n ...simpleTapContactModel(params),\r\n itemPriority: 1,\r\n timer: {\r\n duration: params.multitap.holdLength,\r\n expectedResult: false\r\n }\r\n },\r\n endOnResolve: true\r\n }, {\r\n model: instantContactResolutionModel(),\r\n resetOnInstantFulfill: true\r\n }\r\n ],\r\n rejectionActions: {\r\n timer: {\r\n type: 'replace',\r\n replace: 'simple-tap'\r\n }\r\n },\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'multitap-start',\r\n item: 'none'\r\n }\r\n }\r\n}\r\n\r\nexport function initialTapModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'initial-tap',\r\n resolutionPriority: 1,\r\n contacts: [\r\n {\r\n model: {\r\n ...simpleTapContactModel(params),\r\n pathInheritance: 'chop',\r\n itemPriority: 1,\r\n timer: {\r\n duration: params.multitap.holdLength,\r\n expectedResult: false\r\n },\r\n },\r\n endOnResolve: true\r\n }, {\r\n model: instantContactResolutionModel(),\r\n resetOnInstantFulfill: true\r\n }\r\n ],\r\n sustainWhenNested: true,\r\n rejectionActions: {\r\n timer: {\r\n type: 'replace',\r\n replace: 'simple-tap'\r\n }\r\n },\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'multitap-start',\r\n item: 'base'\r\n }\r\n }\r\n}\r\n\r\nexport function simpleTapModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'simple-tap',\r\n resolutionPriority: 1,\r\n contacts: [\r\n {\r\n model: {\r\n ...simpleTapContactModel(params, true),\r\n itemPriority: 1\r\n },\r\n endOnResolve: true\r\n }, {\r\n model: instantContactResolutionModel(),\r\n resetOnInstantFulfill: true\r\n }\r\n ],\r\n sustainWhenNested: true,\r\n resolutionAction: {\r\n type: 'complete',\r\n item: 'base'\r\n }\r\n };\r\n}\r\n\r\nexport function initialTapModelWithReset(params: FullGestureParams): GestureModel {\r\n const base = initialTapModel(params);\r\n return {\r\n ...base,\r\n rejectionActions: {\r\n ...base.rejectionActions,\r\n item: {\r\n type: 'replace',\r\n replace: 'initial-tap'\r\n }\r\n }\r\n }\r\n}\r\n\r\nexport function simpleTapModelWithReset(params: FullGestureParams): GestureModel {\r\n const simpleModel = simpleTapModel(params);\r\n return {\r\n ...simpleModel,\r\n rejectionActions: {\r\n ...simpleModel.rejectionActions,\r\n item: {\r\n type: 'replace',\r\n replace: 'simple-tap'\r\n }\r\n }\r\n }\r\n}\r\n\r\nexport function subkeySelectModel(): GestureModel {\r\n return {\r\n id: 'subkey-select',\r\n resolutionPriority: 0,\r\n contacts: [\r\n {\r\n model: {\r\n ...subkeySelectContactModel(),\r\n pathInheritance: 'full',\r\n itemPriority: 1\r\n },\r\n endOnResolve: true,\r\n endOnReject: true\r\n }\r\n ],\r\n resolutionAction: {\r\n type: 'complete',\r\n item: 'current'\r\n },\r\n sustainWhenNested: true\r\n };\r\n}\r\n\r\nexport function modipressStartModel(): GestureModel {\r\n return {\r\n id: 'modipress-start',\r\n resolutionPriority: 5,\r\n contacts: [\r\n {\r\n model: {\r\n ...modipressContactStartModel(),\r\n allowsInitialState(incomingSample, comparisonSample, baseItem) {\r\n return keySupportsModipress(baseItem);\r\n },\r\n itemChangeAction: 'reject',\r\n itemPriority: 1\r\n }\r\n }\r\n ],\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'modipress-hold',\r\n selectionMode: 'modipress',\r\n item: 'current' // return the modifier key ID so that we know to shift to it!\r\n }\r\n }\r\n}\r\n\r\nexport function modipressHoldModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'modipress-hold',\r\n resolutionPriority: 5,\r\n contacts: [\r\n {\r\n model: {\r\n ...modipressContactHoldModel(),\r\n itemChangeAction: 'reject',\r\n pathInheritance: 'full',\r\n timer: {\r\n duration: params.multitap.holdLength,\r\n expectedResult: true,\r\n // If entered due to 'reject' on 'modipress-multitap-end',\r\n // we want to immediately resolve.\r\n inheritElapsed: true\r\n }\r\n }\r\n }, {\r\n // If a new touchpoint comes in while in this state, lock in the modipress\r\n // and prevent multitapping on it, as a different key has been tapped before\r\n // the multitap base key since the latter's release.\r\n model: {\r\n ...instantContactResolutionModel(),\r\n },\r\n // The incoming tap belongs to a different gesture; we just care to know that it\r\n // happened.\r\n resetOnInstantFulfill: true\r\n }\r\n ],\r\n // To be clear: any time modipress-hold is triggered and the timer duration elapses,\r\n // we disable any potential to multitap on the modipress key.\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'modipress-end',\r\n selectionMode: 'modipress',\r\n // Key was already emitted from the 'modipress-start' stage.\r\n item: 'none'\r\n },\r\n rejectionActions: {\r\n path: {\r\n type: 'replace',\r\n // Because SHIFT -> CAPS multitap is a thing. Shift gets handled as a modipress first.\r\n // Modipresses resolve before multitaps... unless there's a model designed to handle & disambiguate both.\r\n replace: 'modipress-end-multitap-transition'\r\n }\r\n }\r\n }\r\n}\r\n\r\nexport function modipressMultitapTransitionModel(): GestureModel {\r\n return {\r\n id: 'modipress-end-multitap-transition',\r\n resolutionPriority: 5,\r\n contacts: [\r\n // None. This exists as an intermediate state to transition from\r\n // a basic modipress into a combined multitap + modipress.\r\n ],\r\n sustainTimer: {\r\n duration: 0,\r\n expectedResult: true\r\n },\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'modipress-multitap-start',\r\n item: 'none'\r\n }\r\n }\r\n}\r\n\r\nexport function modipressEndModel(): GestureModel {\r\n return {\r\n id: 'modipress-end',\r\n resolutionPriority: 5,\r\n contacts: [\r\n {\r\n model: {\r\n ...modipressContactEndModel(),\r\n itemChangeAction: 'reject',\r\n pathInheritance: 'full'\r\n }\r\n }\r\n ],\r\n resolutionAction: {\r\n type: 'complete',\r\n // Key was already emitted from the 'modipress-start' stage.\r\n item: 'none',\r\n awaitNested: true\r\n }\r\n }\r\n}\r\n\r\nexport function modipressMultitapStartModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'modipress-multitap-start',\r\n resolutionPriority: 6,\r\n contacts: [\r\n {\r\n model: {\r\n ...modipressContactStartModel(),\r\n pathInheritance: 'reject',\r\n allowsInitialState(incomingSample, comparisonSample, baseItem) {\r\n if(incomingSample.item != baseItem) {\r\n return false;\r\n }\r\n\r\n return keySupportsModipress(baseItem);\r\n },\r\n itemChangeAction: 'reject',\r\n itemPriority: 1\r\n }\r\n }\r\n ],\r\n sustainTimer: {\r\n duration: params.multitap.waitLength,\r\n expectedResult: false,\r\n baseItem: 'base'\r\n },\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'modipress-multitap-end',\r\n selectionMode: 'modipress',\r\n item: 'current' // return the modifier key ID so that we know to shift to it!\r\n }\r\n }\r\n}\r\n\r\nexport function modipressMultitapEndModel(params: FullGestureParams): GestureModel {\r\n return {\r\n id: 'modipress-multitap-end',\r\n resolutionPriority: 5,\r\n contacts: [\r\n {\r\n model: {\r\n ...modipressContactEndModel(),\r\n itemChangeAction: 'reject',\r\n pathInheritance: 'full',\r\n timer: {\r\n duration: params.multitap.holdLength,\r\n expectedResult: false\r\n }\r\n }\r\n }, {\r\n model: {\r\n // If a new touchpoint comes in while in this state, lock in the modipress\r\n // and prevent multitapping on it, as a different key has been tapped before\r\n // the multitap base key since the latter's release.\r\n ...instantContactRejectionModel()\r\n },\r\n // The incoming tap belongs to a different gesture; we just care to know that it\r\n // happened.\r\n resetOnInstantFulfill: true\r\n }\r\n ],\r\n resolutionAction: {\r\n type: 'chain',\r\n // Because SHIFT -> CAPS multitap is a thing. Shift gets handled as a modipress first.\r\n next: 'modipress-multitap-start',\r\n // Key was already emitted from the 'modipress-start' stage.\r\n item: 'none'\r\n },\r\n rejectionActions: {\r\n timer: {\r\n type: 'replace',\r\n replace: 'modipress-multitap-lock-transition'\r\n },\r\n path: {\r\n type: 'replace',\r\n replace: 'modipress-multitap-lock-transition'\r\n }\r\n }\r\n }\r\n}\r\n\r\nexport function modipressMultitapLockModel(): GestureModel {\r\n return {\r\n id: 'modipress-multitap-lock-transition',\r\n resolutionPriority: 5,\r\n contacts: [\r\n // This exists as an intermediate state to transition from\r\n // a modipress-multitap into a plain modipress without further\r\n // multitap rota behavior.\r\n {\r\n model: {\r\n ...instantContactResolutionModel(),\r\n pathResolutionAction: 'resolve' // doesn't end the path; just lets it continue.\r\n },\r\n }\r\n ],\r\n resolutionAction: {\r\n type: 'chain',\r\n next: 'modipress-end',\r\n selectionMode: 'modipress',\r\n item: 'none'\r\n }\r\n };\r\n}\r\n// #endregion", + "import { deepCopy } from '@keymanapp/web-utils';\r\n\r\nimport {\r\n gestures,\r\n GestureModelDefs\r\n} from '@keymanapp/gesture-recognizer';\r\n\r\nimport { BannerSuggestion } from './suggestionBanner.js';\r\nimport { simpleTapModelWithReset } from \"../input/gestures/specsForLayout.js\";\r\n\r\nexport const BannerSimpleTap: gestures.specs.GestureModel = {\r\n ...deepCopy(simpleTapModelWithReset(null)),\r\n resolutionAction: {\r\n type: 'complete',\r\n item: 'current'\r\n }\r\n};\r\n\r\nexport const BANNER_GESTURE_SET: GestureModelDefs = {\r\n gestures: [\r\n BannerSimpleTap\r\n ],\r\n sets: {\r\n default: [BannerSimpleTap.id]\r\n }\r\n}", + "import { DeviceSpec } from \"@keymanapp/web-utils\";\r\nimport { ParsedLengthStyle } from \"./lengthStyle.js\";\r\n\r\nexport function getFontSizeStyle(e: HTMLElement|string): {val: number, absolute: boolean} {\r\n var fs: string;\r\n\r\n if(typeof e == 'string') {\r\n fs = e;\r\n } else {\r\n fs = e.style.fontSize;\r\n if(!fs) {\r\n fs = getComputedStyle(e).fontSize;\r\n }\r\n }\r\n\r\n return new ParsedLengthStyle(fs);\r\n}\r\n\r\nexport function defaultFontSize(device: DeviceSpec, computedHeight: number, isEmbedded: boolean): ParsedLengthStyle {\r\n if(device.touchable) {\r\n const fontScale = device.formFactor == 'phone'\r\n ? 1.6 * (isEmbedded ? 0.65 : 0.6) * 1.2 // Combines original scaling factor with one previously applied to the layer group.\r\n : 2; // iPad or Android tablet\r\n return ParsedLengthStyle.special(fontScale, 'em');\r\n } else {\r\n return computedHeight ? ParsedLengthStyle.inPixels(computedHeight / 8) : undefined;\r\n }\r\n}", + "import { getFontSizeStyle } from \"../fontSizeUtils.js\";\r\n\r\nlet metricsCanvas: HTMLCanvasElement;\r\n\r\n/**\r\n * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.\r\n *\r\n * @param {String} text The text to be rendered.\r\n * @param emScale The absolute `px` size expected to match `1em`.\r\n * @param {String} style The CSSStyleDeclaration for an element to measure against, without modification.\r\n *\r\n * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393\r\n * This version has been substantially modified to work for this particular application.\r\n */\r\nexport function getTextMetrics(text: string, emScale: number, style: {fontFamily?: string, fontSize: string}): TextMetrics {\r\n // Since we may mutate the incoming style, let's make sure to copy it first.\r\n // Only the relevant properties, though.\r\n style = {\r\n fontFamily: style.fontFamily,\r\n fontSize: style.fontSize\r\n };\r\n\r\n // A final fallback - having the right font selected makes a world of difference.\r\n if(!style.fontFamily) {\r\n style.fontFamily = getComputedStyle(document.body).fontFamily;\r\n }\r\n\r\n if(!style.fontSize || style.fontSize == \"\") {\r\n style.fontSize = '1em';\r\n }\r\n\r\n let fontFamily = style.fontFamily;\r\n let fontSpec = getFontSizeStyle(style.fontSize);\r\n\r\n var fontSize: string;\r\n if(fontSpec.absolute) {\r\n // We've already got an exact size - use it!\r\n fontSize = fontSpec.val + 'px';\r\n } else {\r\n fontSize = fontSpec.val * emScale + 'px';\r\n }\r\n\r\n // re-use canvas object for better performance\r\n metricsCanvas = metricsCanvas ?? document.createElement(\"canvas\");\r\n\r\n var context = metricsCanvas.getContext(\"2d\");\r\n context.font = fontSize + \" \" + fontFamily;\r\n var metrics = context.measureText(text);\r\n\r\n return metrics;\r\n}", + "import { InputSample } from \"@keymanapp/gesture-recognizer\";\r\n\r\n/**\r\n * The amount of coordinate 'noise' allowed during a scroll-enabled touch\r\n * before interpreting the currently-ongoing touch command as having scrolled.\r\n */\r\nconst HAS_SCROLLED_FUDGE_FACTOR = 10;\r\n\r\n/**\r\n * This class was added to facilitate scroll handling for overflow-x elements, though it could\r\n * be extended in the future to accept overflow-y if needed.\r\n *\r\n * This is necessary because of the OSK's need to use `.preventDefault()` for stability; that\r\n * same method blocks native handling of overflow scrolling for touch browsers.\r\n */\r\nexport class BannerScrollState {\r\n totalLength = 0;\r\n\r\n baseCoord: InputSample;\r\n curCoord: InputSample;\r\n baseScrollLeft: number;\r\n\r\n constructor(coord: InputSample, baseScrollLeft: number) {\r\n this.baseCoord = coord;\r\n this.curCoord = coord;\r\n this.baseScrollLeft = baseScrollLeft;\r\n\r\n this.totalLength = 0;\r\n }\r\n\r\n updateTo(coord: InputSample): number {\r\n let prevCoord = this.curCoord;\r\n this.curCoord = coord;\r\n\r\n let delta = this.baseCoord.targetX - this.curCoord.targetX + this.baseScrollLeft;\r\n // Track the total amount of scrolling used, even if just a pixel-wide back and forth wiggle.\r\n this.totalLength += Math.abs(this.curCoord.targetX - prevCoord.targetX);\r\n\r\n return delta;\r\n }\r\n\r\n public get hasScrolled(): boolean {\r\n // Allow an accidental fudge-factor for overflow element noise during a touch, but not much.\r\n return this.totalLength > HAS_SCROLLED_FUDGE_FACTOR;\r\n }\r\n}", + "\r\nimport { type PredictionContext } from 'keyman/engine/interfaces';\r\nimport { createUnselectableElement } from 'keyman/engine/dom-utils';\r\n\r\nimport {\r\n GestureRecognizer,\r\n GestureRecognizerConfiguration,\r\n GestureSource,\r\n InputSample,\r\n PaddedZoneSource,\r\n RecognitionZoneSource\r\n} from '@keymanapp/gesture-recognizer';\r\n\r\nimport { BANNER_GESTURE_SET } from './bannerGestureSet.js';\r\n\r\nimport { DeviceSpec, Keyboard, KeyboardProperties, timedPromise } from 'keyman/engine/keyboard';\r\nimport { Banner } from './banner.js';\r\nimport { ParsedLengthStyle } from '../lengthStyle.js';\r\nimport { getFontSizeStyle } from '../fontSizeUtils.js';\r\nimport { getTextMetrics } from '../keyboard-layout/getTextMetrics.js';\r\nimport { BannerScrollState } from './bannerScrollState.js';\r\nimport { Suggestion } from '@keymanapp/common-types';\r\n\r\nconst TOUCHED_CLASS: string = 'kmw-suggest-touched';\r\nconst BANNER_SCROLLER_CLASS = 'kmw-suggest-banner-scroller';\r\n\r\nconst BANNER_VERT_ROAMING_HEIGHT_RATIO = 0.666;\r\n\r\n/**\r\n * The style to temporarily apply when updating suggestion text in order to prevent\r\n * fade transitions at that time.\r\n */\r\nconst FADE_SWALLOW_STYLE = 'swallow-fade-transition';\r\n\r\n/**\r\n * Defines various parameters used by `BannerSuggestion` instances for layout and formatting.\r\n * This object is designed first and foremost for use with `BannerSuggestion.update()`.\r\n */\r\ninterface BannerSuggestionFormatSpec {\r\n /**\r\n * Sets a minimum width to use for the `BannerSuggestion`'s element; this overrides any\r\n * and all settings that would otherwise result in a narrower final width.\r\n */\r\n minWidth?: number;\r\n\r\n /**\r\n * Sets the width of padding around the text of each suggestion. This should generally match\r\n * the 'width' of class = `.kmw-suggest-option::before` and class = `.kmw-suggest-option::after`\r\n * elements as defined in kmwosk.css.\r\n */\r\n paddingWidth: number,\r\n\r\n /**\r\n * The default font size to use for calculations based on relative font-size specs\r\n */\r\n emSize: number,\r\n\r\n /**\r\n * The font style (font-size, font-family) to use for suggestion-banner display text.\r\n */\r\n styleForFont: {\r\n fontSize: typeof CSSStyleDeclaration.prototype.fontSize,\r\n fontFamily: typeof CSSStyleDeclaration.prototype.fontFamily\r\n },\r\n\r\n /**\r\n * Sets a target width to use when 'collapsing' suggestions. Only affects those long\r\n * enough to need said 'collapsing'.\r\n */\r\n collapsedWidth?: number\r\n}\r\n\r\nexport class BannerSuggestion {\r\n div: HTMLDivElement;\r\n container: HTMLDivElement;\r\n private display: HTMLSpanElement;\r\n\r\n private _collapsedWidth: number;\r\n private _textWidth: number;\r\n private _minWidth: number;\r\n private _paddingWidth: number;\r\n\r\n public readonly rtl: boolean;\r\n\r\n private _suggestion: Suggestion;\r\n\r\n private index: number;\r\n\r\n static readonly BASE_ID = 'kmw-suggestion-';\r\n\r\n constructor(index: number, isRTL: boolean) {\r\n this.index = index;\r\n this.rtl = isRTL ?? false;\r\n\r\n this.constructRoot();\r\n\r\n // Provides an empty, base SPAN for text display. We'll swap these out regularly;\r\n // `Suggestion`s will have varying length and may need different styling.\r\n let display = this.display = createUnselectableElement('span');\r\n display.className = 'kmw-suggestion-text';\r\n this.container.appendChild(display);\r\n }\r\n\r\n get computedStyle() {\r\n return getComputedStyle(this.display);\r\n }\r\n\r\n private constructRoot() {\r\n // Add OSK suggestion labels\r\n let div = this.div = createUnselectableElement('div');\r\n div.className = \"kmw-suggest-option\";\r\n div.id = BannerSuggestion.BASE_ID + this.index;\r\n\r\n // @ts-ignore // Tags the element with its backing object.\r\n this.div['suggestion'] = this;\r\n\r\n let container = this.container = document.createElement('div');\r\n container.className = \"kmw-suggestion-container\";\r\n\r\n // Ensures that a reasonable default width, based on % is set. (Since it's not yet in the DOM, we may not yet have actual width info.)\r\n let usableWidth = 100 - SuggestionBanner.MARGIN * (SuggestionBanner.LONG_SUGGESTION_DISPLAY_LIMIT - 1);\r\n\r\n let widthpc = usableWidth / (SuggestionBanner.LONG_SUGGESTION_DISPLAY_LIMIT);\r\n container.style.minWidth = widthpc + '%';\r\n\r\n div.appendChild(container);\r\n }\r\n\r\n public matchKeyboardProperties(keyboardProperties: KeyboardProperties) {\r\n const div = this.div;\r\n\r\n if(keyboardProperties) {\r\n if (keyboardProperties['KLC']) {\r\n div.lang = keyboardProperties['KLC'];\r\n }\r\n\r\n // Establish base font settings\r\n let font = keyboardProperties['KFont'];\r\n if(font && font.family && font.family != '') {\r\n div.style.fontFamily = font.family;\r\n }\r\n }\r\n }\r\n\r\n get suggestion(): Suggestion {\r\n return this._suggestion;\r\n }\r\n\r\n /**\r\n * Function update\r\n * @param {Suggestion} suggestion Suggestion from the lexical model\r\n * @param {BannerSuggestionFormatSpec} format Formatting metadata to use for the Suggestion\r\n *\r\n * Update the ID and text of the BannerSuggestionSpec\r\n */\r\n public update(suggestion: Suggestion, format: BannerSuggestionFormatSpec) {\r\n this._suggestion = suggestion;\r\n\r\n let display = this.generateSuggestionText(this.rtl);\r\n this.container.replaceChild(display, this.display);\r\n this.display = display;\r\n\r\n // Set internal properties for use in format calculations.\r\n if(format.minWidth !== undefined) {\r\n this._minWidth = format.minWidth;\r\n }\r\n\r\n this._paddingWidth = format.paddingWidth;\r\n this._collapsedWidth = format.collapsedWidth;\r\n\r\n if(suggestion && suggestion.displayAs) {\r\n const rawMetrics = getTextMetrics(suggestion.displayAs, format.emSize, format.styleForFont);\r\n this._textWidth = rawMetrics.width;\r\n } else {\r\n this._textWidth = 0;\r\n }\r\n\r\n this.currentWidth = this.collapsedWidth;\r\n this.highlight(suggestion?.autoAccept);\r\n this.updateLayout();\r\n }\r\n\r\n public updateLayout() {\r\n if(!this.suggestion && this.index != 0) {\r\n this.div.style.width='0px';\r\n return;\r\n } else {\r\n this.div.style.width='';\r\n }\r\n\r\n const collapserStyle = this.container.style;\r\n collapserStyle.minWidth = this.collapsedWidth + 'px';\r\n\r\n if(this.rtl) {\r\n collapserStyle.marginRight = (this.collapsedWidth - this.expandedWidth) + 'px';\r\n } else {\r\n collapserStyle.marginLeft = (this.collapsedWidth - this.expandedWidth) + 'px';\r\n }\r\n\r\n this.updateFade();\r\n }\r\n\r\n public updateFade() {\r\n // Note: selected suggestion fade transitions are handled purely by CSS.\r\n // We want to prevent them when updating a suggestion, though.\r\n this.div.classList.add(FADE_SWALLOW_STYLE);\r\n // Be sure that our fade-swallow mechanism is able to trigger once;\r\n // we'll remove it after the current animation frame.\r\n window.requestAnimationFrame(() => {\r\n this.div.classList.remove(FADE_SWALLOW_STYLE);\r\n })\r\n\r\n // Never apply fading to the side that doesn't overflow.\r\n this.div.classList.add(`kmw-hide-fade-${this.rtl ? 'left' : 'right'}`);\r\n\r\n // Matches the side that overflows, depending on if LTR or RTL.\r\n const fadeClass = `kmw-hide-fade-${this.rtl ? 'right' : 'left'}`;\r\n\r\n // Is the suggestion already its ideal width?.\r\n if(!(this.expandedWidth - this.collapsedWidth)) {\r\n // Yes? Don't do any fading.\r\n this.div.classList.add(fadeClass);\r\n } else {\r\n this.div.classList.remove(fadeClass);\r\n }\r\n }\r\n\r\n /**\r\n * Denotes the threshold at which the banner suggestion will no longer gain width\r\n * in its default form, resulting in two separate states: \"collapsed\" and \"expanded\".\r\n */\r\n public get targetCollapsedWidth(): number {\r\n return this._collapsedWidth;\r\n }\r\n\r\n /**\r\n * The raw width needed to display the suggestion's display text without triggering overflow.\r\n */\r\n public get textWidth(): number {\r\n return this._textWidth;\r\n }\r\n\r\n /**\r\n * Width of the padding to apply equally on both sides of the suggestion's display text.\r\n * Is the sum of both, rather than the value applied to each side.\r\n */\r\n public get paddingWidth(): number {\r\n return this._paddingWidth;\r\n }\r\n\r\n /**\r\n * The absolute minimum width to allow for the represented suggestion's banner element.\r\n */\r\n public get minWidth(): number {\r\n return this._minWidth;\r\n }\r\n\r\n /**\r\n * The absolute minimum width to allow for the represented suggestion's banner element.\r\n */\r\n public set minWidth(val: number) {\r\n this._minWidth = val;\r\n }\r\n\r\n /**\r\n * The total width taken by the suggestion's banner element when fully expanded.\r\n * This may equal the `collapsed` width for sufficiently short suggestions.\r\n */\r\n public get expandedWidth(): number {\r\n // minWidth must be defined AND greater for the conditional to return this.minWidth.\r\n return this.minWidth > this.spanWidth ? this.minWidth : this.spanWidth;\r\n }\r\n\r\n /**\r\n * The total width used by the internal contents of the suggestion's banner element when not obscured.\r\n */\r\n public get spanWidth(): number {\r\n let spanWidth = this.textWidth ?? 0;\r\n if(spanWidth) {\r\n spanWidth += this.paddingWidth ?? 0;\r\n }\r\n\r\n return spanWidth;\r\n }\r\n\r\n /**\r\n * The actual width to be used for the `BannerSuggestion`'s display element when in the 'collapsed'\r\n * state and not transitioning.\r\n */\r\n public get collapsedWidth(): number {\r\n // Allow shrinking a suggestion's width if it has excess whitespace.\r\n let utilizedWidth = this.spanWidth < this.targetCollapsedWidth ? this.spanWidth : this.targetCollapsedWidth;\r\n // If a minimum width has been specified, enforce that minimum.\r\n let maxWidth = utilizedWidth < this.expandedWidth ? utilizedWidth : this.expandedWidth;\r\n\r\n // Will return maxWidth if this.minWidth is undefined.\r\n return (this.minWidth > maxWidth ? this.minWidth : maxWidth);\r\n }\r\n\r\n /**\r\n * The actual width currently utilized by the `BannerSuggestion`'s display element, regardless of\r\n * current state.\r\n */\r\n public get currentWidth(): number {\r\n return this.div.offsetWidth;\r\n }\r\n\r\n /**\r\n * The actual width currently utilized by the `BannerSuggestion`'s display element, regardless of\r\n * current state.\r\n */\r\n public set currentWidth(val: number) {\r\n // TODO: probably should set up errors or something here...\r\n if(val < this.collapsedWidth) {\r\n val = this.collapsedWidth;\r\n } else if(val > this.expandedWidth) {\r\n val = this.expandedWidth;\r\n }\r\n\r\n if(this.rtl) {\r\n this.container.style.marginRight = `${val - this.expandedWidth}px`;\r\n } else {\r\n this.container.style.marginLeft = `${val - this.expandedWidth}px`;\r\n }\r\n }\r\n\r\n public highlight(on: boolean) {\r\n const elem = this.div;\r\n\r\n if(on) {\r\n elem.classList.add(TOUCHED_CLASS);\r\n } else {\r\n elem.classList.remove(TOUCHED_CLASS);\r\n }\r\n }\r\n\r\n public isEmpty(): boolean {\r\n return !this._suggestion;\r\n }\r\n\r\n /**\r\n * Function generateSuggestionText\r\n * @return {HTMLSpanElement} Span element of the suggestion\r\n * Description Produces a HTMLSpanElement with the key's actual text.\r\n */\r\n //\r\n public generateSuggestionText(rtl: boolean): HTMLSpanElement {\r\n let suggestion = this._suggestion;\r\n var suggestionText: string;\r\n\r\n var s=createUnselectableElement('span');\r\n s.className = 'kmw-suggestion-text';\r\n\r\n if(suggestion == null) {\r\n return s;\r\n }\r\n\r\n if(suggestion.displayAs == null || suggestion.displayAs == '') {\r\n suggestionText = '\\xa0'; // default: nbsp.\r\n } else {\r\n // Default the LTR ordering to match that of the active keyboard.\r\n let orderCode = rtl ? 0x202e /* RTL */ : 0x202d /* LTR */;\r\n suggestionText = String.fromCharCode(orderCode) + suggestion.displayAs;\r\n }\r\n\r\n // TODO: Dynamic suggestion text resizing. (Refer to OSKKey.getTextWidth in visualKeyboard.ts.)\r\n\r\n // Finalize the suggestion text\r\n s.innerHTML = suggestionText;\r\n return s;\r\n }\r\n}\r\n\r\n/**\r\n * Function SuggestionBanner\r\n * Scope Public\r\n * @param {number} height - If provided, the height of the banner in pixels\r\n * Description Display lexical model suggestions in the banner\r\n */\r\nexport class SuggestionBanner extends Banner {\r\n public static readonly SUGGESTION_LIMIT: number = 8;\r\n public static readonly LONG_SUGGESTION_DISPLAY_LIMIT: number = 3;\r\n public static readonly MARGIN = 1;\r\n\r\n public readonly type = \"suggestion\";\r\n\r\n private currentSuggestions: Suggestion[] = [];\r\n\r\n private options : BannerSuggestion[] = [];\r\n private separators: HTMLElement[] = [];\r\n\r\n private isRTL: boolean = false;\r\n\r\n /**\r\n * The banner 'container', which is also the root element for banner scrolling.\r\n */\r\n private readonly container: HTMLElement;\r\n private highlightAnimation: SuggestionExpandContractAnimation;\r\n\r\n private gestureEngine: GestureRecognizer;\r\n private scrollState: BannerScrollState;\r\n private selectionBounds: RecognitionZoneSource;\r\n\r\n private _predictionContext: PredictionContext;\r\n\r\n constructor(hostDevice: DeviceSpec, height?: number) {\r\n super(height || Banner.DEFAULT_HEIGHT);\r\n\r\n this.getDiv().className = this.getDiv().className + ' ' + SuggestionBanner.BANNER_CLASS;\r\n\r\n this.container = document.createElement('div');\r\n this.container.className = BANNER_SCROLLER_CLASS;\r\n this.getDiv().appendChild(this.container);\r\n this.buildInternals(false);\r\n\r\n this.gestureEngine = this.setupInputHandling();\r\n }\r\n\r\n shutdown() {\r\n this.gestureEngine.destroy();\r\n }\r\n\r\n buildInternals(rtl: boolean) {\r\n this.isRTL = rtl;\r\n if(this.options.length > 0) {\r\n this.options = [];\r\n this.separators = [];\r\n }\r\n\r\n for (var i=0; i {\r\n // Auto-cancels suggestion-selection if the finger moves too far; having very generous\r\n // safe-zone settings also helps keep scrolls active on demo pages, etc.\r\n const safeBounds = new PaddedZoneSource(this.getDiv(), [-Number.MAX_SAFE_INTEGER]);\r\n this.selectionBounds = new PaddedZoneSource(\r\n this.getDiv(),\r\n [-BANNER_VERT_ROAMING_HEIGHT_RATIO * this.height, -Number.MAX_SAFE_INTEGER]\r\n );\r\n\r\n const config: GestureRecognizerConfiguration = {\r\n targetRoot: this.getDiv(),\r\n maxRoamingBounds: safeBounds,\r\n safeBounds: safeBounds,\r\n // touchEventRoot: this.element, // is the default\r\n itemIdentifier: (sample) => {\r\n const selBounds = this.selectionBounds.getBoundingClientRect();\r\n\r\n // Step 1: is the coordinate within the range we permit for selecting _anything_?\r\n if(sample.clientX < selBounds.left || sample.clientX > selBounds.right) {\r\n return null;\r\n }\r\n if(sample.clientY < selBounds.top || sample.clientY > selBounds.bottom) {\r\n return null;\r\n }\r\n\r\n // Step 2: find the best-matching selection.\r\n\r\n let bestMatch: BannerSuggestion = null;\r\n let bestDist = Number.MAX_VALUE;\r\n\r\n for(const option of this.options) {\r\n const optionBounding = option.div.getBoundingClientRect();\r\n\r\n if(optionBounding.left <= sample.clientX && sample.clientX < optionBounding.right) {\r\n // If there is no backing suggestion, then there's no real selection.\r\n // May happen when no suggestions are available.\r\n return option.suggestion ? option : null;\r\n } else {\r\n const dist = (sample.clientX < optionBounding.left ? -1 : 1) * (sample.clientX - optionBounding.left);\r\n\r\n if(dist < bestDist) {\r\n bestDist = dist;\r\n bestMatch = option;\r\n }\r\n }\r\n }\r\n\r\n // If there is no backing suggestion, then there's no real selection.\r\n return bestMatch.suggestion ? bestMatch : null;\r\n }\r\n };\r\n\r\n const engine = new GestureRecognizer(BANNER_GESTURE_SET, config);\r\n\r\n const sourceTracker: {\r\n source: GestureSource,\r\n scrollingHandler: (sample: InputSample) => void,\r\n suggestion: BannerSuggestion\r\n } = {\r\n source: null,\r\n scrollingHandler: null,\r\n suggestion: null\r\n };\r\n\r\n const markSelection = (suggestion: BannerSuggestion) => {\r\n suggestion.highlight(true);\r\n if(this.highlightAnimation) {\r\n this.highlightAnimation.cancel();\r\n this.highlightAnimation.decouple();\r\n }\r\n\r\n this.highlightAnimation = new SuggestionExpandContractAnimation(this.container, suggestion, false);\r\n this.highlightAnimation.expand();\r\n }\r\n\r\n const clearSelection = (suggestion: BannerSuggestion) => {\r\n suggestion.highlight(false);\r\n if(!this.highlightAnimation) {\r\n this.highlightAnimation = new SuggestionExpandContractAnimation(this.container, suggestion, false);\r\n }\r\n this.highlightAnimation.collapse();\r\n }\r\n\r\n engine.on('inputstart', (source) => {\r\n // The banner does not support multi-touch - if one is still current, block all others.\r\n if(sourceTracker.source) {\r\n source.terminate(true);\r\n return;\r\n }\r\n\r\n const autoselection = this._predictionContext.selected;\r\n this._predictionContext.selected = null;\r\n if(autoselection) {\r\n this.options.forEach((entry) => {\r\n if(entry.suggestion == autoselection) {\r\n entry.highlight(false);\r\n };\r\n });\r\n }\r\n\r\n this.scrollState = new BannerScrollState(source.currentSample, this.container.scrollLeft);\r\n const suggestion = source.baseItem;\r\n\r\n sourceTracker.source = source;\r\n sourceTracker.scrollingHandler = (sample) => {\r\n const newScrollLeft = this.scrollState.updateTo(sample);\r\n this.highlightAnimation?.setBaseScroll(newScrollLeft);\r\n\r\n // Only re-enable the original suggestion, even if the touchpoint finds\r\n // itself over a different suggestion. Might happen if a scroll boundary\r\n // is reached.\r\n const incoming = sample.item ? suggestion : null;\r\n\r\n // It's possible to cancel selection while still scrolling.\r\n if(incoming != sourceTracker.suggestion) {\r\n if(sourceTracker.suggestion) {\r\n clearSelection(sourceTracker.suggestion);\r\n }\r\n\r\n sourceTracker.suggestion = incoming;\r\n if(incoming) {\r\n markSelection(incoming);\r\n }\r\n }\r\n };\r\n\r\n sourceTracker.suggestion = source.currentSample.item;\r\n if(sourceTracker.suggestion) {\r\n markSelection(sourceTracker.suggestion);\r\n }\r\n\r\n const terminationHandler = () => {\r\n const currentSuggestions = this.currentSuggestions;\r\n // First, schedule reselection of the autoselected suggestion.\r\n // We shouldn't do it synchronously, as suggestion acceptance triggers\r\n // _after_ this handler is called.\r\n // Delaying via the task queue is enough to get the desired order of events.\r\n timedPromise(0).then(async () => {\r\n // If the suggestion list instance has changed, our state has changed; do\r\n // not reselect.\r\n if(currentSuggestions != this.currentSuggestions) {\r\n return;\r\n }\r\n\r\n // The suggestions are still current? Then restore the original\r\n // auto-correct suggestion and its highlighting.\r\n this._predictionContext.selected = autoselection;\r\n if(autoselection) {\r\n for(let entry of this.options) {\r\n if(entry.suggestion == autoselection) {\r\n entry.highlight(true);\r\n break;\r\n };\r\n }\r\n }\r\n });\r\n\r\n if(sourceTracker.suggestion) {\r\n clearSelection(sourceTracker.suggestion);\r\n sourceTracker.suggestion = null;\r\n }\r\n\r\n sourceTracker.source = null;\r\n sourceTracker.scrollingHandler = null;\r\n }\r\n\r\n source.path.on('complete', terminationHandler);\r\n source.path.on('invalidated', terminationHandler);\r\n source.path.on('step', sourceTracker.scrollingHandler);\r\n });\r\n\r\n engine.on('recognizedgesture', (sequence) => {\r\n // The actual result comes in via the sequence's `stage` event.\r\n sequence.once('stage', (result) => {\r\n const suggestion = result.item; // Should also == sourceTracker.suggestion.\r\n // 1. A valid suggestion has been selected\r\n // 2. The user wasn't scrolling the banner. (If they were, they likely\r\n // need to lift their finger to select a newly-visible suggestion!)\r\n // 3. The suggestions themselves are still valid; avoid suggestion\r\n // double-application or similar.\r\n if(suggestion && !this.scrollState.hasScrolled && this.currentSuggestions.length > 0) {\r\n // Invalidate the suggestions internally, but don't visually update;\r\n // this will avoid banner-flicker.\r\n this.currentSuggestions = [];\r\n this.predictionContext.accept(suggestion.suggestion).then(() => {\r\n // Reset the scroll state\r\n this.container.scrollLeft = this.isRTL ? this.container.scrollWidth : 0;\r\n });\r\n }\r\n\r\n this.scrollState = null;\r\n });\r\n });\r\n\r\n return engine;\r\n }\r\n\r\n protected update() {\r\n const result = super.update();\r\n\r\n // Ensure the banner's extended recognition zone is based on proper, up-to-date layout info.\r\n // Note: during banner init, `this.gestureEngine` may only be defined after\r\n // the first call to this setter!\r\n (this.selectionBounds as PaddedZoneSource)?.updatePadding(\r\n [-BANNER_VERT_ROAMING_HEIGHT_RATIO * this.height, -Number.MAX_SAFE_INTEGER]\r\n );\r\n\r\n return result;\r\n }\r\n\r\n public configureForKeyboard(keyboard: Keyboard, keyboardProperties: KeyboardProperties) {\r\n const rtl = keyboard.isRTL;\r\n\r\n // Removes all previous children. (.replaceChildren requires Chrome for Android 86.)\r\n // Instantly replaces all children with an empty text node, bypassing the need to actually\r\n // parse incoming HTML.\r\n //\r\n // Just in case, alternative approaches: https://stackoverflow.com/a/3955238\r\n this.container.textContent = '';\r\n\r\n // Builds new children to match needed RTL properties.\r\n this.buildInternals(rtl);\r\n\r\n this.options.forEach((option) => option.matchKeyboardProperties(keyboardProperties));\r\n this.onSuggestionUpdate(this.currentSuggestions); // restore suggestions\r\n }\r\n\r\n public get predictionContext(): PredictionContext {\r\n return this._predictionContext;\r\n }\r\n\r\n public set predictionContext(context: PredictionContext) {\r\n if(this._predictionContext) {\r\n // disconnect the old one!\r\n this._predictionContext.off('update', this.onSuggestionUpdate);\r\n }\r\n\r\n // connect the new one!\r\n this._predictionContext = context;\r\n if(context) {\r\n context.on('update', this.onSuggestionUpdate);\r\n this.onSuggestionUpdate(context.currentSuggestions);\r\n }\r\n }\r\n\r\n /**\r\n * Produces a closure useful for updating the SuggestionBanner's UI to match newly-received\r\n * suggestions, including optimization of the banner's layout.\r\n * @param suggestions\r\n */\r\n public onSuggestionUpdate = (suggestions: Suggestion[]): void => {\r\n this.currentSuggestions = suggestions;\r\n // Immediately stop all animations and reset options accordingly.\r\n this.highlightAnimation?.cancel();\r\n\r\n const fontStyleBase = this.options[0].computedStyle;\r\n // Do NOT just re-use the returned object from the line above; it may spontaneously change\r\n // (in a bad way) when the underlying span is replaced!\r\n const fontStyle = {\r\n fontSize: fontStyleBase.fontSize,\r\n fontFamily: fontStyleBase.fontFamily\r\n }\r\n const emSizeStr = getComputedStyle(document.body).fontSize;\r\n const emSize = getFontSizeStyle(emSizeStr).val;\r\n\r\n const textStyle = getComputedStyle(this.options[0].container.firstChild as HTMLSpanElement);\r\n\r\n const targetWidth = this.width / SuggestionBanner.LONG_SUGGESTION_DISPLAY_LIMIT;\r\n\r\n // computedStyle will fail if the element's not in the DOM yet.\r\n // Seeks to get the values specified within kmwosk.css.\r\n const textLeftPad = new ParsedLengthStyle(textStyle.paddingLeft || '4px');\r\n const textRightPad = new ParsedLengthStyle(textStyle.paddingRight || '4px');\r\n\r\n let optionFormat: BannerSuggestionFormatSpec = {\r\n paddingWidth: textLeftPad.val + textRightPad.val, // Assumes fixed px padding.\r\n emSize: emSize,\r\n styleForFont: fontStyle,\r\n collapsedWidth: targetWidth,\r\n minWidth: 0,\r\n }\r\n\r\n for (let i=0; i i) {\r\n const suggestion = suggestions[i];\r\n d.update(suggestion, optionFormat);\r\n } else {\r\n d.update(null, optionFormat);\r\n }\r\n }\r\n\r\n this.refreshLayout();\r\n }\r\n\r\n readonly refreshLayout = () => {\r\n let collapsedOptions: BannerSuggestion[] = [];\r\n let totalWidth = 0;\r\n\r\n let displayCount = Math.min(this.currentSuggestions.length, 8);\r\n for(let i=0; i < displayCount; i++) {\r\n // Note: options is an array of pre-built suggestion-hosting elements, with\r\n // fixed SUGGESTIONS_LIMIT length - not a length that dynamically changes to\r\n // match the number of suggestions available. Those without a suggestion\r\n // are hidden, but preserved.\r\n const opt = this.options[i];\r\n opt.minWidth = 0; // remove any previously-applied padding\r\n totalWidth += opt.collapsedWidth;\r\n\r\n if(opt.collapsedWidth < opt.expandedWidth) {\r\n collapsedOptions.push(opt);\r\n }\r\n }\r\n\r\n // Ensure one suggestion is always displayed, even if empty. (Keep the separators out)\r\n displayCount = displayCount || 1;\r\n\r\n if(totalWidth < this.width) {\r\n let separatorWidth = (this.width * 0.01 * (displayCount-1));\r\n // Prioritize adding padding to suggestions that actually need it.\r\n // Use equal measure for each so long as it still could use extra display space.\r\n while(totalWidth < this.width && collapsedOptions.length > 0) {\r\n let maxFillPadding = (this.width - totalWidth - separatorWidth) / collapsedOptions.length;\r\n collapsedOptions.sort((a, b) => a.expandedWidth - b.expandedWidth);\r\n\r\n let shortestCollapsed = collapsedOptions[0];\r\n let neededWidth = shortestCollapsed.expandedWidth - shortestCollapsed.collapsedWidth;\r\n\r\n let padding = Math.min(neededWidth, maxFillPadding);\r\n\r\n // Check: it is possible that two elements were matched for equal length, thus the second loop's takes no additional padding.\r\n // No need to trigger re-layout ops for that case.\r\n if(padding > 0) {\r\n collapsedOptions.forEach((a) => a.minWidth = a.collapsedWidth + padding);\r\n totalWidth += padding * collapsedOptions.length; // don't forget to record that we added the padding!\r\n }\r\n\r\n collapsedOptions.splice(0, 1); // discard the element we based our judgment upon; we need not consider it any longer.\r\n }\r\n\r\n // If there's STILL leftover padding to distribute, let's do that now.\r\n let fillPadding = (this.width - totalWidth - separatorWidth) / displayCount;\r\n\r\n for(let i=0; i < displayCount; i++) {\r\n const d = this.options[i];\r\n\r\n d.minWidth = d.collapsedWidth + fillPadding;\r\n d.updateLayout();\r\n }\r\n }\r\n\r\n // Hide any separators beyond the final displayed suggestion\r\n for(let i=0; i < SuggestionBanner.SUGGESTION_LIMIT - 1; i++) {\r\n this.separators[i].style.display = i < displayCount - 1 ? '' : 'none';\r\n }\r\n }\r\n}\r\n\r\nclass SuggestionExpandContractAnimation {\r\n private scrollContainer: HTMLElement | null;\r\n private option: BannerSuggestion;\r\n\r\n private collapsedScrollOffset: number;\r\n private rootScrollOffset: number;\r\n\r\n private startTimestamp: number;\r\n private pendingAnimation: number;\r\n\r\n private static TRANSITION_TIME = 250; // in ms.\r\n\r\n constructor(scrollContainer: HTMLElement, option: BannerSuggestion, forRTL: boolean) {\r\n this.scrollContainer = scrollContainer;\r\n this.option = option;\r\n this.collapsedScrollOffset = scrollContainer.scrollLeft;\r\n this.rootScrollOffset = scrollContainer.scrollLeft;\r\n }\r\n\r\n public setBaseScroll(val: number) {\r\n this.collapsedScrollOffset = val;\r\n\r\n // If the user has shifted the scroll position to make more of the element visible, we can remove part\r\n // of the corresponding scrolling offset permanently; the user's taken action to view that area.\r\n if(this.option.rtl) {\r\n // A higher scrollLeft (scrolling right) will reveal more of an initially-clipped suggestion.\r\n if(val > this.rootScrollOffset) {\r\n this.rootScrollOffset = val;\r\n }\r\n } else {\r\n // Here, a lower scrollLeft (scrolling left).\r\n if(val < this.rootScrollOffset) {\r\n this.rootScrollOffset = val;\r\n }\r\n }\r\n\r\n // Synchronize the banner-scroller's offset update with that of the\r\n // animation for expansion and collapsing.\r\n window.requestAnimationFrame(this.setScrollOffset);\r\n }\r\n\r\n /**\r\n * Performs mapping of the user's touchpoint to properly-offset scroll coordinates based on\r\n * the state of the ongoing scroll operation.\r\n *\r\n * First priority: this function aims to keep all currently-visible parts of a selected\r\n * suggestion visible when first selected. Any currently-clipped parts will remain clipped.\r\n *\r\n * Second priority: all animations should be smooth and continuous; aesthetics do matter to\r\n * users.\r\n *\r\n * Third priority: when possible without violating the first two priorities, this (in tandem with\r\n * adjustments within `setBaseScroll`) will aim to sync the touchpoint with its original\r\n * location on an expanded suggestion.\r\n * - For LTR languages, this means that suggestions will \"expand left\" if possible.\r\n * - While for RTL languages, they will \"expand right\" if possible.\r\n * - However, if they would expand outside of the banner's effective viewport, a scroll offset\r\n * will kick in to enforce the \"first priority\" mentioned above.\r\n * - This \"scroll offset\" will be progressively removed (because second priority) if and as\r\n * the user manually scrolls to reveal relevant space that was originally outside of the viewport.\r\n *\r\n * @returns\r\n */\r\n private setScrollOffset = () => {\r\n // If we've been 'decoupled', a different instance (likely for a different suggestion)\r\n // is responsible for counter-scrolling.\r\n if(!this.scrollContainer) {\r\n return;\r\n }\r\n\r\n // -- Clamping / \"scroll offset\" logic --\r\n\r\n // As currently written / defined below, and used internally within this function, \"clamping\"\r\n // refers to alterations to scroll-positioned mapping designed to keep as much of the expanded\r\n // option visible as possible via the offsets below (that is, \"clamped\" to the relevant border)\r\n // while not adding extra discontinuity by pushing already-obscured parts of the expanded option\r\n // into visible range.\r\n //\r\n // In essence, it's an extra \"scroll offset\" we apply that is dynamically adjusted depending on\r\n // scroll position as it changes. This offset may be decreased when it is no longer needed to\r\n // make parts of the element visible.\r\n\r\n // The amount of extra space being taken by a partially or completely expanded suggestion.\r\n const maxWidthToCounterscroll = this.option.currentWidth - this.option.collapsedWidth;\r\n const rtl = this.option.rtl;\r\n\r\n // If non-zero, indicates the pixel-width of the collapsed form of the suggestion clipped by the relevant screen border.\r\n const ltrOverflow = Math.max(this.rootScrollOffset - this.option.div.offsetLeft, 0);\r\n const rtlOverflow = Math.max(this.option.div.offsetLeft + this.option.collapsedWidth - (this.rootScrollOffset + this.scrollContainer.offsetWidth));\r\n\r\n const srcCounterscrollOverflow = Math.max(rtl ? rtlOverflow : ltrOverflow, 0); // positive offset into overflow-land.\r\n\r\n // Base position for scrollLeft clamped within std element scroll bounds, including:\r\n // - an adjustment to cover the extra width from expansion\r\n // - preserving the base expected overflow levels\r\n // Does NOT make adjustments to force extra visibility on the element being highlighted/focused.\r\n const unclampedExpandingScrollOffset = Math.max(this.collapsedScrollOffset + (rtl ? 0 : 1) * maxWidthToCounterscroll, 0) + (rtl ? 0 : -1) * srcCounterscrollOverflow;\r\n // The same, but for our 'root scroll coordinate'.\r\n const rootUnclampedExpandingScrollOffset = Math.max(this.rootScrollOffset + (rtl ? 0 : 1) * maxWidthToCounterscroll, 0) + (rtl ? 0 : -1) * srcCounterscrollOverflow;\r\n\r\n // Do not shift an element clipped by the screen border further than its original scroll starting point.\r\n const elementOffsetForClamping = rtl\r\n ? Math.max(unclampedExpandingScrollOffset, rootUnclampedExpandingScrollOffset)\r\n : Math.min(unclampedExpandingScrollOffset, rootUnclampedExpandingScrollOffset);\r\n\r\n // Based on the scroll point selected, determine how far to offset scrolls to keep the option in visible range.\r\n // Higher .scrollLeft values make this non-zero and reflect when scroll has begun clipping the element.\r\n const elementOffsetFromBorder = rtl\r\n // RTL offset: \"offsetRight\" based on \"scrollRight\"\r\n ? Math.max(this.option.div.offsetLeft + this.option.currentWidth - (elementOffsetForClamping + this.scrollContainer.offsetWidth), 0) // double-check this one.\r\n // LTR: based on scrollLeft offsetLeft\r\n : Math.max(elementOffsetForClamping - this.option.div.offsetLeft, 0);\r\n\r\n // If the element is close enough to the border, don't offset beyond the element!\r\n // If it is further, do not add excess padding - it'd effectively break scrolling.\r\n // Do maintain any remaining scroll offset that exists, though.\r\n const clampedExpandingScrollOffset = Math.min(maxWidthToCounterscroll, elementOffsetFromBorder);\r\n\r\n const finalScrollOffset = unclampedExpandingScrollOffset // base scroll-coordinate transform mapping based on extra width from element expansion\r\n + (rtl ? 1 : -1) * clampedExpandingScrollOffset // offset to scroll to put word-start border against the corresponding screen border, fully visible\r\n + (rtl ? 0 : 1) * srcCounterscrollOverflow; // offset to maintain original overflow past that border if it existed\r\n\r\n // -- Final step: Apply & fine-tune the final scroll positioning --\r\n this.scrollContainer.scrollLeft = finalScrollOffset;\r\n\r\n // Prevent \"jitters\" during counterscroll that occur on expansion / collapse animation.\r\n // A one-frame \"error correction\" effect at the end of animation is far less jarring.\r\n if(this.pendingAnimation) {\r\n // scrollLeft doesn't work well with fractional values, unlike marginLeft / marginRight\r\n const fractionalOffset = this.scrollContainer.scrollLeft - finalScrollOffset;\r\n // So we put the fractional difference into marginLeft to force it to sync.\r\n this.option.currentWidth += fractionalOffset;\r\n }\r\n }\r\n\r\n public decouple() {\r\n this.cancel();\r\n this.scrollContainer = null;\r\n }\r\n\r\n private clear() {\r\n this.startTimestamp = null;\r\n window.cancelAnimationFrame(this.pendingAnimation);\r\n this.pendingAnimation = null;\r\n }\r\n\r\n cancel() {\r\n this.clear();\r\n this.option.currentWidth = this.option.collapsedWidth;\r\n }\r\n\r\n public expand() {\r\n // Cancel any prior iterating animation-frame commands.\r\n this.clear();\r\n\r\n // set timestamp, adjusting the current time based on intermediate progress\r\n this.startTimestamp = performance.now();\r\n\r\n let progress = this.option.currentWidth - this.option.collapsedWidth;\r\n let expansionDiff = this.option.expandedWidth - this.option.collapsedWidth;\r\n\r\n if(progress != 0) {\r\n // Offset the timestamp by noting what start time would have given rise to\r\n // the current position, keeping related animations smooth.\r\n this.startTimestamp -= (progress / expansionDiff) * SuggestionExpandContractAnimation.TRANSITION_TIME;\r\n }\r\n\r\n this.pendingAnimation = window.requestAnimationFrame(this._expand);\r\n }\r\n\r\n private _expand = (timestamp: number) => {\r\n if(this.startTimestamp === undefined) {\r\n return; // No active expand op exists. May have been cancelled via `clear`.\r\n }\r\n\r\n let progressTime = timestamp - this.startTimestamp;\r\n let fin = progressTime > SuggestionExpandContractAnimation.TRANSITION_TIME;\r\n\r\n if(fin) {\r\n progressTime = SuggestionExpandContractAnimation.TRANSITION_TIME;\r\n }\r\n\r\n // -- Part 1: handle option expand / collapse state --\r\n let expansionDiff = this.option.expandedWidth - this.option.collapsedWidth;\r\n let expansionRatio = progressTime / SuggestionExpandContractAnimation.TRANSITION_TIME;\r\n\r\n // expansionDiff * expansionRatio: the total adjustment from 'collapsed' width, in px.\r\n const expansionPx = expansionDiff * expansionRatio;\r\n this.option.currentWidth = expansionPx + this.option.collapsedWidth;\r\n\r\n // Part 2: trigger the next animation frame.\r\n if(!fin) {\r\n this.pendingAnimation = window.requestAnimationFrame(this._expand);\r\n } else {\r\n this.clear();\r\n }\r\n\r\n // Part 3: perform any needed counter-scrolling, scroll clamping, etc\r\n // Existence of a followup animation frame is part of the logic, so keep this 'after'!\r\n this.setScrollOffset();\r\n };\r\n\r\n public collapse() {\r\n // Cancel any prior iterating animation-frame commands.\r\n this.clear();\r\n\r\n // set timestamp, adjusting the current time based on intermediate progress\r\n this.startTimestamp = performance.now();\r\n\r\n let progress = this.option.expandedWidth - this.option.currentWidth;\r\n let expansionDiff = this.option.expandedWidth - this.option.collapsedWidth;\r\n\r\n if(progress != 0) {\r\n // Offset the timestamp by noting what start time would have given rise to\r\n // the current position, keeping related animations smooth.\r\n this.startTimestamp -= (progress / expansionDiff) * SuggestionExpandContractAnimation.TRANSITION_TIME;\r\n }\r\n\r\n this.pendingAnimation = window.requestAnimationFrame(this._collapse);\r\n }\r\n\r\n private _collapse = (timestamp: number) => {\r\n if(this.startTimestamp === undefined) {\r\n return; // No active collapse op exists. May have been cancelled via `clear`.\r\n }\r\n\r\n let progressTime = timestamp - this.startTimestamp;\r\n let fin = progressTime > SuggestionExpandContractAnimation.TRANSITION_TIME;\r\n if(fin) {\r\n progressTime = SuggestionExpandContractAnimation.TRANSITION_TIME;\r\n }\r\n\r\n // -- Part 1: handle option expand / collapse state --\r\n let expansionDiff = this.option.expandedWidth - this.option.collapsedWidth;\r\n let expansionRatio = 1 - progressTime / SuggestionExpandContractAnimation.TRANSITION_TIME;\r\n\r\n // expansionDiff * expansionRatio: the total adjustment from 'collapsed' width, in px.\r\n const expansionPx = expansionDiff * expansionRatio;\r\n this.option.currentWidth = expansionPx + this.option.collapsedWidth;\r\n\r\n // Part 2: trigger the next animation frame.\r\n if(!fin) {\r\n this.pendingAnimation = window.requestAnimationFrame(this._collapse);\r\n } else {\r\n this.clear();\r\n }\r\n\r\n // Part 3: perform any needed counter-scrolling, scroll clamping, etc\r\n // Existence of a followup animation frame is part of the logic, so keep this 'after'!\r\n this.setScrollOffset();\r\n };\r\n}\r\n\r\n", + "import { Banner } from \"./banner.js\";\r\n\r\nexport class HTMLBanner extends Banner {\r\n readonly container: ShadowRoot | HTMLElement;\r\n readonly type = 'html';\r\n\r\n constructor(contents?: string) {\r\n super();\r\n\r\n const bannerHost = this.getDiv();\r\n\r\n // Ensure any HTML styling applied for the banner contents only apply to the contents,\r\n // and not the banner's `position: 'relative'` hosting element.\r\n const div = document.createElement('div');\r\n div.style.userSelect = 'none';\r\n div.style.height = '100%';\r\n div.style.width = '100%';\r\n bannerHost.appendChild(div);\r\n\r\n // If possible, quarantine styling and JS for the banner contents within Shadow DOM.\r\n this.container = (div.attachShadow) ? div.attachShadow({mode: 'closed'}) : div;\r\n this.container.innerHTML = contents;\r\n }\r\n\r\n get innerHTML() {\r\n return this.container.innerHTML;\r\n }\r\n\r\n set innerHTML(raw: string) {\r\n this.container.innerHTML = raw;\r\n }\r\n}", + "import { DeviceSpec } from '@keymanapp/web-utils';\r\nimport type { PredictionContext, StateChangeEnum } from 'keyman/engine/interfaces';\r\nimport { ImageBanner } from './imageBanner.js';\r\nimport { SuggestionBanner } from './suggestionBanner.js';\r\nimport { BannerView } from './bannerView.js';\r\nimport { Banner } from './banner.js';\r\nimport { BlankBanner } from './blankBanner.js';\r\nimport { HTMLBanner } from './htmlBanner.js';\r\nimport { Keyboard, KeyboardProperties } from 'keyman/engine/keyboard';\r\n\r\nexport class BannerController {\r\n private container: BannerView;\r\n\r\n private predictionContext?: PredictionContext;\r\n\r\n private readonly hostDevice: DeviceSpec;\r\n\r\n private _inactiveBanner: Banner;\r\n\r\n private keyboard: Keyboard;\r\n private keyboardStub: KeyboardProperties;\r\n\r\n /**\r\n * Builds a banner for use when predictions are not active, supporting a single image.\r\n */\r\n public readonly ImageBanner = ImageBanner;\r\n\r\n /**\r\n * Builds a banner for use when predictions are not active, supporting a more generalized\r\n * content pattern than ImageBanner via `innerHTML` specifications.\r\n */\r\n public readonly HTMLBanner = HTMLBanner;\r\n\r\n constructor(bannerView: BannerView, hostDevice: DeviceSpec, predictionContext?: PredictionContext) {\r\n // Step 1 - establish the container element. Must come before this.setOptions.\r\n this.hostDevice = hostDevice;\r\n this.container = bannerView;\r\n this.predictionContext = predictionContext;\r\n\r\n this.inactiveBanner = new BlankBanner();\r\n }\r\n\r\n /**\r\n * Specifies the `Banner` instance to use when predictive-text is _not_ available to the user.\r\n *\r\n * Defaults to a hidden, \"blank\" `Banner` if not otherwise specified. Changes to its value\r\n * when predictive-text is not active will result in banner hot-swapping.\r\n *\r\n * The assigned instance will persist until directly changed through a new assignment,\r\n * regardless of any keyboard swaps and/or activations of the suggestion banner that may\r\n * occur in the meantime.\r\n */\r\n public get inactiveBanner() {\r\n return this._inactiveBanner;\r\n }\r\n\r\n public set inactiveBanner(banner: Banner) {\r\n this._inactiveBanner = banner ?? new BlankBanner();\r\n\r\n if(!(this.container.banner instanceof SuggestionBanner)) {\r\n this.container.banner = this._inactiveBanner;\r\n }\r\n }\r\n\r\n /**\r\n * Sets the active `Banner` to match the specified state for predictive text.\r\n *\r\n * @param on Whether prediction is active (`true`) or disabled (`false`).\r\n */\r\n public activateBanner(on: boolean) {\r\n const oldBanner = this.container.banner;\r\n if(oldBanner instanceof SuggestionBanner) {\r\n // Frees all handlers, etc registered previously by the banner.\r\n oldBanner.predictionContext = null;\r\n }\r\n\r\n if(!on) {\r\n this.container.banner = this.inactiveBanner;\r\n } else {\r\n let suggestBanner = new SuggestionBanner(this.hostDevice, this.container.activeBannerHeight);\r\n suggestBanner.predictionContext = this.predictionContext;\r\n\r\n // Registers for prediction-engine events & handles its needed connections.\r\n this.container.banner = suggestBanner;\r\n }\r\n }\r\n\r\n /**\r\n * Handles `LanguageProcessor`'s `'statechange'` events,\r\n * allowing logic to automatically hot-swap `Banner`s as needed.\r\n * @param state\r\n */\r\n selectBanner(state: StateChangeEnum) {\r\n // Only display a SuggestionBanner when LanguageProcessor states it is active.\r\n this.activateBanner(state == 'active' || state == 'configured');\r\n\r\n if(this.keyboard) {\r\n this.container.banner.configureForKeyboard(this.keyboard, this.keyboardStub);\r\n }\r\n }\r\n\r\n /**\r\n * Allows banners to adapt based on the active keyboard and related properties, such as\r\n * associated fonts.\r\n * @param keyboard\r\n * @param keyboardProperties\r\n */\r\n public configureForKeyboard(keyboard: Keyboard, keyboardProperties: KeyboardProperties) {\r\n this.keyboard = keyboard;\r\n this.keyboardStub = keyboardProperties;\r\n\r\n this.container.banner.configureForKeyboard(keyboard, keyboardProperties);\r\n }\r\n\r\n public shutdown() {\r\n if(this.container.banner instanceof SuggestionBanner) {\r\n this.container.banner.predictionContext = null;\r\n }\r\n }\r\n}", + "import KeyboardView from \"./keyboardView.interface.js\";\r\nimport { ParsedLengthStyle } from \"../lengthStyle.js\";\r\n\r\nexport default class EmptyView implements KeyboardView {\r\n readonly element: HTMLDivElement;\r\n\r\n constructor() {\r\n let Ldiv = this.element = document.createElement('div');\r\n Ldiv.style.userSelect = 'none';\r\n Ldiv.className='kmw-osk-none';\r\n }\r\n\r\n // No operations needed; this is a stand-in for the desktop OSK when no keyboard is active.\r\n public postInsert() { }\r\n public updateState() { }\r\n\r\n public refreshLayout() { }\r\n\r\n public get layoutHeight(): ParsedLengthStyle {\r\n return ParsedLengthStyle.inPixels(0);\r\n }\r\n}", + "import { Keyboard } from 'keyman/engine/keyboard';\r\n\r\nimport KeyboardView from './keyboardView.interface.js';\r\nimport { ParsedLengthStyle } from \"../lengthStyle.js\";\r\n\r\nexport default class HelpPageView implements KeyboardView {\r\n private readonly kbd: Keyboard;\r\n public readonly element: HTMLDivElement;\r\n\r\n private static readonly ID = 'kmw-osk-help-page';\r\n\r\n constructor(keyboard: Keyboard) {\r\n this.kbd = keyboard;\r\n\r\n var Ldiv = this.element = document.createElement('div');\r\n Ldiv.style.userSelect = \"none\";\r\n Ldiv.className = 'kmw-osk-static';\r\n Ldiv.id = HelpPageView.ID;\r\n Ldiv.innerHTML = keyboard.helpText;\r\n }\r\n\r\n public postInsert() {\r\n if(!this.element.parentElement || !document.getElementById(HelpPageView.ID)) {\r\n throw new Error(\"The HelpPage root element has not yet been inserted into the DOM.\");\r\n }\r\n\r\n if(this.kbd.hasScript) {\r\n // .parentElement: ensure this matches the _Box element from OSKManager / OSKView\r\n // Not a hard requirement for any known keyboards, but is asserted by legacy docs.\r\n this.kbd.embedScript(this.element.parentElement);\r\n }\r\n }\r\n\r\n public updateState() { }\r\n public refreshLayout() { }\r\n\r\n public get layoutHeight(): ParsedLengthStyle {\r\n return ParsedLengthStyle.inPercent(100);\r\n }\r\n}", + "import { ActiveKey, ActiveKeyBase, ActiveLayer, ActiveRow, Codes } from \"keyman/engine/keyboard\";\r\n\r\n/**\r\n * Defines correction-layout mappings for keys to be considered by\r\n * the fat-finger algorithm and its related calculations, which are\r\n * used to determine the \"closest keys\" for corrections.\r\n */\r\nexport interface CorrectionLayoutEntry {\r\n /**\r\n * The ID of the key corresponding to this entry.\r\n */\r\n readonly keySpec: ActiveKeyBase;\r\n\r\n /**\r\n * Represents the center x coordinate of the key based on the coordinate system\r\n * with the keyboard's layout bounding box mapped to a box from <0, 0> to <1, 1>.\r\n */\r\n readonly centerX: number;\r\n\r\n /**\r\n * Represents the center y coordinate of the key based on the coordinate system\r\n * with the keyboard's layout bounding box mapped to a box from <0, 0> to <1, 1>.\r\n */\r\n readonly centerY: number;\r\n\r\n /**\r\n * Represents the key's width based on the coordinate system with the\r\n * keyboard's layout bounding box mapped to a box from <0, 0> to <1, 1>.\r\n */\r\n readonly width: number;\r\n\r\n /**\r\n * Represents the key's height based on the coordinate system with the\r\n * keyboard's layout bounding box mapped to a box from <0, 0> to <1, 1>.\r\n */\r\n readonly height: number;\r\n}\r\n\r\nexport interface CorrectionLayout {\r\n /**\r\n * Defines the mappings of each key to be considered by a key-correction\r\n * algorithm. The key's bounding box should be defined relative to its\r\n * container's bounding box, with both mapped to a coordinate system from\r\n * <0, 0> to <1, 1> - a unit square.\r\n */\r\n keys: CorrectionLayoutEntry[];\r\n\r\n /**\r\n * The ratio of the keyboard's horizontal scale to its vertical scale.\r\n * For a 400 x 200 keyboard, should be 2.\r\n */\r\n kbdScaleRatio: number;\r\n}\r\n\r\n// Not compatible with subkeys - their layout data is only determined (presently) at runtime.\r\nexport class CorrectiveBaseKeyLayout implements CorrectionLayoutEntry {\r\n readonly keySpec: ActiveKey;\r\n readonly centerX: number;\r\n readonly centerY: number;\r\n readonly width: number;\r\n readonly height: number;\r\n\r\n constructor(layer: ActiveLayer, row: ActiveRow, key: ActiveKey) {\r\n this.keySpec = key;\r\n this.centerX = key.proportionalX;\r\n this.centerY = row.proportionalY;\r\n this.width = key.proportionalWidth;\r\n this.height = layer.rowProportionalHeight;\r\n }\r\n}\r\n\r\n/**\r\n * Indicates whether or not the specified key should be considered as a valid\r\n * key-correction target during fat-finger operations.\r\n * @param key\r\n * @returns `true` if valid, `false` if invalid.\r\n */\r\nexport function correctionKeyFilter(key: ActiveKeyBase): boolean {\r\n // If the key lacks an ID, just skip it. Sometimes used for padding.\r\n if(!key.baseKeyID) {\r\n return false;\r\n // Attempt to filter out known non-output keys.\r\n // Results in a more optimized distribution.\r\n } else if(Codes.isFrameKey(key.baseKeyID)) {\r\n return false;\r\n } else if(key.isPadding) { // to the user, blank / padding keys do not exist.\r\n return false;\r\n } else {\r\n return true;\r\n }\r\n}\r\n\r\n\r\n/**\r\n * Builds the corrective layout object corresponding to the specified keyboard layer,\r\n * as needed for use of our key-correction algorithms.\r\n *\r\n * @param layer The layer spec to reference for key corrections.\r\n * @param kbdScaleRatio The ratio of the keyboard's horizontal scale to its vertical scale.\r\n * For a 400 x 200 keyboard, should be 2.\r\n */\r\nexport function buildCorrectiveLayout(layer: ActiveLayer, kbdScaleRatio: number) {\r\n return {\r\n keys: layer.row.map((row) => {\r\n return row.key.map((key) => new CorrectiveBaseKeyLayout(layer, row, key));\r\n // ... and flatten/merge the resulting arrays.\r\n }).reduce((flattened, rowEntries) => flattened.concat(rowEntries), [])\r\n .filter((entry) => correctionKeyFilter(entry.keySpec)),\r\n kbdScaleRatio: kbdScaleRatio\r\n };\r\n}", + "// Defines the PUA code mapping for the various 'special' modifier/control/non-printing keys on keyboards.\r\n// `specialCharacters` must be kept in sync with the same variable in constants.js. See also CompileKeymanWeb.pas: CSpecialText10\r\nlet specialCharacters = {\r\n '*Shift*': 8,\r\n '*Enter*': 5,\r\n '*Tab*': 6,\r\n '*BkSp*': 4,\r\n '*Menu*': 11,\r\n '*Hide*': 10,\r\n '*Alt*': 25,\r\n '*Ctrl*': 1,\r\n '*Caps*': 3,\r\n '*ABC*': 16,\r\n '*abc*': 17,\r\n '*123*': 19,\r\n '*Symbol*': 21,\r\n '*Currency*': 20,\r\n '*Shifted*': 9,\r\n '*AltGr*': 2,\r\n '*TabLeft*': 7,\r\n '*LAlt*': 0x56,\r\n '*RAlt*': 0x57,\r\n '*LCtrl*': 0x58,\r\n '*RCtrl*': 0x59,\r\n '*LAltCtrl*': 0x60,\r\n '*RAltCtrl*': 0x61,\r\n '*LAltCtrlShift*': 0x62,\r\n '*RAltCtrlShift*': 0x63,\r\n '*AltShift*': 0x64,\r\n '*CtrlShift*': 0x65,\r\n '*AltCtrlShift*': 0x66,\r\n '*LAltShift*': 0x67,\r\n '*RAltShift*': 0x68,\r\n '*LCtrlShift*': 0x69,\r\n '*RCtrlShift*': 0x70,\r\n // Added in Keyman 14.0.\r\n '*LTREnter*': 0x05, // Default alias of '*Enter*'.\r\n '*LTRBkSp*': 0x04, // Default alias of '*BkSp*'.\r\n '*RTLEnter*': 0x71,\r\n '*RTLBkSp*': 0x72,\r\n '*ShiftLock*': 0x73,\r\n '*ShiftedLock*': 0x74,\r\n '*ZWNJ*': 0x75, // If this one is specified, auto-detection will kick in.\r\n '*ZWNJiOS*': 0x75, // The iOS version will be used by default, but the\r\n '*ZWNJAndroid*': 0x76, // Android platform has its own default glyph.\r\n // Added in Keyman 17.0.\r\n // Reference: https://github.com/silnrsi/font-symchar/blob/v4.000/documentation/encoding.md\r\n '*ZWNJGeneric*': 0x79, // Generic version of ZWNJ (no override)\r\n '*Sp*': 0x80, // Space\r\n '*NBSp*': 0x82, // No-break Space\r\n '*NarNBSp*': 0x83, // Narrow No-break Space\r\n '*EnQ*': 0x84, // En Quad\r\n '*EmQ*': 0x85, // Em Quad\r\n '*EnSp*': 0x86, // En Space\r\n '*EmSp*': 0x87, // Em Space\r\n // TODO: Skipping #-per-em-space\r\n '*PunctSp*': 0x8c, // Punctuation Space\r\n '*ThSp*': 0x8d, // Thin Space\r\n '*HSp*': 0x8e, // Hair Space\r\n '*ZWSp*': 0x81, // Zero Width Space\r\n '*ZWJ*': 0x77, // Zero Width Joiner\r\n '*WJ*': 0x78, // Word Joiner\r\n '*CGJ*': 0x7a, // Combining Grapheme Joiner\r\n '*LTRM*': 0x90, // Left-to-right Mark\r\n '*RTLM*': 0x91, // Right-to-left Mark\r\n '*SH*': 0xa1, // Soft Hyphen\r\n '*HTab*': 0xa2, // Horizontal Tabulation\r\n // TODO: Skipping size references\r\n\r\n};\r\n\r\nexport default specialCharacters;", + "\r\n/**\r\n * Maps 'sp' properties on a touch-layout spec to their corresponding CSS class names.\r\n */\r\nlet BUTTON_CLASSES = [\r\n 'default',\r\n 'shift',\r\n 'shift-on',\r\n 'special',\r\n 'special-on',\r\n '', // Key classes 5 through 7 are reserved for future use.\r\n '',\r\n '',\r\n 'deadkey',\r\n 'blank',\r\n 'hidden'\r\n];\r\n\r\nexport default BUTTON_CLASSES;", + "import { ActiveKey, ActiveSubKey, ButtonClass, ButtonClasses, DeviceSpec } from 'keyman/engine/keyboard';\r\n\r\n// At present, we don't use @keymanapp/keyman. Just `keyman`. (Refer to /web/package.json.)\r\nimport specialChars from '../specialCharacters.js';\r\nimport buttonClassNames from '../buttonClassNames.js';\r\n\r\nimport { KeyElement } from '../keyElement.js';\r\nimport VisualKeyboard from '../visualKeyboard.js';\r\nimport { getTextMetrics } from './getTextMetrics.js';\r\nimport { ParsedLengthStyle } from '../lengthStyle.js';\r\n\r\nexport interface KeyLayoutParams {\r\n keyWidth: number;\r\n keyHeight: number;\r\n baseEmFontSize: ParsedLengthStyle;\r\n layoutFontSize: ParsedLengthStyle;\r\n}\r\n\r\n/**\r\n * Replace default key names by special font codes for modifier keys\r\n *\r\n * @param {string} oldText\r\n * @return {string}\r\n **/\r\nexport function renameSpecialKey(oldText: string, vkbd: VisualKeyboard): string {\r\n // If a 'special key' mapping exists for the text, replace it with its corresponding special OSK character.\r\n switch(oldText) {\r\n case '*ZWNJ*':\r\n // Default ZWNJ symbol comes from iOS. We'd rather match the system defaults where\r\n // possible / available though, and there's a different standard symbol on Android.\r\n oldText = vkbd.device.OS == DeviceSpec.OperatingSystem.Android ?\r\n '*ZWNJAndroid*' :\r\n '*ZWNJiOS*';\r\n break;\r\n case '*Enter*':\r\n oldText = vkbd.isRTL ? '*RTLEnter*' : '*LTREnter*';\r\n break;\r\n case '*BkSp*':\r\n oldText = vkbd.isRTL ? '*RTLBkSp*' : '*LTRBkSp*';\r\n break;\r\n default:\r\n // do nothing.\r\n }\r\n\r\n const specialCode = specialChars[oldText as keyof typeof specialChars];\r\n let specialCodePUA = 0XE000 + specialCode;\r\n\r\n return specialCode ?\r\n String.fromCharCode(specialCodePUA) :\r\n oldText;\r\n}\r\n\r\nexport default abstract class OSKKey {\r\n // Only set here to act as an alias for code built against legacy versions.\r\n static readonly specialCharacters = specialChars;\r\n\r\n static readonly BUTTON_CLASSES = buttonClassNames;\r\n\r\n static readonly HIGHLIGHT_CLASS = 'kmw-key-touched';\r\n readonly spec: ActiveKey | ActiveSubKey;\r\n\r\n btn: KeyElement;\r\n label: HTMLSpanElement;\r\n square: HTMLDivElement;\r\n\r\n private _fontSize: ParsedLengthStyle;\r\n private _fontFamily: string;\r\n\r\n /**\r\n * The layer of the OSK on which the key is displayed.\r\n */\r\n readonly layer: string;\r\n\r\n constructor(spec: ActiveKey | ActiveSubKey, layer: string) {\r\n this.spec = spec;\r\n this.layer = layer;\r\n }\r\n\r\n abstract getId(): string;\r\n\r\n /**\r\n * Attach appropriate class to each key button, according to the layout\r\n *\r\n * @param {Object=} layout source layout description (optional, sometimes)\r\n */\r\n public setButtonClass() {\r\n let key = this.spec;\r\n let btn = this.btn;\r\n\r\n var n=0;\r\n // @ts-ignore // (Probably) supports legacy KMW keyboards that predate the sp entry\r\n if(typeof key['dk'] == 'string' && key['dk'] == '1') {\r\n n=8;\r\n }\r\n\r\n n = key['sp'] ?? n;\r\n\r\n if(n < 0 || n > 10) {\r\n n=0;\r\n }\r\n\r\n btn.className='kmw-key kmw-key-'+ buttonClassNames[n];\r\n }\r\n\r\n /**\r\n * For keys with button classes that support toggle states, this method\r\n * may be used to toggle which state the key's button class is in.\r\n * - shift <=> shift-on\r\n * - special <=> special-on\r\n * @param {boolean=} flag The new toggle state\r\n */\r\n public setToggleState(flag?: boolean) {\r\n let btnClassId: number;\r\n\r\n btnClassId = this.spec['sp'];\r\n\r\n // 1 + 2: shift + shift-on\r\n // 3 + 4: special + special-on\r\n switch(buttonClassNames[btnClassId]) {\r\n case 'shift':\r\n case 'shift-on':\r\n if(flag === undefined) {\r\n flag = buttonClassNames[btnClassId] == 'shift';\r\n }\r\n\r\n this.spec['sp'] = 1 + (flag ? 1 : 0) as ButtonClass;\r\n break;\r\n // Added in 15.0: special key highlight toggling.\r\n // Was _intended_ in earlier versions, but not actually implemented.\r\n case 'special':\r\n case 'special-on':\r\n if(flag === undefined) {\r\n flag = buttonClassNames[btnClassId] == 'special';\r\n }\r\n\r\n this.spec['sp'] = 3 + (flag ? 1 : 0) as ButtonClass;\r\n break;\r\n default:\r\n return;\r\n }\r\n\r\n this.setButtonClass();\r\n }\r\n\r\n // \"Frame key\" - generally refers to non-linguistic keys on the keyboard\r\n public isFrameKey(): boolean {\r\n let classIndex = this.spec['sp'] || 0;\r\n switch(buttonClassNames[classIndex]) {\r\n case 'default':\r\n case 'deadkey':\r\n // Note: will (generally) include the spacebar.\r\n return false;\r\n default:\r\n return true;\r\n }\r\n }\r\n\r\n public allowsKeyTip(): boolean {\r\n if(this.isFrameKey()) {\r\n return false;\r\n } else {\r\n return !this.btn.classList.contains('kmw-spacebar');\r\n }\r\n }\r\n\r\n public highlight(on: boolean) {\r\n var classes=this.btn.classList;\r\n\r\n if(on) {\r\n if(!classes.contains(OSKKey.HIGHLIGHT_CLASS)) {\r\n classes.add(OSKKey.HIGHLIGHT_CLASS);\r\n }\r\n } else {\r\n classes.remove(OSKKey.HIGHLIGHT_CLASS);\r\n }\r\n }\r\n\r\n /**\r\n * Calculate the font size required for a key cap, scaling to fit longer text\r\n * @param text\r\n * @param layoutParams specification for the key\r\n * @param scale additional scaling to apply for the font-size calculation (used by keytips)\r\n * @returns font size as a style string\r\n */\r\n getIdealFontSize(text: string, layoutParams: KeyLayoutParams, scale?: number): ParsedLengthStyle {\r\n // Fallback in case not all style info is currently ready.\r\n if(!this._fontFamily) {\r\n return new ParsedLengthStyle('1em');\r\n }\r\n\r\n scale ??= 1;\r\n\r\n const keyWidth = layoutParams.keyWidth;\r\n const keyHeight = layoutParams.keyHeight;\r\n const emScale = layoutParams.baseEmFontSize.scaledBy(layoutParams.layoutFontSize.val);\r\n\r\n // Among other things, ensures we use SpecialOSK styling for special key text.\r\n // It's set on the key-span, not on the button.\r\n //\r\n // Also helps ensure that the stub's font-family name is used for keys, should\r\n // that mismatch the font-family name specified within the keyboard's touch layout.\r\n\r\n let originalSize = this._fontSize;\r\n if(!originalSize.absolute) {\r\n originalSize = emScale.scaledBy(originalSize.val);\r\n }\r\n\r\n const style = {\r\n fontFamily: this._fontFamily,\r\n fontSize: originalSize.styleString,\r\n height: layoutParams.keyHeight\r\n }\r\n\r\n let metrics = getTextMetrics(text, emScale.scaledBy(scale).val, style);\r\n\r\n const MAX_X_PROPORTION = 0.90;\r\n const MAX_Y_PROPORTION = 0.90;\r\n const X_PADDING = 2;\r\n\r\n var fontHeight: number;\r\n if(metrics.fontBoundingBoxAscent) {\r\n fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;\r\n }\r\n\r\n // Don't add extra padding to height - multiplying with MAX_Y_PROPORTION already gives\r\n // padding\r\n let textHeight = fontHeight ?? 0;\r\n let xProportion = (keyWidth * MAX_X_PROPORTION) / (metrics.width + X_PADDING); // How much of the key does the text want to take?\r\n let yProportion = textHeight && keyHeight ? (keyHeight * MAX_Y_PROPORTION) / textHeight : undefined;\r\n\r\n var proportion: number = xProportion;\r\n if(yProportion && yProportion < xProportion) {\r\n proportion = yProportion;\r\n }\r\n\r\n // Never upscale keys past the default * the specified scale - only downscale them.\r\n // Proportion < 1: ratio of key width to (padded [loosely speaking]) text width\r\n // maxProportion determines the 'padding' involved.\r\n return ParsedLengthStyle.forScalar(scale * Math.min(proportion, 1));\r\n }\r\n\r\n public get keyText(): string {\r\n const spec = this.spec;\r\n const DEFAULT_BLANK = '\\xa0';\r\n\r\n // Add OSK key labels\r\n let keyText = null;\r\n if(spec['text'] == null || spec['text'] == '') {\r\n // U_ codes are handled during keyboard pre-processing.\r\n keyText = DEFAULT_BLANK;\r\n } else {\r\n keyText=spec['text'];\r\n\r\n // Unique layer-based transformation: SHIFT-TAB uses a different glyph.\r\n if(keyText == '*Tab*' && this.layer == 'shift') {\r\n keyText = '*TabLeft*';\r\n }\r\n }\r\n\r\n return keyText;\r\n }\r\n\r\n // Produces a HTMLSpanElement with the key's actual text.\r\n protected generateKeyText(vkbd: VisualKeyboard): HTMLSpanElement {\r\n const spec = this.spec;\r\n\r\n let t = document.createElement('span'), ts=t.style;\r\n t.className='kmw-key-text';\r\n\r\n // Add OSK key labels\r\n let keyText = this.keyText;\r\n let specialText = renameSpecialKey(keyText, vkbd);\r\n if(specialText != keyText) {\r\n // The keyboard wants to use the code for a special glyph defined by the SpecialOSK font.\r\n keyText = specialText;\r\n spec['font'] = \"SpecialOSK\";\r\n }\r\n\r\n //Override font spec if set for this key in the layout\r\n if(typeof spec['font'] == 'string' && spec['font'] != '') {\r\n ts.fontFamily=spec['font'];\r\n }\r\n\r\n if(typeof spec['fontsize'] == 'string' && spec['fontsize'] != '') {\r\n ts.fontSize=spec['fontsize'];\r\n }\r\n\r\n // For some reason, fonts will sometimes 'bug out' for the embedded iOS page if we\r\n // instead assign fontFamily to the existing style 'ts'. (Occurs in iOS 12.)\r\n let styleSpec: {fontFamily?: string, fontSize: string} = {fontSize: ts.fontSize};\r\n\r\n if(ts.fontFamily) {\r\n styleSpec.fontFamily = ts.fontFamily;\r\n } else {\r\n styleSpec.fontFamily = vkbd.fontFamily; // Helps with style sheet calculations.\r\n }\r\n\r\n // Check the key's display width - does the key visualize well?\r\n if(vkbd.isRTL) {\r\n // Add the RTL marker to ensure it displays properly.\r\n keyText = '\\u200f' + keyText;\r\n }\r\n\r\n // Finalize the key's text.\r\n t.innerText = keyText;\r\n\r\n return t;\r\n }\r\n\r\n public resetFontPrecalc() {\r\n this._fontFamily = undefined;\r\n this._fontSize = undefined;\r\n this.label.style.fontSize = '';\r\n }\r\n\r\n /**\r\n * Any style-caching behavior needed for use in layout manipulation should be\r\n * computed within this method, not within refreshLayout. This is to prevent\r\n * unnecessary layout-reflow.\r\n * @param layoutParams\r\n * @returns\r\n */\r\n public detectStyles(layoutParams: KeyLayoutParams): void {\r\n // Avoid doing any font-size related calculations if there's no text to display.\r\n if(this.spec.sp == ButtonClasses.spacer || this.spec.sp == ButtonClasses.blank) {\r\n return;\r\n }\r\n\r\n // Attempt to detect static but key-specific style properties if they haven't yet\r\n // been detected.\r\n if(this._fontFamily === undefined) {\r\n const lblStyle = getComputedStyle(this.label);\r\n\r\n // Abort if the element is not currently in the DOM; we can't get any info this way.\r\n if(!lblStyle.fontFamily) {\r\n return;\r\n }\r\n this._fontFamily = lblStyle.fontFamily;\r\n\r\n // Detect any difference in base (em) font size and that which is computed for the key itself.\r\n const computedFontSize = new ParsedLengthStyle(lblStyle.fontSize);\r\n const layoutFontSize = layoutParams.layoutFontSize;\r\n if(layoutFontSize.absolute) {\r\n // rather than just straight-up taking .layoutFontSize\r\n this._fontSize = computedFontSize;\r\n } else {\r\n const baseEmFontSize = layoutParams.baseEmFontSize;\r\n const baseFontSize = layoutFontSize.scaledBy(baseEmFontSize.val);\r\n const localFontScaling = computedFontSize.val / baseFontSize.val;\r\n this._fontSize = ParsedLengthStyle.forScalar(localFontScaling);\r\n }\r\n }\r\n }\r\n\r\n // Avoid any references to getComputedStyle, offset_, or other layout-reflow\r\n // dependent values. Refer to https://gist.github.com/paulirish/5d52fb081b3570c81e3a.\r\n public refreshLayout(layoutParams: KeyLayoutParams) {\r\n // space bar may not define the text span!\r\n if(this.label) {\r\n if(!this.label.classList.contains('kmw-spacebar-caption')) {\r\n // Do not use `this.keyText` - it holds *___* codes for special keys, not the actual glyph!\r\n const keyCapText = this.label.textContent;\r\n const fontSize = this.getIdealFontSize(keyCapText, layoutParams);\r\n this.label.style.fontSize = fontSize.styleString;\r\n } else {\r\n // Spacebar text, on the other hand, is available via this.keyText.\r\n // Using this field helps prevent layout reflow during updates.\r\n const fontSize = this.getIdealFontSize(this.keyText, layoutParams);\r\n\r\n // Since the kmw-spacebar-caption version uses !important, we must specify\r\n // it directly on the element too; otherwise, scaling gets ignored.\r\n this.label.style.setProperty(\"font-size\", fontSize.styleString, \"important\");\r\n }\r\n }\r\n }\r\n}\r\n", + "import { ActiveSubKey } from 'keyman/engine/keyboard';\r\nimport OSKKey from \"./keyboard-layout/oskKey.js\";\r\n\r\nexport class KeyData {\r\n ['key']: OSKKey;\r\n ['keyId']: string;\r\n ['subKeys']?: ActiveSubKey[];\r\n\r\n constructor(keyData: OSKKey, keyId: string) {\r\n this['key'] = keyData;\r\n this['keyId'] = keyId;\r\n }\r\n}\r\n\r\nexport type KeyElement = HTMLDivElement & KeyData;\r\n\r\n// Many thanks to https://www.typescriptlang.org/docs/handbook/advanced-types.html for this.\r\nexport function link(elem: HTMLDivElement, data: KeyData): KeyElement {\r\n let e = elem;\r\n\r\n // Merges all properties and methods of KeyData onto the underlying HTMLDivElement, creating a merged class.\r\n for(let id in data) {\r\n if(!e.hasOwnProperty(id)) {\r\n (e)[id] = (data)[id];\r\n }\r\n }\r\n\r\n return e;\r\n}\r\n\r\nexport function isKey(elem: Node): boolean {\r\n return elem && ('key' in elem) && (( elem['key']) instanceof OSKKey);\r\n}\r\n\r\nexport function getKeyFrom(elem: Node): KeyElement {\r\n if(isKey(elem)) {\r\n return elem;\r\n } else {\r\n return null;\r\n }\r\n}", + "import { ActiveKey, Codes } from 'keyman/engine/keyboard';\r\n\r\nimport OSKKey, { KeyLayoutParams, renameSpecialKey } from './oskKey.js';\r\nimport { KeyData, KeyElement, link } from '../keyElement.js';\r\nimport OSKRow from './oskRow.js';\r\nimport VisualKeyboard from '../visualKeyboard.js';\r\nimport { ParsedLengthStyle } from '../lengthStyle.js';\r\nimport { GesturePreviewHost } from './gesturePreviewHost.js';\r\n\r\nexport default class OSKBaseKey extends OSKKey {\r\n private capLabel: HTMLDivElement;\r\n private previewHost: GesturePreviewHost;\r\n private preview: HTMLDivElement;\r\n\r\n public readonly row: OSKRow;\r\n\r\n constructor(spec: ActiveKey, layer: string, row: OSKRow) {\r\n super(spec, layer);\r\n this.row = row;\r\n }\r\n\r\n getId(): string {\r\n // Define each key element id by layer id and key id (duplicate possible for SHIFT - does it matter?)\r\n return this.spec.elementID;\r\n }\r\n\r\n getCoreId(): string {\r\n return this.spec.coreID;\r\n }\r\n\r\n getBaseId(): string {\r\n return this.spec.baseKeyID;\r\n }\r\n\r\n // Produces a small reference label for the corresponding physical key on a US keyboard.\r\n private generateKeyCapLabel(): HTMLDivElement {\r\n // Create the default key cap labels (letter keys, etc.)\r\n var x = Codes.keyCodes[this.spec.baseKeyID];\r\n switch(x) {\r\n // Converts the keyman key id code for common symbol keys into its representative ASCII code.\r\n // K_COLON -> K_BKQUOTE\r\n case 186: x=59; break;\r\n case 187: x=61; break;\r\n case 188: x=44; break;\r\n case 189: x=45; break;\r\n case 190: x=46; break;\r\n case 191: x=47; break;\r\n case 192: x=96; break;\r\n // K_LBRKT -> K_QUOTE\r\n case 219: x=91; break;\r\n case 220: x=92; break;\r\n case 221: x=93; break;\r\n case 222: x=39; break;\r\n default:\r\n // No other symbol character represents a base key on the standard QWERTY English layout.\r\n if(x < 48 || x > 90) {\r\n x=0;\r\n }\r\n }\r\n\r\n let q = document.createElement('div');\r\n q.className='kmw-key-label';\r\n if(x > 0) {\r\n q.innerText=String.fromCharCode(x);\r\n } else {\r\n // Keyman-only virtual keys have no corresponding physical key.\r\n // So, no text for the key-cap.\r\n }\r\n return q;\r\n }\r\n\r\n private processSubkeys(btn: KeyElement, vkbd: VisualKeyboard) {\r\n // Add reference to subkey array if defined\r\n var bsn: number, bsk=btn['subKeys'] = this.spec['sk'];\r\n // Transform any special keys into their PUA representations.\r\n for(bsn=0; bsn {\r\n this.setPreview(null);\r\n });\r\n\r\n this.btn.replaceChild(this.preview, oldPreview);\r\n }\r\n\r\n public refreshLayout(layoutParams: KeyLayoutParams) {\r\n super.refreshLayout(layoutParams); // key labels in particular.\r\n\r\n const emFont = layoutParams.baseEmFontSize;\r\n // Rescale keycap labels on small phones\r\n if(emFont.val < 12) {\r\n this.capLabel.style.fontSize = '6px';\r\n } else {\r\n // The default value set within kmwosk.css.\r\n this.capLabel.style.fontSize = ParsedLengthStyle.forScalar(0.5).styleString;\r\n }\r\n }\r\n\r\n public get displaysKeyCap(): boolean {\r\n return this.capLabel && this.capLabel.style.display == 'block';\r\n }\r\n\r\n public set displaysKeyCap(flag: boolean) {\r\n if(!this.capLabel) {\r\n throw new Error(\"Key element not yet constructed; cannot display key cap\");\r\n }\r\n this.capLabel.style.display = flag ? 'block' : 'none';\r\n }\r\n}\r\n", + "import { ActiveKey, ActiveLayer, ActiveRow } from 'keyman/engine/keyboard';\r\n\r\nimport OSKBaseKey from './oskBaseKey.js';\r\nimport { ParsedLengthStyle } from '../lengthStyle.js';\r\nimport VisualKeyboard from '../visualKeyboard.js';\r\nimport OSKKey, { KeyLayoutParams } from './oskKey.js';\r\nimport { LayerLayoutParams } from './oskLayer.js';\r\n\r\n/*\r\n The total proportion of key-square height used as key-button padding.\r\n The 'padding' is visible to users as the vertical space between keys\r\n and exists both in \"fixed\" and \"absolute\" sizing modes.\r\n*/\r\nexport const KEY_BTN_Y_PAD_RATIO = 0.15;\r\n\r\n/**\r\n * Models one row of one layer of the OSK (`VisualKeyboard`) for a keyboard.\r\n */\r\nexport default class OSKRow {\r\n public readonly element: HTMLDivElement;\r\n public readonly keys: OSKBaseKey[];\r\n public readonly heightFraction: number;\r\n public readonly spec: ActiveRow;\r\n\r\n public constructor(vkbd: VisualKeyboard,\r\n layerSpec: ActiveLayer,\r\n rowSpec: ActiveRow) {\r\n const rDiv = this.element = document.createElement('div');\r\n rDiv.className='kmw-key-row';\r\n\r\n // Calculate default row height\r\n this.heightFraction = 1 / layerSpec.row.length;\r\n\r\n // Apply defaults, setting the width and other undefined properties for each key\r\n const keys=rowSpec.key;\r\n this.spec = rowSpec;\r\n this.keys = [];\r\n\r\n // Calculate actual key widths by multiplying by the OSK's width and rounding appropriately,\r\n // adjusting the width of the last key to make the total exactly 100%.\r\n // Overwrite the previously-computed percent.\r\n // NB: the 'percent' suffix is historical, units are percent on desktop devices, but pixels on touch devices\r\n // All key widths and paddings are rounded for uniformity\r\n for(let j=0; j 0) {\r\n return this.keys[0].displaysKeyCap;\r\n } else {\r\n return undefined;\r\n }\r\n }\r\n\r\n public set displaysKeyCaps(flag: boolean) {\r\n for(const key of this.keys) {\r\n key.displaysKeyCap = flag;\r\n }\r\n }\r\n\r\n // Avoid any references to getComputedStyle, offset_, or other layout-reflow\r\n // dependent values. Refer to https://gist.github.com/paulirish/5d52fb081b3570c81e3a.\r\n public refreshLayout(layoutParams: LayerLayoutParams) {\r\n const rs = this.element.style;\r\n\r\n const rowHeight = layoutParams.heightStyle.scaledBy(this.heightFraction);\r\n rs.maxHeight=rs.lineHeight=rs.height=rowHeight.styleString;\r\n\r\n const keyHeightBase = layoutParams.heightStyle.absolute ? rowHeight : ParsedLengthStyle.forScalar(1);\r\n const padTop = keyHeightBase.scaledBy(KEY_BTN_Y_PAD_RATIO / 2);\r\n const keyHeight = keyHeightBase.scaledBy(1 - KEY_BTN_Y_PAD_RATIO);\r\n\r\n // Update all key-square layouts.\r\n this.keys.forEach((key) => {\r\n const keySquare = key.square;\r\n const keyElement = key.btn;\r\n\r\n // Set the kmw-key-square position\r\n const kss = keySquare.style;\r\n kss.height=kss.minHeight=keyHeightBase.styleString;\r\n\r\n const kes = keyElement.style;\r\n kes.top = padTop.styleString;\r\n kes.height=kes.lineHeight=kes.minHeight=keyHeight.styleString;\r\n });\r\n }\r\n\r\n private buildKeyLayout(layoutParams: LayerLayoutParams, key: OSKKey) {\r\n // Calculate changes to be made...\r\n const keyWidth = layoutParams.widthStyle.scaledBy(key.spec.proportionalWidth);\r\n\r\n // We maintain key-btn padding within the key-square - the latter `scaledBy`\r\n // adjusts for that, providing the final key-btn height.\r\n const keyHeight = layoutParams.heightStyle.scaledBy(this.heightFraction).scaledBy(1 - KEY_BTN_Y_PAD_RATIO);\r\n\r\n const keyStyle: KeyLayoutParams = {\r\n keyWidth: keyWidth.val * (keyWidth.absolute ? 1 : layoutParams.keyboardWidth),\r\n keyHeight: keyHeight.val * (keyHeight.absolute ? 1 : layoutParams.keyboardHeight),\r\n baseEmFontSize: layoutParams.baseEmFontSize,\r\n layoutFontSize: layoutParams.layoutFontSize\r\n };\r\n\r\n return keyStyle;\r\n }\r\n\r\n /**\r\n * Any style-caching behavior needed for use in layout manipulation should be\r\n * computed within this method, not within refreshLayout. This is to prevent\r\n * unnecessary layout-reflow.\r\n * @param layoutParams\r\n * @returns\r\n */\r\n public detectStyles(layoutParams: LayerLayoutParams) {\r\n this.keys.forEach((key) => {\r\n key.detectStyles(this.buildKeyLayout(layoutParams, key));\r\n });\r\n }\r\n\r\n // Avoid any references to getComputedStyle, offset_, or other layout-reflow\r\n // dependent values. Refer to https://gist.github.com/paulirish/5d52fb081b3570c81e3a.\r\n public refreshKeyLayouts(layoutParams: LayerLayoutParams) {\r\n this.keys.forEach((key) => {\r\n // Calculate changes to be made...\r\n const keyElement = key.btn;\r\n\r\n const widthStyle = layoutParams.widthStyle;\r\n const heightStyle = layoutParams.heightStyle;\r\n\r\n const keyWidth = widthStyle.scaledBy(key.spec.proportionalWidth);\r\n const keyPad = widthStyle.scaledBy(key.spec.proportionalPad);\r\n\r\n // We maintain key-btn padding within the key-square - the latter `scaledBy`\r\n // adjusts for that, providing the final key-btn height.\r\n const keyHeight = heightStyle.scaledBy(this.heightFraction).scaledBy(1 - KEY_BTN_Y_PAD_RATIO);\r\n\r\n // Match the row height (if fixed-height) or use full row height (if percent-based)\r\n const styleHeight = heightStyle.absolute ? keyHeight.styleString : '100%';\r\n\r\n const keyStyle: KeyLayoutParams = this.buildKeyLayout(layoutParams, key);\r\n keyElement.key?.refreshLayout(keyStyle);\r\n\r\n key.square.style.width = keyWidth.styleString;\r\n key.square.style.marginLeft = keyPad.styleString;\r\n\r\n key.btn.style.width = widthStyle.absolute ? keyWidth.styleString : '100%';\r\n key.square.style.height = styleHeight;\r\n });\r\n }\r\n}", + "import { ActiveLayer, ActiveLayout } from 'keyman/engine/keyboard';\r\n\r\nimport OSKRow from './oskRow.js';\r\nimport OSKBaseKey from './oskBaseKey.js';\r\nimport VisualKeyboard from '../visualKeyboard.js';\r\nimport { ParsedLengthStyle } from '../lengthStyle.js';\r\n\r\nexport interface LayerLayoutParams {\r\n keyboardWidth: number;\r\n keyboardHeight: number;\r\n widthStyle: ParsedLengthStyle;\r\n heightStyle: ParsedLengthStyle;\r\n baseEmFontSize: ParsedLengthStyle;\r\n layoutFontSize: ParsedLengthStyle;\r\n spacebarText: string;\r\n}\r\nexport default class OSKLayer {\r\n public readonly element: HTMLDivElement;\r\n public readonly rows: OSKRow[];\r\n public readonly spec: ActiveLayer;\r\n public readonly nextlayer: string;\r\n\r\n public readonly globeKey: OSKBaseKey;\r\n public readonly spaceBarKey: OSKBaseKey;\r\n public readonly hideKey: OSKBaseKey;\r\n public readonly capsKey: OSKBaseKey;\r\n public readonly numKey: OSKBaseKey;\r\n public readonly scrollKey: OSKBaseKey;\r\n\r\n private _rowHeight: number;\r\n\r\n public get rowHeight(): number {\r\n return this._rowHeight;\r\n }\r\n\r\n public get id(): string {\r\n return this.spec.id;\r\n }\r\n\r\n public constructor(vkbd: VisualKeyboard,\r\n layout: ActiveLayout,\r\n layer: ActiveLayer) {\r\n this.spec = layer;\r\n\r\n const gDiv = this.element = document.createElement('div');\r\n const gs=gDiv.style;\r\n gDiv.className='kmw-key-layer';\r\n\r\n var nRows=layer['row'].length;\r\n if(nRows > 4 && vkbd.device.formFactor == 'phone') {\r\n gDiv.className = gDiv.className + ' kmw-5rows';\r\n }\r\n\r\n // Set font for layer if defined in layout\r\n gs.fontFamily = 'font' in layout ? layout['font'] : '';\r\n\r\n this.nextlayer = layer.id;\r\n //@ts-ignore\r\n gDiv['layer'] = layer.id;\r\n // @ts-ignore\r\n if(typeof layer['nextlayer'] == 'string') {\r\n // The gDiv['nextLayer'] is no longer referenced in KMW 15.0+, but is\r\n // maintained for partial back-compat in case any site devs actually\r\n // relied on its value from prior versions.\r\n //\r\n // We won't pay attention to any mutations to the gDiv copy, though.\r\n // @ts-ignore\r\n gDiv['nextLayer'] = this.nextlayer = layer['nextlayer'];\r\n }\r\n\r\n // Create a DIV for each row of the group\r\n let rows=layer['row'];\r\n this.rows = [];\r\n\r\n for(let i=0; i row.detectStyles(layoutParams));\r\n\r\n // Hereafter, avoid any references to getComputedStyle, offset_, or other\r\n // layout-reflow dependent values. Refer to\r\n // https://gist.github.com/paulirish/5d52fb081b3570c81e3a.\r\n\r\n // Check the heights of each row, in case different layers have different row counts.\r\n const layerHeight = layoutParams.keyboardHeight;\r\n const nRows = this.rows.length;\r\n const rowHeight = this._rowHeight = Math.floor(layerHeight/(nRows == 0 ? 1 : nRows));\r\n\r\n const usesFixedWidthScaling = layoutParams.widthStyle.absolute;\r\n if(usesFixedWidthScaling) {\r\n this.element.style.height=(layerHeight)+'px';\r\n }\r\n\r\n this.showLanguage(layoutParams.spacebarText);\r\n\r\n // Update row layout properties\r\n for(let nRow=0; nRow spec.id);\r\n\r\n for(const id of layerIds) {\r\n this.buildLayer(id);\r\n }\r\n\r\n return this._layers;\r\n }\r\n\r\n public get activeLayerId(): string {\r\n return this._activeLayerId;\r\n }\r\n\r\n public set activeLayerId(id: string) {\r\n this._activeLayerId = id;\r\n\r\n // Trigger construction of the layer if it does not already exist.\r\n this.getLayer(id);\r\n\r\n for (let key of Object.keys(this._layers)) {\r\n const layer = this._layers[key];\r\n const layerElement = layer.element;\r\n if (layer.id == id) {\r\n layerElement.style.display = 'block';\r\n } else {\r\n layerElement.style.display = 'none';\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * The core function referenced by the gesture engine for determining the key that\r\n * best matches the state of contact points and ongoing gestures.\r\n *\r\n * Calls to this function may temporarily change which layer is set for display,\r\n * as layout reflows are necessary for lookups in layers not currently set active.\r\n * Such changes layer will be reverted once the JS microtask queue regains control;\r\n * this delay is to prevent costly layout thrashing effects.\r\n * @param coord\r\n * @returns\r\n */\r\n findNearestKey(coord: Omit, 'item'>): KeyElement {\r\n if(!coord) {\r\n return null;\r\n }\r\n const layerId = coord.stateToken;\r\n if(!layerId) {\r\n throw new Error(`Layer id not set for input coordinate`);\r\n }\r\n\r\n const layer = this._layers[layerId];\r\n if(!layer) {\r\n throw new Error(`Layer id ${layerId} could not be found`);\r\n }\r\n\r\n return this.nearestKey(coord, layer);\r\n }\r\n\r\n /**\r\n * Temporarily enables the specified layer for page layout calculations and\r\n * queues an immediate reversion to the 'true' active layer at the earliest\r\n * opportunity on the JS microtask queue.\r\n * @param layer\r\n */\r\n public blinkLayer(arg: OSKLayer | string) {\r\n if(typeof arg === 'string') {\r\n const layerId = arg;\r\n arg = this.getLayer(layerId);\r\n if(!arg) {\r\n throw new Error(`Layer id ${layerId} could not be found`);\r\n }\r\n }\r\n\r\n const layer = arg as OSKLayer;\r\n\r\n // Note: we do NOT manipulate `._activeLayerId` here! This is designed\r\n // explicitly to be temporary.\r\n if(layer.element.style.display != 'block') {\r\n for(let id in this._layers) {\r\n if(this._layers[id].element.style.display == 'block') {\r\n const priorLayer = this._layers[id];\r\n priorLayer.element.style.display = 'none';\r\n }\r\n this._layers[id].element.style.display = 'none';\r\n }\r\n }\r\n layer.element.style.display = 'block';\r\n\r\n /* As soon as control returns to the JS microtask queue, restore the original layer.\r\n * We want to avoid doing it sooner in case another lookup occurs before the standard\r\n * async reflow, as that could trigger expensive \"layout thrashing\" effects.\r\n *\r\n * In the case that a gesture-source's path needs to be remapped to a different layer,\r\n * multiple synchronous calls to this method may occur. This is a pattern that may\r\n * result during input layer-remapping used to solve issues like #7173 and possibly\r\n * also during multitap operations.\r\n *\r\n * On \"layout thrashing\": https://webperf.tips/tip/layout-thrashing/\r\n */\r\n Promise.resolve().then(() => {\r\n const trueLayer = this._layers[this._activeLayerId];\r\n // If either condition holds, we have to trigger a layout reflow; it's the same cost\r\n // whether one changes or both do.\r\n if(layer.element.style.display == 'block' || trueLayer.element.style.display != 'block') {\r\n layer.element.style.display = 'none';\r\n trueLayer.element.style.display = 'block';\r\n }\r\n });\r\n }\r\n\r\n private nearestKey(coord: Omit, 'item'>, layer: OSKLayer): KeyElement {\r\n // If there are no rows, there are no keys; return instantly.\r\n if(layer.rows.length == 0) {\r\n return null;\r\n }\r\n\r\n // Our pre-processed layout info maps whatever shape the keyboard is in into a unit square.\r\n // So, we map our coord to find its location within that square.\r\n const proportionalCoords = {\r\n x: coord.targetX / this.computedWidth,\r\n y: coord.targetY / this.computedHeight\r\n };\r\n\r\n // If our computed width and/or height are 0, it's best to abort; key distance\r\n // calculations are not viable.\r\n if(!isFinite(proportionalCoords.x) || !isFinite(proportionalCoords.y)) {\r\n return null;\r\n }\r\n\r\n // Step 1: find the nearest row.\r\n // Rows aren't variable-height - this value is \"one size fits all.\"\r\n\r\n /*\r\n If 4 rows, y = .2 x 4 = .8 - still within the row with index 0 (spanning from 0 to .25)\r\n y = .6 x 4 = 2.4 - within row with index 2 (third row, spanning .5 to .75)\r\n\r\n Assumes there is no fine-tuning of the row ranges to be done - each takes a perfect\r\n fraction of the overall layer height without any padding above or below.\r\n */\r\n const rowIndex = Math.max(0, Math.min(layer.rows.length-1, Math.floor(proportionalCoords.y * layer.rows.length)));\r\n const row = layer.rows[rowIndex];\r\n\r\n // Assertion: row no longer `null`.\r\n // (We already prevented the no-rows available scenario, anyway.)\r\n\r\n // Step 2: Find minimum distance from any key\r\n // - If the coord is within a key's square, go ahead and return it.\r\n let closestKey: OSKBaseKey = null;\r\n // Is percentage-based!\r\n let minDistance = Number.MAX_VALUE;\r\n\r\n for (let key of row.keys) {\r\n const keySpec = key.spec;\r\n if(keySpec.sp == ButtonClasses.blank || keySpec.sp == ButtonClasses.spacer) {\r\n continue;\r\n }\r\n\r\n // Max distance from the key's center to consider, horizontally.\r\n const keyRadius = keySpec.proportionalWidth / 2;\r\n const distanceFromCenter = Math.abs(proportionalCoords.x - keySpec.proportionalX);\r\n\r\n // Find the actual key element.\r\n if(distanceFromCenter - keyRadius <= 0) {\r\n // As noted above: if we land within a key's square, match instantly!\r\n return key.btn;\r\n } else {\r\n const distance = distanceFromCenter - keyRadius;\r\n if(distance < minDistance) {\r\n minDistance = distance;\r\n closestKey = key;\r\n }\r\n }\r\n }\r\n\r\n /*\r\n Step 3: If the input coordinate wasn't within any valid key's \"square\",\r\n determine if the nearest valid key is acceptable - if it's within 60% of\r\n a standard key's width from the touch location.\r\n\r\n If the condition is not met, there are no valid keys within this row.\r\n */\r\n if (minDistance /* %age-based! */ <= NEAREST_KEY_TOUCH_MARGIN_PERCENT) {\r\n return closestKey.btn;\r\n }\r\n\r\n // Step 4: no matches => return null. The caller should be able to handle such cases,\r\n // anyway.\r\n return null;\r\n }\r\n\r\n public resetPrecalcFontSizes() {\r\n for(const layer of Object.values(this._layers)) {\r\n for(const row of layer.rows) {\r\n for(const key of row.keys) {\r\n key.resetFontPrecalc();\r\n }\r\n }\r\n }\r\n\r\n // This method is called whenever all related stylesheets are fully loaded and applied.\r\n // The actual padding data may not have been available until now.\r\n this._heightPadding = undefined;\r\n }\r\n\r\n public refreshLayout(layoutParams: LayerLayoutParams) {\r\n if(isNaN(layoutParams.keyboardWidth) || isNaN(layoutParams.keyboardHeight)) {\r\n // We're not in the DOM yet; we'll refresh properly once that changes.\r\n // Can be reached if the layerId is changed before the keyboard enters the DOM.\r\n return;\r\n }\r\n // Set layer-group copies of relevant computed-size values; they are used by nearest-key\r\n // detection.\r\n this.computedWidth = layoutParams.keyboardWidth;\r\n this.computedHeight = layoutParams.keyboardHeight;\r\n\r\n // Assumption: this styling value will not change once the keyboard and\r\n // related stylesheets are loaded and applied.\r\n if(this._heightPadding === undefined) {\r\n // Should not trigger a new layout reflow; VisualKeyboard should have made no further DOM\r\n // style changes since the last one.\r\n\r\n // For touch-based OSK layouts, kmwosk.css may include top & bottom\r\n // padding on the layer-group element.\r\n const computedGroupStyle = getComputedStyle(this.element);\r\n\r\n // parseInt('') => NaN, which is falsy; we want to fallback to zero.\r\n let pt = parseInt(computedGroupStyle.paddingTop, 10) || 0;\r\n let pb = parseInt(computedGroupStyle.paddingBottom, 10) || 0;\r\n this._heightPadding = pt + pb;\r\n }\r\n\r\n if(this.activeLayer) {\r\n this.activeLayer.refreshLayout(layoutParams);\r\n }\r\n }\r\n\r\n public get verticalPadding() {\r\n return this._heightPadding ?? 0;\r\n }\r\n}", + "import OSKBaseKey from '../../../keyboard-layout/oskBaseKey.js';\r\nimport { KeyElement } from '../../../keyElement.js';\r\nimport KeyTipInterface from '../../../keytip.interface.js';\r\nimport VisualKeyboard from '../../../visualKeyboard.js';\r\nimport { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js';\r\nimport { ParsedLengthStyle } from '../../../lengthStyle.js';\r\n\r\nconst CSS_PREFIX = 'kmw-';\r\nconst DEFAULT_TIP_ORIENTATION: PhoneKeyTipOrientation = 'top';\r\n\r\nexport type PhoneKeyTipOrientation = 'top' | 'bottom';\r\n\r\nexport default class KeyTip implements KeyTipInterface {\r\n public readonly element: HTMLDivElement;\r\n public key: KeyElement;\r\n public state: boolean = false;\r\n\r\n private orientation: PhoneKeyTipOrientation = DEFAULT_TIP_ORIENTATION;\r\n\r\n // -----\r\n // | | <-- tip\r\n // | x | <-- preview\r\n // |_ _|\r\n // | |\r\n // | | <-- cap\r\n // |___|\r\n\r\n private readonly cap: HTMLDivElement;\r\n private readonly tip: HTMLDivElement;\r\n private previewHost: GesturePreviewHost;\r\n private preview: HTMLDivElement;\r\n private readonly vkbd: VisualKeyboard;\r\n\r\n private readonly constrain: boolean;\r\n private readonly reorient: (orientation: PhoneKeyTipOrientation) => void;\r\n\r\n /**\r\n *\r\n * @param constrain keep the keytip within the bounds of the overall OSK.\r\n * Will probably be handled via function in a later pass.\r\n */\r\n constructor(vkbd: VisualKeyboard, constrain: boolean) {\r\n this.vkbd = vkbd;\r\n let tipElement = this.element=document.createElement('div');\r\n tipElement.className='kmw-keytip';\r\n tipElement.id = 'kmw-keytip';\r\n\r\n // The following style is critical, so do not rely on external CSS\r\n tipElement.style.pointerEvents='none';\r\n tipElement.style.display='none';\r\n\r\n tipElement.appendChild(this.tip = document.createElement('div'));\r\n tipElement.appendChild(this.cap = document.createElement('div'));\r\n this.tip.appendChild(this.preview = document.createElement('div'));\r\n\r\n this.tip.className = 'kmw-keytip-tip';\r\n this.cap.className = 'kmw-keytip-cap';\r\n\r\n this.constrain = constrain;\r\n\r\n this.reorient = (orientation: PhoneKeyTipOrientation) => {\r\n this.orientation = orientation;\r\n this.show(this.key, this.state, this.previewHost);\r\n }\r\n }\r\n\r\n show(key: KeyElement, on: boolean, previewHost: GesturePreviewHost) {\r\n const vkbd = this.vkbd;\r\n\r\n // During quick input sequences - especially during a multitap-modipress - it's possible\r\n // for a user to request a preview for a key from a layer that is currently active, but\r\n // currently not visible due to need previously-requested layout calcs for a different layer.\r\n if(on) {\r\n // Necessary for `key.offsetParent` and client-rect methods referenced below.\r\n // Will not unnecessarily force reflow if the layer is already in proper document flow,\r\n // but otherwise restores it.\r\n vkbd.layerGroup.blinkLayer(key.key.spec.displayLayer);\r\n }\r\n\r\n // Create and display the preview\r\n // If !key.offsetParent, the OSK is probably hidden. Either way, it's a half-\r\n // decent null-guard check.\r\n if(on && key.offsetParent) {\r\n // The key element is positioned relative to its key-square, which is,\r\n // in turn, relative to its row. Rows take 100% width, so this is sufficient.\r\n //\r\n let rowElement = (key.key as OSKBaseKey).row.element;\r\n\r\n // May need adjustment for borders if ever enabled for the desktop form-factor target.\r\n let rkey = key.getClientRects()[0], rrow = rowElement.getClientRects()[0];\r\n let xLeft = rkey.left - rrow.left,\r\n xWidth = rkey.width,\r\n xHeight = rkey.height,\r\n previewFontScale = 1.8;\r\n\r\n let kts = this.element.style;\r\n\r\n // Roughly matches how the subkey positioning is set.\r\n const _Box = vkbd.topContainer as HTMLDivElement;\r\n const _BoxRect = _Box.getBoundingClientRect();\r\n const keyRect = key.getBoundingClientRect();\r\n\r\n let y: number;\r\n const orientation = this.orientation;\r\n const distFromTop = keyRect.bottom - _BoxRect.top;\r\n y = (distFromTop + (orientation == 'top' ? 1 : -1));\r\n let ySubPixelPadding = y - Math.floor(y);\r\n\r\n // Canvas dimensions must be set explicitly to prevent clipping\r\n // This gives us exactly the same number of pixels on left and right\r\n let canvasWidth = xWidth + Math.ceil(xWidth * 0.3) * 2;\r\n let canvasHeight = Math.ceil(2.3 * xHeight) + (ySubPixelPadding); //\r\n\r\n if(orientation == 'bottom') {\r\n y += canvasHeight - xHeight;\r\n }\r\n\r\n kts.top = 'auto';\r\n const unselectedOrientation = orientation == 'top' ? 'bottom' : 'top';\r\n this.tip.classList.remove(`${CSS_PREFIX}${unselectedOrientation}`);\r\n this.tip.classList.add(`${CSS_PREFIX}${orientation}`);\r\n\r\n kts.bottom = Math.floor(_BoxRect.height - y) + 'px';\r\n kts.textAlign = 'center';\r\n kts.overflow = 'visible';\r\n kts.width = canvasWidth+'px';\r\n kts.height = canvasHeight+'px';\r\n\r\n // Some keyboards (such as `balochi_scientific`) do not _package_ a font but\r\n // specify an extremely common one, such as Arial. In such cases, .kmw-key-text\r\n // custom styling doesn't exist, relying on the layer object to simply specify\r\n // the font-family.\r\n const layerFontFamily = this.vkbd.currentLayer.element.style.fontFamily;\r\n const ckts = getComputedStyle(vkbd.element);\r\n kts.fontFamily = key.key.spec.font || layerFontFamily || ckts.fontFamily;\r\n\r\n var px=parseInt(ckts.fontSize,10);\r\n if(px == Number.NaN) {\r\n px = 0;\r\n }\r\n\r\n if(px != 0) {\r\n let scaleStyle = {\r\n keyWidth: 1.6 * xWidth,\r\n keyHeight: 1.6 * xHeight, // as opposed to the canvas height of 2.3 * xHeight.\r\n baseEmFontSize: vkbd.getKeyEmFontSize(),\r\n layoutFontSize: new ParsedLengthStyle(vkbd.kbdDiv.style.fontSize)\r\n };\r\n\r\n kts.fontSize = key.key.getIdealFontSize(key.key.keyText, scaleStyle, previewFontScale).styleString;\r\n }\r\n\r\n // Adjust shape if at edges\r\n var xOverflow = (canvasWidth - xWidth) / 2;\r\n if(xLeft < xOverflow) {\r\n this.cap.style.left = '1px';\r\n xLeft += xOverflow - 1;\r\n } else if(xLeft > window.innerWidth - xWidth - xOverflow) {\r\n this.cap.style.left = (canvasWidth - xWidth - 1) + 'px';\r\n xLeft -= xOverflow - 1;\r\n } else {\r\n this.cap.style.left = xOverflow + 'px';\r\n }\r\n\r\n kts.left=(xLeft - xOverflow) + 'px';\r\n\r\n let cs = getComputedStyle(this.element);\r\n let oskHeight = _BoxRect.height;\r\n let bottomY = parseFloat(cs.bottom);\r\n let tipHeight = parseFloat(cs.height);\r\n let halfHeight = Math.ceil(canvasHeight / 2);\r\n\r\n this.cap.style.width = xWidth + 'px';\r\n this.tip.style.height = halfHeight + 'px';\r\n\r\n const capOffset = 3;\r\n const capStart = (halfHeight - capOffset) + 'px';\r\n if(orientation == 'top') {\r\n this.cap.style.top = capStart;\r\n this.cap.style.bottom = '';\r\n } else {\r\n this.cap.style.top = '';\r\n this.cap.style.bottom = capStart;\r\n }\r\n const defaultCapHeight = (distFromTop - Math.floor(y) + canvasHeight - (orientation == 'top' ? halfHeight : -capOffset * 2));\r\n this.cap.style.height = defaultCapHeight + 'px';\r\n\r\n if(this.constrain && tipHeight + bottomY > oskHeight) {\r\n const delta = tipHeight + bottomY - oskHeight;\r\n kts.height = (canvasHeight-delta) + 'px';\r\n const hx = Math.max(0, (canvasHeight-delta)-(canvasHeight/2) + 2);\r\n this.cap.style.height = hx + 'px';\r\n } else if(bottomY < 0) { // we'll assume that we always constrain at the OSK's bottom.\r\n kts.bottom = '0px';\r\n this.cap.style.height = Math.max(0, defaultCapHeight + bottomY) + 'px';\r\n }\r\n\r\n kts.display = 'block';\r\n\r\n if(this.previewHost == previewHost) {\r\n return;\r\n }\r\n\r\n const oldHost = this.preview;\r\n\r\n if(this.previewHost) {\r\n this.previewHost.off('preferredOrientation', this.reorient);\r\n }\r\n this.previewHost = previewHost;\r\n\r\n if(previewHost) {\r\n this.previewHost.on('preferredOrientation', this.reorient);\r\n this.preview = this.previewHost.element;\r\n this.tip.replaceChild(this.preview, oldHost);\r\n previewHost.setCancellationHandler(() => this.show(null, false, null));\r\n previewHost.on('startFade', () => {\r\n this.element.classList.remove('kmw-preview-fade');\r\n // Note: a reflow is needed to reset the transition animation.\r\n this.element.offsetWidth;\r\n this.element.classList.add('kmw-preview-fade');\r\n });\r\n }\r\n } else { // Hide the key preview\r\n this.element.style.display = 'none';\r\n this.previewHost?.off('preferredOrientation', this.reorient);\r\n this.previewHost = null;\r\n const oldPreview = this.preview;\r\n this.preview = document.createElement('div');\r\n this.tip.replaceChild(this.preview, oldPreview);\r\n this.element.classList.remove('kmw-preview-fade');\r\n\r\n this.orientation = DEFAULT_TIP_ORIENTATION;\r\n }\r\n\r\n // Save the key preview state\r\n this.key = key;\r\n this.state = on;\r\n }\r\n}", + "import { KeyElement } from '../../../keyElement.js';\r\nimport KeyTipInterface from '../../../keytip.interface.js';\r\nimport VisualKeyboard from '../../../visualKeyboard.js';\r\nimport { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js';\r\n\r\nconst BASE_CLASS = 'kmw-keypreview';\r\nconst OVERLAY_CLASS = 'kmw-preview-overlay';\r\nconst BASE_ID = 'kmw-keytip';\r\n\r\nexport class TabletKeyTip implements KeyTipInterface {\r\n public readonly element: HTMLDivElement;\r\n public key: KeyElement;\r\n public state: boolean = false;\r\n\r\n private previewHost: GesturePreviewHost;\r\n private preview: HTMLDivElement;\r\n private readonly vkbd: VisualKeyboard;\r\n\r\n /**\r\n * Pre-builds a reusable element to serve as a gesture-preview host for selected keys\r\n * on tablet-form factor devices. This element hovers over the keyboard, staying in\r\n * place (and on top) even when the layer changes.\r\n */\r\n constructor(vkbd: VisualKeyboard) {\r\n this.vkbd = vkbd;\r\n const base = this.element=document.createElement('div');\r\n base.className=BASE_CLASS;\r\n base.id = 'kmw-keytip';\r\n\r\n // The following style is critical, so do not rely on external CSS\r\n base.style.pointerEvents='none';\r\n base.style.display='none';\r\n\r\n this.preview = document.createElement('div');\r\n base.appendChild(this.preview);\r\n }\r\n\r\n show(key: KeyElement, on: boolean, previewHost: GesturePreviewHost) {\r\n const vkbd = this.vkbd;\r\n const keyLayer = key?.key.spec.displayLayer;\r\n\r\n // During quick input sequences - especially during a multitap-modipress - it's possible\r\n // for a user to request a preview for a key from a layer that is currently active, but\r\n // currently not visible due to need previously-requested layout calcs for a different layer.\r\n if(on) {\r\n // Necessary for `key.offsetParent` and client-rect methods referenced below.\r\n // Will not unnecessarily force reflow if the layer is already in proper document flow,\r\n // but otherwise restores it.\r\n vkbd.layerGroup.blinkLayer(keyLayer);\r\n }\r\n\r\n // Create and display the preview\r\n // If !key.offsetParent, the OSK is probably hidden. Either way, it's a half-\r\n // decent null-guard check.\r\n if(on && key?.offsetParent) {\r\n // May need adjustment for borders if ever enabled for the desktop form-factor target.\r\n\r\n // Note: this.vkbd does not set relative or absolute positioning. Nearest positioned\r\n // ancestor = the OSKView's _Box, accessible as this.vkbd.topContainer.\r\n const hostRect = this.vkbd.topContainer.getBoundingClientRect();\r\n const keyRect = key.getBoundingClientRect();\r\n\r\n // Used to apply box-shadow overlay styling when the preview is for a key on a layer not\r\n // currently active. This is done in case the layers don't have perfect alignment for\r\n // all keys.\r\n const conditionalOverlayStyle = (keyLayer != vkbd.layerId) ? OVERLAY_CLASS : '';\r\n this.element.className = `${BASE_CLASS} ${key.className} ${conditionalOverlayStyle}`;\r\n\r\n // Some keyboards use custom CSS styling based on partial-matching the key ID\r\n // (like sil_cameroon_azerty); this lets us map the custom styles onto the tablet\r\n // preview, too.\r\n this.element.id = `${BASE_ID}-${key.id}`;\r\n\r\n const kts = this.element.style;\r\n\r\n // Some keyboards (such as `balochi_scientific`) do not _package_ a font but\r\n // specify an extremely common one, such as Arial. In such cases, .kmw-key-text\r\n // custom styling doesn't exist, relying on the layer object to simply specify\r\n // the font-family.\r\n const fontFamily = this.vkbd.currentLayer.element.style.fontFamily;\r\n kts.fontFamily = key.key.spec.font || fontFamily;\r\n\r\n kts.left = (keyRect.left - hostRect.left) + 'px';\r\n kts.top = (keyRect.top - hostRect.top) + 'px';\r\n kts.width = keyRect.width + 'px';\r\n kts.height = keyRect.height + 'px';\r\n\r\n this.element.style.display = 'block';\r\n\r\n if(this.previewHost == previewHost) {\r\n return;\r\n }\r\n\r\n const oldHost = this.preview;\r\n this.previewHost = previewHost;\r\n\r\n if(previewHost) {\r\n this.preview = this.previewHost.element;\r\n this.element.replaceChild(this.preview, oldHost);\r\n previewHost.setCancellationHandler(() => this.show(null, false, null));\r\n previewHost.on('startFade', () => {\r\n this.element.classList.remove('kmw-preview-fade');\r\n // Note: a reflow is needed to reset the transition animation.\r\n this.element.offsetWidth;\r\n this.element.classList.add('kmw-preview-fade');\r\n });\r\n }\r\n } else { // Hide the key preview\r\n this.element.style.display = 'none';\r\n this.element.className = BASE_CLASS;\r\n\r\n this.previewHost = null;\r\n const oldPreview = this.preview;\r\n this.preview = document.createElement('div');\r\n this.element.replaceChild(this.preview, oldPreview);\r\n this.element.classList.remove('kmw-preview-fade');\r\n }\r\n\r\n // Save the key preview state\r\n this.key = key;\r\n this.state = on;\r\n }\r\n}", + "import { landscapeView } from \"keyman/engine/dom-utils\";\r\nimport { DeviceSpec } from \"@keymanapp/web-utils\";\r\n\r\n/**\r\n * Get viewport scale factor for this document\r\n *\r\n * @return {number}\r\n */\r\nexport function getViewportScale(formFactor: DeviceSpec.FormFactor): number {\r\n // This can sometimes fail with some browsers if called before document defined,\r\n // so catch the exception\r\n try {\r\n // For emulation of iOS on a desktop device, use a default value\r\n if(formFactor == 'desktop') {\r\n return 1;\r\n }\r\n\r\n // Get viewport width\r\n var viewportWidth = document.documentElement.clientWidth;\r\n\r\n // Return a default value if screen width is greater than the viewport width (not fullscreen).\r\n if(screen.width > viewportWidth) {\r\n return 1;\r\n }\r\n\r\n // Get the orientation corrected screen width\r\n var screenWidth = screen.width;\r\n if(landscapeView()) {\r\n // Take larger of the two dimensions\r\n if(screen.width < screen.height) {\r\n screenWidth = screen.height;\r\n }\r\n } else {\r\n // Take smaller of the two dimensions\r\n if(screen.width > screen.height) {\r\n screenWidth = screen.height;\r\n }\r\n }\r\n // Calculate viewport scale\r\n return Math.round(100*screenWidth / viewportWidth)/100;\r\n } catch(ex) {\r\n return 1;\r\n }\r\n}", + "import { GestureSequence } from \"@keymanapp/gesture-recognizer\";\r\nimport { KeyDistribution } from \"keyman/engine/keyboard\";\r\n\r\nimport { KeyElement } from \"../../keyElement.js\";\r\nimport { GestureHandler } from './gestureHandler.js';\r\n\r\nexport class HeldRepeater implements GestureHandler {\r\n readonly directlyEmitsKeys = true;\r\n\r\n static readonly INITIAL_DELAY = 500;\r\n static readonly REPEAT_DELAY = 100;\r\n\r\n readonly source: GestureSequence;\r\n readonly hasModalVisualization = false;\r\n readonly repeatClosure: () => void;\r\n\r\n timerHandle: number;\r\n\r\n constructor(source: GestureSequence, closureToRepeat: () => void) {\r\n this.source = source;\r\n\r\n const baseKey = source.stageReports[0].item;\r\n baseKey.key.highlight(true);\r\n\r\n this.repeatClosure = () => {\r\n closureToRepeat();\r\n // The repeat-closure may cancel key highlighting. This restores it afterward.\r\n baseKey.key.highlight(true);\r\n }\r\n\r\n\r\n this.timerHandle = window.setTimeout(this.deleteRepeater, HeldRepeater.INITIAL_DELAY);\r\n\r\n this.source.on('complete', () => {\r\n window.clearTimeout(this.timerHandle);\r\n this.timerHandle = undefined;\r\n baseKey.key.highlight(false);\r\n });\r\n }\r\n\r\n cancel() {\r\n this.deleteRepeater();\r\n this.source.cancel();\r\n }\r\n\r\n readonly deleteRepeater = () => {\r\n this.repeatClosure();\r\n\r\n this.timerHandle = window.setTimeout(this.deleteRepeater, HeldRepeater.REPEAT_DELAY);\r\n }\r\n\r\n currentStageKeyDistribution(): KeyDistribution {\r\n return null;\r\n }\r\n}", + "import { ActiveSubKey } from 'keyman/engine/keyboard';\r\nimport OSKKey from '../../../keyboard-layout/oskKey.js';\r\nimport { KeyData, KeyElement, link } from '../../../keyElement.js';\r\nimport VisualKeyboard from '../../../visualKeyboard.js';\r\n\r\n// Typing is to ensure that the keys specified below actually are on the type...\r\n// and to gain Intellisense if more need to be added.\r\n\r\nexport default class OSKSubKey extends OSKKey {\r\n constructor(spec: ActiveSubKey, layer: string) {\r\n if(typeof(layer) != 'string' || layer == '') {\r\n throw \"The 'layer' parameter for subkey construction must be properly defined.\";\r\n }\r\n\r\n super(spec, layer);\r\n }\r\n\r\n getId(): string {\r\n return 'popup-'+this.spec.elementID;\r\n }\r\n\r\n construct(osk: VisualKeyboard, baseKey: KeyElement, width: number, topMargin: boolean): HTMLDivElement {\r\n let spec = this.spec;\r\n\r\n let kDiv=document.createElement('div');\r\n let ks=kDiv.style;\r\n\r\n kDiv.className='kmw-key-square-ex';\r\n if(topMargin) {\r\n ks.marginTop='5px';\r\n }\r\n\r\n ks.width=width+'px';\r\n ks.height=baseKey.offsetHeight+'px';\r\n\r\n let btnEle=document.createElement('div');\r\n let btn = this.btn = link(btnEle, new KeyData(this, spec['id']));\r\n\r\n this.setButtonClass();\r\n btn.id = this.getId();\r\n\r\n // Must set button size (in px) dynamically, not from CSS\r\n let bs=btn.style;\r\n bs.height=ks.height;\r\n bs.lineHeight=baseKey.style.lineHeight;\r\n bs.width=ks.width;\r\n\r\n // Must set position explicitly, at least for Android\r\n bs.position='absolute';\r\n\r\n btn.appendChild(this.label = this.generateKeyText(osk));\r\n kDiv.appendChild(btn);\r\n\r\n return this.square = kDiv;\r\n }\r\n\r\n public allowsKeyTip(): boolean {\r\n return false;\r\n }\r\n}", + "import OSKSubKey from './oskSubKey.js';\r\nimport { type KeyElement } from '../../../keyElement.js';\r\nimport OSKBaseKey from '../../../keyboard-layout/oskBaseKey.js';\r\nimport VisualKeyboard from '../../../visualKeyboard.js';\r\n\r\nimport { DeviceSpec, ActiveSubKey, KeyDistribution, ActiveKeyBase } from 'keyman/engine/keyboard';\r\nimport { ConfigChangeClosure, GestureRecognizerConfiguration, GestureSequence, PaddedZoneSource, RecognitionZoneSource } from '@keymanapp/gesture-recognizer';\r\nimport { GestureHandler } from '../gestureHandler.js';\r\nimport { CorrectionLayout, CorrectionLayoutEntry } from '../../../correctionLayout.js';\r\nimport { distributionFromDistanceMaps, keyTouchDistances } from '../../../corrections.js';\r\nimport { GestureParams } from '../specsForLayout.js';\r\n\r\n/**\r\n * The fraction of the base key's height to add to an unconstrained subkey-menu\r\n * callout.\r\n */\r\nconst CALLOUT_ROW_HEIGHT_RATIO = 0.2;\r\n\r\n/**\r\n * The max fraction of a base key's width to use for a subkey-menu callout\r\n */\r\nconst MAX_CALLOUT_KEY_WIDTH = 1.2;\r\n\r\n/**\r\n * Space to keep between the top of the base key and the bottom of the subkey menu,\r\n * if possible.\r\n */\r\nconst SUBKEY_MENU_VERT_OFFSET = 3;\r\n\r\n/**\r\n * Should match the margin-left style value for 'kmw-key-square-ex' in kmwosk.css\r\n * if possible.\r\n */\r\nconst SUBKEY_DEFAULT_MARGIN_LEFT = 5;\r\n\r\n/**\r\n * The minimum pixel-height value to use for subkey-menu callout height when not\r\n * obscured by an overlapping subkey menu (due to WebView boundary constraint effects)\r\n */\r\nconst CALLOUT_BASE_HEIGHT = 6 + SUBKEY_MENU_VERT_OFFSET;\r\n\r\n/**\r\n * Represents a 'realized' longpress gesture's default implementation\r\n * within KeymanWeb. Once a touch sequence has been confirmed to\r\n * correspond to a longpress gesture, implementations of this class\r\n * provide the following:\r\n * * The UI needed to present a subkey menu\r\n * * The state management needed to present feedback about the\r\n * currently-selected subkey to the user\r\n *\r\n * As selection of the subkey occurs after the subkey popup is\r\n * displayed, selection of the subkey is inherently asynchronous.\r\n */\r\nexport default class SubkeyPopup implements GestureHandler {\r\n readonly directlyEmitsKeys = true;\r\n\r\n public readonly element: HTMLDivElement;\r\n public readonly shim: HTMLDivElement;\r\n\r\n private currentSelection: KeyElement;\r\n\r\n private callout: HTMLDivElement;\r\n private readonly menuWidth: number;\r\n\r\n public readonly baseKey: KeyElement;\r\n public readonly subkeys: KeyElement[];\r\n\r\n private source: GestureSequence;\r\n private readonly gestureParams: GestureParams;\r\n\r\n readonly shouldLockLayer: boolean = false;\r\n\r\n constructor(\r\n source: GestureSequence,\r\n configChanger: ConfigChangeClosure,\r\n vkbd: VisualKeyboard,\r\n e: KeyElement,\r\n gestureParams: GestureParams\r\n ) {\r\n this.baseKey = e;\r\n this.source = source;\r\n this.gestureParams = gestureParams;\r\n\r\n if(vkbd.layerLocked) {\r\n this.shouldLockLayer = true;\r\n }\r\n\r\n source.on('complete', () => {\r\n this.currentSelection?.key.highlight(false);\r\n this.clear();\r\n });\r\n\r\n // When subkey-selection is fully triggered, emit the selected key.\r\n source.on('stage', () => {\r\n const key = this.currentSelection;\r\n if(key) {\r\n const keyEvent = vkbd.keyEventFromSpec(key.key.spec);\r\n keyEvent.keyDistribution = this.currentStageKeyDistribution();\r\n\r\n vkbd.raiseKeyEvent(keyEvent, key);\r\n }\r\n });\r\n\r\n // From here, we want to make decisions based on only the subkey-menu portion of the gesture path.\r\n const subkeyComponent = source.stageReports[0].sources[0].constructSubview(true, false);\r\n\r\n // Watch for touchpoint selection of new keys.\r\n subkeyComponent.path.on('step', (sample) => {\r\n // Require a fudge-factor before dropping the default key.\r\n if(subkeyComponent.path.stats.netDistance >= 4) {\r\n this.currentSelection?.key.highlight(false);\r\n sample.item?.key.highlight(true);\r\n this.currentSelection = sample.item;\r\n }\r\n });\r\n\r\n // If the user doesn't move their finger and releases, we'll output the base key\r\n // by default.\r\n this.currentSelection = e;\r\n e.key.highlight(true);\r\n\r\n // A tag we directly set on a key element during its construction.\r\n let subKeySpec: ActiveSubKey[] = e['subKeys'];\r\n\r\n // The holder is position:fixed, but the keys do not need to be, as no scrolling\r\n // is possible while the array is visible. So it is simplest to let the keys have\r\n // position:static and display:inline-block\r\n const elements = this.element = document.createElement('div');\r\n\r\n elements.id='kmw-popup-keys';\r\n\r\n // #3718: No longer prepend base key to popup array\r\n\r\n // Must set position dynamically, not in CSS\r\n var ss=elements.style;\r\n\r\n // Set key font according to layout, or defaulting to OSK font\r\n // (copied, not inherited, since OSK is not a parent of popup keys)\r\n ss.fontFamily=vkbd.fontFamily;\r\n\r\n // Copy the font size from the parent key, allowing for style inheritance\r\n const computedStyle = getComputedStyle(e);\r\n ss.fontSize=computedStyle.fontSize;\r\n ss.visibility='hidden';\r\n\r\n let layer = e['key'].layer;\r\n if (typeof (layer) != 'string' || layer == '') {\r\n // Use the currently-active layer.\r\n layer = vkbd.layerId;\r\n }\r\n\r\n const nKeys = subKeySpec.length;\r\n // Put a maximum of 9 keys in a row to reduce travel distance\r\n const nRows=Math.ceil(nKeys/9);\r\n const nCols=Math.ceil(nKeys/nRows);\r\n\r\n // Add nested button elements for each sub-key\r\n this.subkeys = [];\r\n let thisRowWidth = SUBKEY_DEFAULT_MARGIN_LEFT;\r\n let iRow = 0;\r\n for(let i=0, iCol=0; i vkbd.width - 2 * SUBKEY_DEFAULT_MARGIN_LEFT) {\r\n subkeyWidth = vkbd.width - 2 * SUBKEY_DEFAULT_MARGIN_LEFT;\r\n }\r\n\r\n if (thisRowWidth + subkeyWidth + SUBKEY_DEFAULT_MARGIN_LEFT > vkbd.width || iCol >= nCols) {\r\n // New subkey doesn't fit in the current row. Start a new row.\r\n // TODO: currently we don't check that the rows fit vertically,\r\n // so it's possible that the top or bottom of the subkey menu\r\n // is not visible.\r\n iRow++;\r\n iCol = 0;\r\n thisRowWidth = SUBKEY_DEFAULT_MARGIN_LEFT;\r\n }\r\n const keyGenerator = new OSKSubKey(subKeySpec[i], layer);\r\n const kDiv = keyGenerator.construct(vkbd, e, subkeyWidth, iRow > 0);\r\n thisRowWidth += subkeyWidth + SUBKEY_DEFAULT_MARGIN_LEFT;\r\n this.menuWidth = Math.max(this.menuWidth ?? 0, thisRowWidth);\r\n this.subkeys.push(kDiv.firstChild as KeyElement);\r\n\r\n elements.appendChild(kDiv);\r\n }\r\n\r\n ss.width = this.menuWidth + 'px';\r\n\r\n // And add a filter to fade main keyboard\r\n this.shim = document.createElement('div');\r\n this.shim.id = 'kmw-popup-shim';\r\n\r\n // Highlight the duplicated base key or ideal subkey (if a phone)\r\n if(vkbd.device.formFactor == DeviceSpec.FormFactor.Phone) {\r\n this.selectDefaultSubkey(e, elements /* == this.element */);\r\n }\r\n\r\n vkbd.element.appendChild(this.element);\r\n // The shim should probably fade the banner, too.\r\n vkbd.topContainer.appendChild(this.shim);\r\n\r\n // Must be placed after its `.element` has been inserted into the DOM.\r\n this.reposition(vkbd);\r\n\r\n const config = this.buildPopupRecognitionConfig(vkbd);\r\n configChanger({\r\n type: 'push',\r\n config: config\r\n });\r\n }\r\n\r\n private buildPopupRecognitionConfig(vkbd: VisualKeyboard): GestureRecognizerConfiguration {\r\n const baseBounding = this.element.getBoundingClientRect();\r\n const underlyingKeyBounding = this.baseKey.getBoundingClientRect();\r\n\r\n const subkeyStyle = this.subkeys[0].style;\r\n const subkeyHeight = Number.parseInt(subkeyStyle.height, 10);\r\n const basePadding = -0.666 * subkeyHeight; // extends bounds by the absolute value.\r\n const topScalar = 3;\r\n\r\n const bottomDistance = underlyingKeyBounding.bottom - baseBounding.bottom;\r\n\r\n const roamBounding = new PaddedZoneSource(this.element, [\r\n // top\r\n basePadding * topScalar, // be extra-loose for the top!\r\n // left, right\r\n basePadding,\r\n // bottom: ensure the recognition zone includes the row of the base key.\r\n // basePadding is already negative, but bottomDistance isn't.\r\n -bottomDistance < basePadding ? -bottomDistance : basePadding\r\n ]);\r\n\r\n const sustainBounding: RecognitionZoneSource = {\r\n getBoundingClientRect() {\r\n // We don't want to actually use Number.NEGATIVE_INFINITY or Number.POSITIVE_INFINITY\r\n // because that produces a DOMRect with a few NaN fields, and we don't want _that_.\r\n\r\n // Way larger than any screen resolution should ever be.\r\n const base = Number.MAX_SAFE_INTEGER;\r\n return new DOMRect(-base, -base, 2*base, 2*base);\r\n },\r\n }\r\n\r\n return {\r\n targetRoot: this.element,\r\n inputStartBounds: vkbd.element,\r\n maxRoamingBounds: sustainBounding,\r\n safeBounds: sustainBounding, // if embedded, ensure top boundary extends outside the WebView!\r\n itemIdentifier: (coord, target) => {\r\n const roamingRect = roamBounding.getBoundingClientRect();\r\n\r\n let bestMatchKey: KeyElement = null;\r\n let bestYdist = Number.MAX_VALUE;\r\n let bestXdist = Number.MAX_VALUE;\r\n\r\n // Step 1: is the coordinate within the range we permit for selecting _anything_?\r\n if(coord.clientX < roamingRect.left || coord.clientX > roamingRect.right) {\r\n return null;\r\n }\r\n if(coord.clientY < roamingRect.top || coord.clientY > roamingRect.bottom) {\r\n return null;\r\n }\r\n\r\n // Step 2: okay, selection is permitted. So... what to select?\r\n for(let key of this.subkeys) {\r\n const keyBounds = key.getBoundingClientRect();\r\n\r\n let xDist = Number.MAX_VALUE;\r\n let yDist = Number.MAX_VALUE;\r\n\r\n if(keyBounds.left <= coord.clientX && coord.clientX < keyBounds.right) {\r\n xDist = 0;\r\n } else {\r\n xDist = (keyBounds.left >= coord.clientX) ? keyBounds.left - coord.clientX : coord.clientX - keyBounds.right;\r\n }\r\n\r\n if(keyBounds.top <= coord.clientY && coord.clientY < keyBounds.bottom) {\r\n yDist = 0;\r\n } else {\r\n yDist = (keyBounds.top >= coord.clientY) ? keyBounds.top - coord.clientY : coord.clientY - keyBounds.bottom;\r\n }\r\n\r\n if(xDist == 0 && yDist == 0) {\r\n // Perfect match!\r\n return key;\r\n } else if(xDist < bestXdist || (xDist == bestXdist && yDist < bestYdist)) {\r\n bestXdist = xDist;\r\n bestMatchKey = key;\r\n bestYdist = yDist;\r\n }\r\n }\r\n\r\n return bestMatchKey;\r\n }\r\n }\r\n }\r\n\r\n reposition(vkbd: VisualKeyboard) {\r\n let subKeys = this.element;\r\n let e = this.baseKey;\r\n\r\n // And correct its position with respect to that element\r\n const _Box = vkbd.topContainer;\r\n let rowElement = (e.key as OSKBaseKey).row.element;\r\n let ss=subKeys.style;\r\n let parentOffsetLeft = e.offsetParent ? (e.offsetParent).offsetLeft : 0;\r\n var x = e.offsetLeft + parentOffsetLeft + 0.5*(e.offsetWidth-subKeys.offsetWidth);\r\n var xMax = vkbd.width - subKeys.offsetWidth;\r\n\r\n if(x > xMax) {\r\n x=xMax;\r\n }\r\n if(x < 0) {\r\n x=0;\r\n }\r\n ss.left=x+'px';\r\n\r\n let _BoxRect = _Box.getBoundingClientRect();\r\n let rowElementRect = rowElement.getBoundingClientRect();\r\n ss.top = (rowElementRect.top - _BoxRect.top - subKeys.offsetHeight - SUBKEY_MENU_VERT_OFFSET) + 'px';\r\n\r\n // Make the popup keys visible\r\n ss.visibility='visible';\r\n\r\n // For now, should only be true (in production) when keyman.isEmbedded == true.\r\n let constrainPopup = vkbd.isEmbedded;\r\n\r\n let cs = getComputedStyle(subKeys);\r\n let topY = parseFloat(cs.top);\r\n\r\n // Adjust the vertical position of the popup to keep it within the\r\n // bounds of the keyboard rectangle, when on iPhone (system keyboard)\r\n const topOffset = 0; // Set this when testing constrainPopup, e.g. to -80px\r\n let delta = 0;\r\n if(topY < topOffset && constrainPopup) {\r\n delta = topOffset - topY;\r\n ss.top = topOffset + 'px';\r\n }\r\n\r\n // Add the callout\r\n this.callout = this.addCallout(e, delta, vkbd.element, vkbd.topContainer, vkbd.device.formFactor == 'tablet');\r\n }\r\n\r\n /**\r\n * Add a callout for popup keys (if KeymanWeb on a phone device)\r\n *\r\n * @param {Object} key HTML key element\r\n * @param {number} delta The pixel offset for the callout from the position it would have if not\r\n * constrained due to WebView boundaries.\r\n * @return {Object} callout object\r\n */\r\n addCallout(key: KeyElement, delta: number, host: HTMLElement, _Box: HTMLElement, isTablet: boolean): HTMLDivElement {\r\n delta = delta || 0;\r\n\r\n // Uses content-box styling, so ignores border aspects for reported positions.\r\n /**\r\n * \"Computed Menu Style\"\r\n */\r\n const cms = getComputedStyle(this.element);\r\n const borderRadius = Math.max(Number.parseInt(cms.borderRadius), 0);\r\n\r\n // Create the callout\r\n let keyRect = key.getBoundingClientRect();\r\n let _BoxRect = _Box.getBoundingClientRect();\r\n\r\n // Set position and style\r\n // We're going to adjust the top of the box to ensure it stays\r\n // pixel aligned, otherwise we can get antialiasing artifacts\r\n // that look ugly\r\n let calloutTop = Math.floor(\r\n Number.parseInt(cms.top, 10) +\r\n Number.parseInt(cms.height, 10) +\r\n // Padding is not included in content-box (or in content-box's top positioning)...\r\n // but half the padding-top seems useful on all tested devices.\r\n // Not sure exactly why.\r\n Number.parseInt(cms.paddingTop, 10)/2 +\r\n Number.parseInt(cms.paddingBottom, 10)\r\n );\r\n\r\n const calloutProportionalHeight = CALLOUT_ROW_HEIGHT_RATIO * (keyRect.height - delta);\r\n const maxProportionalHeight = CALLOUT_ROW_HEIGHT_RATIO * keyRect.height;\r\n\r\n const targetHeight = calloutProportionalHeight + CALLOUT_BASE_HEIGHT;\r\n const calloutDownscaleRatio = targetHeight / (maxProportionalHeight + CALLOUT_BASE_HEIGHT)\r\n\r\n // Shorten the callout if the subkey menu is being constrained within WebView bounds, thus\r\n // overlapping the base key. Do so (mostly) proportionally to how much is obscured.\r\n const maxHeight = (keyRect.bottom - _BoxRect.top) - calloutTop - 1;\r\n const selectedHeight = maxHeight < targetHeight ? maxHeight : targetHeight;\r\n\r\n if(selectedHeight > 0) {\r\n const cc = document.createElement('div');\r\n const ccs = cc.style;\r\n cc.id = 'kmw-popup-callout';\r\n host.appendChild(cc);\r\n\r\n ccs.top = calloutTop + 'px';\r\n ccs.borderTopWidth = (selectedHeight) + 'px';\r\n\r\n const calloutKeyWidthRatio = MAX_CALLOUT_KEY_WIDTH * calloutDownscaleRatio;\r\n\r\n const desiredCalloutWidth = keyRect.width * calloutKeyWidthRatio;\r\n const maxCalloutWidth = this.menuWidth - 2 * borderRadius;\r\n const targetCalloutWidth = maxCalloutWidth < desiredCalloutWidth ? maxCalloutWidth : desiredCalloutWidth;\r\n\r\n const calloutLeftOffset = (keyRect.left - _BoxRect.left - (targetCalloutWidth - keyRect.width)/2);\r\n\r\n // Avoid letting the callout overrun screen bounds or overshooting the transition to curved borders\r\n const calloutRightOverrun = Math.max(0, (calloutLeftOffset + targetCalloutWidth) - (_BoxRect.right - borderRadius));\r\n const calloutLeftOverrun = Math.max(0, borderRadius-calloutLeftOffset);\r\n\r\n ccs.left = (keyRect.left - _BoxRect.left - (targetCalloutWidth - keyRect.width)/2 + calloutLeftOverrun) /*- ADJUSTMENT*/ + 'px';\r\n ccs.borderLeftWidth = targetCalloutWidth/2 - calloutLeftOverrun + 'px';\r\n ccs.borderRightWidth = targetCalloutWidth/2 - calloutRightOverrun + 'px';\r\n\r\n // Return callout element, to allow removal later\r\n return cc;\r\n } else {\r\n return null;\r\n }\r\n }\r\n\r\n selectDefaultSubkey(baseKey: KeyElement, popupBase: HTMLElement) {\r\n var bk: KeyElement;\r\n let subkeys = baseKey['subKeys'];\r\n for(let i=0; i < subkeys.length; i++) {\r\n let skSpec = subkeys[i];\r\n let skElement = popupBase.childNodes[i].firstChild;\r\n\r\n // Preference order:\r\n // #1: if a default subkey has been specified, select it.\r\n // #2: if no default subkey is specified, default to a subkey with the same\r\n // key ID and layer / modifier spec.\r\n if(skSpec.default) {\r\n bk = skElement;\r\n break;\r\n } else if(!baseKey.key || !baseKey.key.spec) {\r\n continue;\r\n }\r\n\r\n if(skSpec.elementID == baseKey.key.spec.elementID) {\r\n bk = skElement;\r\n }\r\n }\r\n\r\n if(bk) {\r\n this.currentSelection?.key.highlight(false);\r\n this.currentSelection = bk;\r\n\r\n // Subkeys never get key previews, so we can directly highlight the subkey.\r\n bk.key.highlight(true);\r\n }\r\n }\r\n\r\n get hasModalVisualization() {\r\n return this.element.style.visibility == 'visible';\r\n }\r\n\r\n buildCorrectiveLayout(): CorrectionLayout {\r\n const baseBounding = this.element.getBoundingClientRect();\r\n const aspectRatio = baseBounding.width / baseBounding.height;\r\n\r\n const keys = this.subkeys.map((keyElement) => {\r\n const subkeyBounds = keyElement.getBoundingClientRect();\r\n\r\n // Ensures we have the right typing.\r\n const correctiveData: CorrectionLayoutEntry = {\r\n keySpec: keyElement.key.spec,\r\n centerX: ((subkeyBounds.right - subkeyBounds.width / 2) - baseBounding.left) / baseBounding.width,\r\n centerY: ((subkeyBounds.bottom - subkeyBounds.height / 2) - baseBounding.top) / baseBounding.height,\r\n width: subkeyBounds.width / baseBounding.width,\r\n height: subkeyBounds.height / baseBounding.height\r\n }\r\n\r\n return correctiveData;\r\n });\r\n\r\n return {\r\n keys: keys,\r\n kbdScaleRatio: aspectRatio\r\n }\r\n }\r\n\r\n currentStageKeyDistribution(): KeyDistribution {\r\n const latestStage = this.source.stageReports[this.source.stageReports.length-1];\r\n const baseStage = this.source.stageReports[0];\r\n const gestureSource = latestStage.sources[0];\r\n const lastCoord = gestureSource.currentSample;\r\n\r\n const baseBounding = this.element.getBoundingClientRect();\r\n const mappedCoord = {\r\n x: lastCoord.targetX / baseBounding.width,\r\n y: lastCoord.targetY / baseBounding.height\r\n }\r\n\r\n // Lock the coordinate within base-element bounds; corrects for the allowed 'popup roaming' zone.\r\n //\r\n // To consider: add a 'clipping' feature to `keyTouchDistances`? It could make sense for base\r\n // keys, too - especially when emulating a touch OSK via the inline-OSK mode used in the\r\n // Developer host page.\r\n mappedCoord.x = mappedCoord.x < 0 ? 0 : (mappedCoord.x > 1 ? 1: mappedCoord.x);\r\n mappedCoord.y = mappedCoord.y < 0 ? 0 : (mappedCoord.y > 1 ? 1: mappedCoord.y);\r\n\r\n const rawSqDistances = keyTouchDistances(mappedCoord, this.buildCorrectiveLayout());\r\n const currentKeyDist = rawSqDistances.get(lastCoord.item.key.spec);\r\n\r\n /*\r\n * - how long has the subkey menu been visible?\r\n * - Base key should be less likely if it's been visible a while,\r\n * but reasonably likely if it only just appeared.\r\n * - Especially if up-flicks are allowed. Though, in that case, consider\r\n * base-layer neighbors, and particularly the one directly under the touchpoint?\r\n * - raw distance traveled (since the menu appeared)\r\n * - similarly, short distance = a more likely base key?\r\n */\r\n\r\n // The concept: how likely is it that the user MEANT to output a subkey?\r\n let timeDistance = Math.min(\r\n // The full path is included by the model - meaning the base wait is included here in\r\n // in the stats; we subtract it to get just the duration of the subkey menu.\r\n gestureSource.path.stats.duration - baseStage.sources[0].path.stats.duration,\r\n this.gestureParams.longpress.waitLength\r\n ) / (2 * this.gestureParams.longpress.waitLength); // normalize: max time distance of 0.5\r\n\r\n let pathDistance = Math.min(\r\n gestureSource.path.stats.rawDistance,\r\n this.gestureParams.longpress.noiseTolerance*4\r\n ) / (this.gestureParams.longpress.noiseTolerance * 8); // normalize similarly.\r\n\r\n // We only want to add a single distance 'dimension' - we'll choose the one that affects\r\n // the interpreted distance the least. (This matters for upflick-shortcutting in particular)\r\n const layerDistance = Math.min(timeDistance * timeDistance, pathDistance * pathDistance);\r\n const baseKeyDistance = currentKeyDist + layerDistance;\r\n\r\n // Include the base key as a corrective option.\r\n const baseKeyMap = new Map();\r\n const subkeyMatch = this.subkeys.find((entry) => entry.keyId == this.baseKey.keyId);\r\n if(subkeyMatch) {\r\n // Ensure that the base key's entry can be merged with that of its subkey.\r\n // (Assuming that always makes sense.)\r\n baseKeyMap.set(subkeyMatch.key.spec, baseKeyDistance);\r\n } else {\r\n baseKeyMap.set(this.baseKey.key.spec, baseKeyDistance);\r\n }\r\n\r\n return distributionFromDistanceMaps([rawSqDistances, baseKeyMap]);\r\n }\r\n\r\n cancel() {\r\n this.clear();\r\n this.source.cancel();\r\n }\r\n\r\n clear() {\r\n // Remove the displayed subkey array, if any\r\n if(this.element.parentNode) {\r\n this.element.parentNode.removeChild(this.element);\r\n }\r\n\r\n if(this.shim.parentNode) {\r\n this.shim.parentNode.removeChild(this.shim);\r\n }\r\n\r\n if(this.callout && this.callout.parentNode) {\r\n this.callout.parentNode.removeChild(this.callout);\r\n }\r\n }\r\n}\r\n", + "import { type KeyElement } from '../../../keyElement.js';\r\nimport VisualKeyboard from '../../../visualKeyboard.js';\r\n\r\nimport { KeyDistribution, ActiveKeyBase } from 'keyman/engine/keyboard';\r\nimport { GestureSequence } from '@keymanapp/gesture-recognizer';\r\nimport { GestureHandler } from '../gestureHandler.js';\r\n\r\n/**\r\n * Represents a potential modipress gesture's implementation within KeymanWeb, including\r\n * modipresses generated at the end of multitap sequences.\r\n *\r\n * This involves \"locking\" the current layer in place until the modipress is complete.\r\n */\r\nexport default class Modipress implements GestureHandler {\r\n readonly directlyEmitsKeys = true;\r\n\r\n private completionCallback: () => void;\r\n private originalLayer: string;\r\n private shouldRestore: boolean = false;\r\n private source: GestureSequence;\r\n\r\n constructor(\r\n source: GestureSequence,\r\n vkbd: VisualKeyboard,\r\n completionCallback: () => void,\r\n ) {\r\n const initialStage = source.stageReports[0];\r\n this.originalLayer = initialStage.sources[0].stateToken;\r\n this.source = source;\r\n\r\n this.completionCallback = () => {\r\n vkbd.lockLayer(false);\r\n if(this.shouldRestore) {\r\n vkbd.layerId = this.originalLayer;\r\n vkbd.updateState();\r\n }\r\n completionCallback?.();\r\n };\r\n\r\n vkbd.lockLayer(true);\r\n\r\n source.on('stage', (stage) => {\r\n const stageName = stage.matchedId;\r\n if(stageName.includes('modipress') && stageName.includes('-end')) {\r\n this.clear();\r\n } else if(stageName.includes('modipress') && stageName.includes('-hold')) {\r\n this.shouldRestore = true;\r\n }\r\n });\r\n\r\n source.on('complete', () => this.cancel());\r\n }\r\n\r\n get isLocked(): boolean {\r\n return this.shouldRestore;\r\n }\r\n\r\n setLocked() {\r\n this.shouldRestore = true;\r\n }\r\n\r\n get completed(): boolean {\r\n return this.completionCallback === null;\r\n }\r\n\r\n clear() {\r\n const callback = this.completionCallback;\r\n this.completionCallback = null;\r\n callback?.();\r\n }\r\n\r\n cancel() {\r\n this.clear();\r\n this.source.cancel();\r\n }\r\n\r\n readonly hasModalVisualization = false;\r\n\r\n currentStageKeyDistribution(baseDistMap: Map): KeyDistribution {\r\n return null;\r\n }\r\n}", + "import { type KeyElement } from '../../../keyElement.js';\r\nimport VisualKeyboard from '../../../visualKeyboard.js';\r\n\r\nimport { ActiveSubKey, ActiveKey, KeyDistribution, ActiveKeyBase } from 'keyman/engine/keyboard';\r\nimport { GestureSequence, GestureStageReport } from '@keymanapp/gesture-recognizer';\r\nimport { GestureHandler } from '../gestureHandler.js';\r\nimport { distributionFromDistanceMaps } from '../../../corrections.js';\r\nimport Modipress from './modipress.js';\r\nimport { keySupportsModipress } from '../specsForLayout.js';\r\nimport { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js';\r\n\r\n/**\r\n * Represents a potential multitap gesture's implementation within KeymanWeb.\r\n * Once a simple-tap gesture occurs on a key with specified multitap subkeys,\r\n * this class is designed to take over further processing of said gesture.\r\n * This includes providing:\r\n * * UI feedback regarding the state of the ongoing multitap, as appropriate\r\n * * Proper selection of the appropriate multitap key for subsequent taps.\r\n */\r\nexport default class Multitap implements GestureHandler {\r\n readonly directlyEmitsKeys = true;\r\n\r\n public readonly baseKey: KeyElement;\r\n public readonly baseContextToken: number;\r\n public readonly hasModalVisualization = false;\r\n\r\n private readonly originalLayer: string;\r\n\r\n private readonly multitaps: ActiveSubKey[];\r\n private tapIndex = 0;\r\n private modipress: Modipress;\r\n\r\n private readonly sequence: GestureSequence;\r\n\r\n constructor(\r\n source: GestureSequence,\r\n vkbd: VisualKeyboard,\r\n e: KeyElement,\r\n contextToken: number,\r\n previewHost: GesturePreviewHost\r\n ) {\r\n this.baseKey = e;\r\n this.baseContextToken = contextToken;\r\n this.multitaps = [e.key.spec].concat(e.key.spec.multitap);\r\n this.sequence = source;\r\n\r\n const startModipress = (tap: GestureStageReport) => {\r\n // In case of a previous modipress that somehow wasn't cleared.\r\n this.modipress?.clear();\r\n\r\n const modipressHandler = new Modipress(source, vkbd, () => {\r\n this.modipress = vkbd.activeModipress = null;\r\n });\r\n this.modipress = vkbd.activeModipress = modipressHandler;\r\n }\r\n\r\n this.originalLayer = vkbd.layerId;\r\n\r\n const tapLookahead = (offset: number) => (this.tapIndex + offset) % this.multitaps.length;\r\n\r\n const updatePreview = () => {\r\n previewHost?.setMultitapHint(this.multitaps[tapLookahead(0)], this.multitaps[tapLookahead(1)], vkbd);\r\n }\r\n\r\n source.on('complete', () => {\r\n this.modipress?.cancel();\r\n this.clear();\r\n });\r\n\r\n const stageHandler = (tap: GestureStageReport) => {\r\n switch(tap.matchedId) {\r\n // In the case that a modifier key supports multitap, reaching this stage\r\n // indicates that the multitapping is over. Not the modipressing, though.\r\n case 'modipress-hold':\r\n this.clear();\r\n // We'll let the co-existing modipress handler continue.\r\n source.off('stage', stageHandler);\r\n return;\r\n case 'modipress-end-multitap-transition':\r\n case 'modipress-multitap-end':\r\n case 'modipress-end':\r\n case 'multitap-end':\r\n case 'simple-tap':\r\n return;\r\n case 'modipress-multitap-lock-transition':\r\n this.modipress?.setLocked();\r\n return;\r\n // Once a multitap starts, it's better to emit keys on keydown; that way,\r\n // if a user holds long, they get what they see if they decide to stop,\r\n // but also have time to decide if they want to continue to what's next.\r\n case 'modipress-multitap-start':\r\n case 'multitap-start':\r\n break;\r\n default:\r\n throw new Error(`Unsupported gesture state encountered during multitap: ${tap.matchedId}`);\r\n }\r\n\r\n // For rota-style behavior\r\n this.tapIndex = tapLookahead(1);\r\n const selection = this.multitaps[this.tapIndex];\r\n updatePreview();\r\n\r\n const keyEvent = vkbd.keyEventFromSpec(selection);\r\n keyEvent.baseTranscriptionToken = this.baseContextToken;\r\n\r\n const coord = tap.sources[0].currentSample;\r\n const baseDistances = vkbd.getSimpleTapCorrectionDistances(coord, this.baseKey.key.spec as ActiveKey);\r\n if(coord.stateToken != vkbd.layerId && !tap.matchedId.includes('modipress')) {\r\n const matchKey = vkbd.layerGroup.findNearestKey({...coord, stateToken: vkbd.layerId});\r\n\r\n // Replace the key at the current location for the current layer key\r\n // with the multitap base key.\r\n const p = baseDistances.get(matchKey.key.spec);\r\n if(p == null) {\r\n console.warn(\"Could not find current layer's key\")\r\n }\r\n baseDistances.delete(matchKey.key.spec);\r\n baseDistances.set(coord.item.key.spec, p);\r\n }\r\n keyEvent.keyDistribution = this.currentStageKeyDistribution(baseDistances);\r\n\r\n // TODO for future: multitap previews.\r\n\r\n // When _some_ multitap keys support layer-swapping but others don't,\r\n // landing on a non-swap key should preserve the original layer... even\r\n // if no such 'nextLayer' is specified by default.\r\n keyEvent.kNextLayer ||= this.originalLayer;\r\n\r\n vkbd.raiseKeyEvent(keyEvent, null);\r\n\r\n // Now that the key has been processed, with a layer possibly changed as a result...\r\n if(tap.matchedId == 'modipress-multitap-start') {\r\n startModipress(tap);\r\n }\r\n };\r\n\r\n source.on('stage', stageHandler);\r\n\r\n const initialTap = source.stageReports[0];\r\n if(initialTap.matchedId == 'modipress-start') {\r\n startModipress(source.stageReports[0]);\r\n }\r\n\r\n // For this specific instance, we'll go ahead and directly maintain the preview;\r\n // a touch just ended, and all other updates occur on the start of a new touch.\r\n updatePreview();\r\n\r\n /* In theory, setting up a specialized recognizer config limited to the base key's surface area\r\n * would be pretty ideal - it'd provide automatic cancellation if anywhere else were touched.\r\n *\r\n * However, because multitap keys can swap layers, and because an invisible layer doesn't provide\r\n * the expected bounding-box that it would were it visible, it's anything but straightforward to\r\n * do for certain supported cases. It's simpler to handle this problem by leveraging the\r\n * key-finding operation specified on the gesture model and ensuring the base key remains in place.\r\n */\r\n }\r\n\r\n currentStageKeyDistribution(baseDistances: Map): KeyDistribution {\r\n /* Concept: use the base distance map - what if the tap was meant for elsewhere?\r\n * That said, given the base key's probability... modify that by a 'tap distance' metric,\r\n * where the probability of all taps in the multitap rota sum up to the base key's original\r\n * probability.\r\n */\r\n\r\n const baseDistribution = distributionFromDistanceMaps(baseDistances);\r\n const keyIndex = baseDistribution.findIndex((entry) => entry.keySpec == this.baseKey.key.spec);\r\n\r\n if(keyIndex == -1) { // also covers undefined, but does not include 0.\r\n // Modipress keys generally get left out of the key-correction calculations.\r\n if(!keySupportsModipress(this.baseKey)) {\r\n console.warn(\"Could not find base key's probability for multitap correction\");\r\n }\r\n\r\n // Decently recoverable; just use the simple-tap distances instead.\r\n return baseDistribution;\r\n }\r\n\r\n const baseProb = baseDistribution.splice(keyIndex, 1)[0].p;\r\n\r\n let totalWeight = 0;\r\n let multitapEntries: {keySpec: ActiveKeyBase, p: number}[] = [];\r\n for(let i = 0; i < this.multitaps.length; i++) {\r\n const key = this.multitaps[i];\r\n // 'standard distance', no real modular effects needed.\r\n const distStd = Math.abs(i - this.tapIndex) % this.multitaps.length;\r\n // 'wrapped distance', when the modular effects are definitely needed.\r\n const distWrap = (i + this.multitaps.length - this.tapIndex) % this.multitaps.length;\r\n const modularLinDist = distStd < distWrap ? distStd : distWrap;\r\n\r\n // Simple approach for now - we'll ignore timing considerations and\r\n // just use raw modular distance.\r\n // Actual tap: 1 (base weight)\r\n // \"one off\": 1/4 as likely\r\n // \"two off\": 1/9 as likely\r\n // etc.\r\n const keyWeight = 1.0 / ((1 + modularLinDist) * (1 + modularLinDist));\r\n totalWeight += keyWeight;\r\n multitapEntries.push({\r\n keySpec: key,\r\n p: keyWeight\r\n });\r\n }\r\n\r\n // Converts from the weights to the final probability values specified by the\r\n // top comment within this method.\r\n const scalar = baseProb / totalWeight;\r\n multitapEntries.forEach((entry) => {\r\n entry.p = scalar * entry.p;\r\n });\r\n\r\n return baseDistribution.concat(multitapEntries).sort((a, b) => b.p - a.p);\r\n }\r\n\r\n cancel() {\r\n this.clear();\r\n this.sequence.cancel();\r\n }\r\n\r\n clear() {\r\n // TODO: for hint stuff.\r\n }\r\n}", + "import { ActiveKeyBase } from \"keyman/engine/keyboard\";\r\nimport { EventEmitter } from \"eventemitter3\";\r\n\r\nimport { KeyElement } from \"../keyElement.js\";\r\nimport { FlickNameCoordMap, OrderedFlickDirections } from \"../input/gestures/browser/flick.js\";\r\nimport { PhoneKeyTipOrientation } from \"../input/gestures/browser/keytip.js\";\r\nimport { default as VisualKeyboard } from \"../visualKeyboard.js\";\r\nimport { renameSpecialKey } from \"./oskKey.js\";\r\n\r\n/**With edge lengths of 1, to keep flick-text invisible at the start, the\r\n * hypotenuse for an inter-cardinal path is sqrt(2). To keep a perfect circle\r\n * for all flicks, then, requires the straight-edge length for pure cardinal\r\n * paths to match - sqrt(2).\r\n */\r\nconst FLICK_OVERFLOW_OFFSET = 1.4142;\r\n\r\n// If it's a rounding error off of 0, force it to 0.\r\n// Values such as -1.32e-18 have been seen when 0 was expected.\r\nconst coerceZeroes = (val: number) => Math.abs(val) < 1e-10 ? 0 : val;\r\n\r\ninterface EventMap {\r\n preferredOrientation: (orientation: PhoneKeyTipOrientation) => void;\r\n startFade: () => void;\r\n}\r\n\r\nexport class GesturePreviewHost extends EventEmitter {\r\n private readonly div: HTMLDivElement;\r\n private readonly label: HTMLSpanElement;\r\n private readonly hintLabel: HTMLSpanElement;\r\n private readonly previewImgContainer: HTMLDivElement;\r\n\r\n private flickPreviews = new Map;\r\n private flickEdgeLength: number;\r\n\r\n private orientation: PhoneKeyTipOrientation = 'top';\r\n\r\n private onCancel: () => void;\r\n\r\n get element(): HTMLDivElement {\r\n return this.div;\r\n }\r\n\r\n constructor(key: KeyElement, isPhone: boolean, width: number, height: number) {\r\n super();\r\n\r\n const keySpec = key.key.spec;\r\n const edgeLength = this.flickEdgeLength = Math.max(width, height);\r\n\r\n const base = this.div = document.createElement('div');\r\n base.className = base.id = 'kmw-gesture-preview';\r\n\r\n base.style.pointerEvents='none';\r\n\r\n // We want this to be distinct from the base element so that we can scroll it;\r\n // this matters greatly for doing flick things.\r\n const previewImgContainer = this.previewImgContainer = document.createElement('div');\r\n this.previewImgContainer.id = 'kmw-preview-img-container';\r\n\r\n const label = this.label = document.createElement('span');\r\n label.className='kmw-gesture-base-label kmw-key-text';\r\n label.id = 'kmw-gesture-base-label';\r\n previewImgContainer.appendChild(label);\r\n\r\n // Re-use the text value from the base key's label.\r\n label.textContent = key.key.label.textContent;\r\n\r\n this.div.appendChild(this.previewImgContainer);\r\n\r\n if(keySpec.flick) {\r\n const flickSpec = keySpec.flick || {};\r\n\r\n Object.keys(flickSpec).forEach((dir) => {\r\n const flickPreview = document.createElement('div');\r\n flickPreview.className = 'kmw-flick-preview kmw-key-text';\r\n flickPreview.textContent = flickSpec[dir as typeof OrderedFlickDirections[number]].text;\r\n\r\n const ps /* preview style */ = flickPreview.style;\r\n\r\n // is in polar coords, origin toward north, clockwise.\r\n const coords = FlickNameCoordMap.get(dir as typeof OrderedFlickDirections[number]);\r\n\r\n const x = coerceZeroes(-Math.sin(coords[0])); // Put 'e' flick at left\r\n const y = coerceZeroes(Math.cos(coords[0])); // Put 'n' flick at bottom\r\n\r\n ps.width = '100%';\r\n ps.textAlign = 'center';\r\n\r\n if(x < 0) {\r\n ps.right = (-x * FLICK_OVERFLOW_OFFSET * edgeLength) + 'px';\r\n } else if(x > 0) {\r\n ps.left = ( x * FLICK_OVERFLOW_OFFSET * edgeLength) + 'px';\r\n } else {\r\n ps.left = '0px';\r\n }\r\n\r\n ps.height = '100%';\r\n ps.lineHeight = '100%';\r\n\r\n if(y < 0) {\r\n ps.bottom = (-y * FLICK_OVERFLOW_OFFSET * edgeLength) + 'px';\r\n } else if(y > 0) {\r\n ps.top = ( y * FLICK_OVERFLOW_OFFSET * edgeLength) + 'px';\r\n } else {\r\n ps.top = '0px';\r\n }\r\n\r\n this.flickPreviews.set(dir, flickPreview);\r\n previewImgContainer.appendChild(flickPreview);\r\n });\r\n }\r\n\r\n const hintLabel = this.hintLabel = document.createElement('div');\r\n hintLabel.className='kmw-key-popup-icon';\r\n\r\n if(!isPhone) {\r\n hintLabel.textContent = keySpec == keySpec.hintSrc ? keySpec.hint : keySpec.hintSrc?.text;\r\n hintLabel.style.fontWeight= hintLabel.textContent == '\\u2022' ? 'bold' : '';\r\n }\r\n\r\n base.appendChild(hintLabel);\r\n }\r\n\r\n public refreshLayout() {\r\n const compStyle = getComputedStyle(this.div);\r\n const height = Number.parseInt(compStyle.height, 10);\r\n\r\n this.flickPreviews.forEach((ele) => {\r\n ele.style.lineHeight = ele.style.height = `${height}px`;\r\n });\r\n }\r\n\r\n public cancel() {\r\n this.onCancel?.();\r\n this.onCancel = null;\r\n }\r\n\r\n public setCancellationHandler(handler: () => void) {\r\n this.onCancel = handler;\r\n }\r\n\r\n public setMultitapHint(currentSrc: ActiveKeyBase, nextSrc: ActiveKeyBase, vkbd?: VisualKeyboard) {\r\n const current = renameSpecialKey(currentSrc.text, vkbd);\r\n const next = renameSpecialKey(nextSrc.text, vkbd);\r\n\r\n this.label.textContent = current;\r\n this.hintLabel.textContent = next;\r\n\r\n this.label.style.fontFamily = (current != currentSrc.text) ? 'SpecialOSK' : currentSrc.font ?? this.label.style.fontFamily;\r\n this.hintLabel.style.fontFamily = (next != nextSrc.text) ? 'SpecialOSK' : nextSrc.font ?? this.hintLabel.style.fontFamily;\r\n\r\n this.emit('startFade');\r\n\r\n this.clearFlick();\r\n }\r\n\r\n public scrollFlickPreview(x: number, y: number) {\r\n this.clearHint();\r\n\r\n const scrollStyle = this.previewImgContainer.style;\r\n const edge = this.flickEdgeLength * FLICK_OVERFLOW_OFFSET;\r\n\r\n scrollStyle.marginLeft = `${edge * x}px`;\r\n scrollStyle.marginTop = `${edge * y}px`;\r\n\r\n const preferredOrientation = coerceZeroes(y) < 0 ? 'bottom' : 'top';\r\n if(this.orientation != preferredOrientation) {\r\n this.orientation = preferredOrientation;\r\n this.emit('preferredOrientation', preferredOrientation);\r\n }\r\n }\r\n\r\n // These may not exist like this longterm.\r\n public clearFlick() {\r\n this.previewImgContainer.style.marginTop = '0px';\r\n this.previewImgContainer.style.marginLeft = '0px';\r\n\r\n this.previewImgContainer.classList.add('kmw-flick-clear');\r\n }\r\n\r\n public clearHint() {\r\n this.hintLabel.classList.add('kmw-hint-clear');\r\n }\r\n\r\n public clearAll() {\r\n this.clearFlick();\r\n }\r\n}", + "import { EventEmitter } from 'eventemitter3';\r\n\r\nimport {\r\n ActiveKey,\r\n ActiveLayout,\r\n DeviceSpec,\r\n type InternalKeyboardFont,\r\n Keyboard,\r\n KeyboardProperties,\r\n KeyDistribution,\r\n KeyEvent,\r\n Layouts,\r\n StateKeyMap,\r\n ActiveSubKey,\r\n timedPromise,\r\n ActiveKeyBase\r\n} from 'keyman/engine/keyboard';\r\nimport { isEmptyTransform } from 'keyman/engine/js-processor';\r\n\r\nimport { buildCorrectiveLayout } from './correctionLayout.js';\r\nimport { distributionFromDistanceMaps, keyTouchDistances } from './corrections.js';\r\n\r\nimport {\r\n GestureRecognizer,\r\n GestureRecognizerConfiguration,\r\n GestureSequence,\r\n GestureSource,\r\n InputSample,\r\n PaddedZoneSource\r\n} from '@keymanapp/gesture-recognizer';\r\n\r\nimport { createStyleSheet, StylesheetManager } from 'keyman/engine/dom-utils';\r\n\r\nimport { KeyEventHandler, KeyEventResultCallback } from './views/keyEventSource.interface.js';\r\n\r\nimport GlobeHint from './globehint.interface.js';\r\nimport KeyboardView from './components/keyboardView.interface.js';\r\nimport { type KeyElement, getKeyFrom } from './keyElement.js';\r\nimport KeyTip from './keytip.interface.js';\r\nimport OSKKey from './keyboard-layout/oskKey.js';\r\nimport OSKLayer, { LayerLayoutParams } from './keyboard-layout/oskLayer.js';\r\nimport OSKLayerGroup from './keyboard-layout/oskLayerGroup.js';\r\nimport OSKView from './views/oskView.js';\r\nimport { ParsedLengthStyle } from './lengthStyle.js';\r\nimport { defaultFontSize } from './fontSizeUtils.js';\r\nimport PhoneKeyTip from './input/gestures/browser/keytip.js';\r\nimport { TabletKeyTip } from './input/gestures/browser/tabletPreview.js';\r\nimport CommonConfiguration from './config/commonConfiguration.js';\r\n\r\nimport { DEFAULT_GESTURE_PARAMS, GestureParams, gestureSetForLayout } from './input/gestures/specsForLayout.js';\r\n\r\nimport { getViewportScale } from './screenUtils.js';\r\nimport { HeldRepeater } from './input/gestures/heldRepeater.js';\r\nimport SubkeyPopup from './input/gestures/browser/subkeyPopup.js';\r\nimport Multitap from './input/gestures/browser/multitap.js';\r\nimport { GestureHandler } from './input/gestures/gestureHandler.js';\r\nimport Modipress from './input/gestures/browser/modipress.js';\r\nimport Flick from './input/gestures/browser/flick.js';\r\nimport { GesturePreviewHost } from './keyboard-layout/gesturePreviewHost.js';\r\nimport OSKBaseKey from './keyboard-layout/oskBaseKey.js';\r\nimport { OSKResourcePathConfiguration } from 'keyman/engine/interfaces';\r\nimport KEYMAN_VERSION from '@keymanapp/keyman-version';\r\n\r\n/**\r\n * Gesture history data will include each touchpath sample observed during its\r\n * lifetime in addition to its lifetime stats.\r\n */\r\n// @ts-ignore\r\nconst DEBUG_GESTURES: boolean = KEYMAN_VERSION.TIER != 'stable' || KEYMAN_VERSION.VERSION_ENVIRONMENT != '';\r\n\r\n/**\r\n * If greater than zero, `this.gestureEngine.history` & `this.gestureEngine.historyJSON`\r\n * will contain report-data this many of the most-recently completed gesture inputs in\r\n * order of their time of completion.\r\n */\r\nconst DEBUG_HISTORY_COUNT: number = DEBUG_GESTURES ? 10 : 0;\r\n\r\n// #region KeyRuleEffects\r\ninterface KeyRuleEffects {\r\n contextToken?: number,\r\n alteredText?: boolean\r\n};\r\n// #endregion\r\n\r\n// #region VisualKeyboardConfiguration\r\nexport interface VisualKeyboardConfiguration extends CommonConfiguration {\r\n /**\r\n * The Keyman keyboard on which to base the on-screen keyboard being represented.\r\n */\r\n keyboard: Keyboard,\r\n\r\n /**\r\n * Metadata about the keyboard, such as relevant fonts, display name, and language code.\r\n *\r\n * Designed for use with `KeyboardStub` objects, which are defined external to the\r\n * on-screen keyboard module.\r\n */\r\n keyboardMetadata: KeyboardProperties,\r\n\r\n /**\r\n * OSK-internal: the top-most element of the full on-screen keyboard element hierarchy.\r\n *\r\n * May be set to `null` if `isStatic` is `true`.\r\n */\r\n topContainer: HTMLElement,\r\n\r\n /**\r\n * Set to `true` for documentation keyboards, disabling all user-interactivity.\r\n */\r\n isStatic?: boolean,\r\n\r\n /**\r\n * Provide this field with the OSKView's stylesheet per-keyboard manager instance.\r\n *\r\n * Interim developer note: do NOT attach kmwosk.css using the same instance! We don't\r\n * want to remove that one when swapping keyboards.\r\n */\r\n styleSheetManager: StylesheetManager;\r\n\r\n /**\r\n * A promise for loading of the font used by special keys.\r\n */\r\n specialFont?: InternalKeyboardFont;\r\n}\r\n// #endregion\r\n\r\ninterface EventMap {\r\n /**\r\n * Designed to pass key events off to any consuming modules/libraries.\r\n *\r\n * Note: the following code block was originally used to integrate with the keyboard & input\r\n * processors, but it requires entanglement with components external to this OSK module.\r\n */\r\n 'keyevent': KeyEventHandler,\r\n\r\n 'hiderequested': (keyElement: KeyElement) => void,\r\n\r\n 'globekey': (keyElement: KeyElement, on: boolean) => void\r\n}\r\n\r\n// #region VisualKeyboard\r\nexport default class VisualKeyboard extends EventEmitter implements KeyboardView {\r\n /**\r\n * The gesture-engine used to support user interaction with this keyboard.\r\n *\r\n * Note: `stateToken` should match a layer id from this.layoutKeyboard; this helps to\r\n * prevent issue #7173.\r\n */\r\n readonly gestureEngine: GestureRecognizer;\r\n\r\n /**\r\n * Tweakable gesture parameters referenced by supported gestures and the gesture engine.\r\n */\r\n get gestureParams(): GestureParams {\r\n return this.config.gestureParams;\r\n };\r\n\r\n // Legacy alias, maintaining a reference for code built against older\r\n // versions of KMW.\r\n static readonly specialCharacters = OSKKey.specialCharacters;\r\n\r\n /**\r\n * Contains layout properties corresponding to the OSK's layout. Needs to be public\r\n * so that its geometry may be updated on rotations and keyboard resize events, as\r\n * said geometry needs to be accurate for fat-finger probability calculations.\r\n */\r\n kbdLayout: ActiveLayout;\r\n layerGroup: OSKLayerGroup;\r\n\r\n readonly config: VisualKeyboardConfiguration;\r\n\r\n layerLocked: boolean = false;\r\n layerIndex: number = 0; // the index of the default layer\r\n readonly isRTL: boolean;\r\n\r\n readonly isStatic: boolean = false;\r\n _fixedWidthScaling: boolean = false;\r\n _fixedHeightScaling: boolean = true;\r\n\r\n // Stores the base element for this instance of the visual keyboard.\r\n kbdDiv: HTMLDivElement;\r\n styleSheet: HTMLStyleElement;\r\n\r\n /**\r\n * The configured width for this VisualKeyboard. May be `undefined` or `null`\r\n * to allow automatic width scaling.\r\n */\r\n private _width: number;\r\n\r\n /**\r\n * The configured height for this VisualKeyboard. May be `undefined` or `null`\r\n * to allow automatic height scaling.\r\n */\r\n private _height: number;\r\n\r\n /**\r\n * The main VisualKeyboard element's border-width styling.\r\n *\r\n * Assumption: is a fixed, uniform length that doesn't vary between refreshLayout() calls.\r\n */\r\n private _borderWidth: number = 0;\r\n\r\n /**\r\n * The computed width for this VisualKeyboard. May be null if auto sizing\r\n * is allowed and the VisualKeyboard is not currently in the DOM hierarchy.\r\n */\r\n private _computedWidth: number;\r\n\r\n /**\r\n * The computed height for this VisualKeyboard. May be null if auto sizing\r\n * is allowed and the VisualKeyboard is not currently in the DOM hierarchy.\r\n */\r\n private _computedHeight: number;\r\n\r\n // Style-related properties\r\n fontFamily: string;\r\n private _fontSize: ParsedLengthStyle;\r\n // fontSize: string;\r\n\r\n // State-related properties\r\n deleteKey: KeyElement;\r\n deleting: number; // Tracks a timer id for repeated deletions.\r\n nextLayer: string;\r\n currentKey: string;\r\n stateKeys: StateKeyMap = {\r\n K_CAPS: false,\r\n K_NUMLOCK: false,\r\n K_SCROLL: false\r\n };\r\n\r\n // Touch-tracking properties\r\n touchCount: number;\r\n currentTarget: KeyElement;\r\n\r\n // Popup key management\r\n keytip: KeyTip;\r\n gesturePreviewHost: GesturePreviewHost;\r\n globeHint: GlobeHint;\r\n\r\n activeGestures: GestureHandler[] = [];\r\n activeModipress: Modipress = null;\r\n public deferLayout: boolean;\r\n\r\n // The keyboard object corresponding to this VisualKeyboard.\r\n public readonly layoutKeyboard: Keyboard;\r\n public readonly layoutKeyboardProperties: KeyboardProperties;\r\n\r\n get layerId(): string {\r\n return this.layerGroup?.activeLayerId ?? 'default';\r\n }\r\n\r\n set layerId(value: string) {\r\n const changedLayer = value != this.layerId;\r\n if(!this.layerGroup.getLayer(value)) {\r\n throw new Error(`Keyboard ${this.layoutKeyboard.id} does not have a layer with id ${value}`);\r\n } else {\r\n this.layerGroup.activeLayerId = value;\r\n\r\n // Does not exist for documentation keyboards!\r\n if(this.gestureEngine) {\r\n this.gestureEngine.stateToken = value;\r\n }\r\n }\r\n\r\n if(changedLayer && !this.deferLayout) {\r\n this.updateState();\r\n // We changed the active layer, but not any layout property of the keyboard as a whole.\r\n this.layerGroup.refreshLayout(this.constructLayoutParams());\r\n }\r\n }\r\n\r\n get currentLayer(): OSKLayer {\r\n return this.layerGroup?.activeLayer;\r\n }\r\n\r\n // Special keys (for the currently-visible layer)\r\n get lgKey(): KeyElement { // currently, must be visible for the touch language menu.\r\n return this.currentLayer?.globeKey?.btn;\r\n }\r\n\r\n private get hkKey(): KeyElement { // hide keyboard key\r\n return this.currentLayer?.hideKey?.btn;\r\n }\r\n\r\n public get spaceBar(): KeyElement { // also referenced by the touch language menu.\r\n return this.currentLayer?.spaceBarKey?.btn;\r\n }\r\n\r\n //#region VisualKeyboard - constructor and helpers\r\n\r\n /**\r\n * @param {Object} PVK Visual keyboard name\r\n * @param {Object} Lhelp true if OSK defined for this keyboard\r\n * @param {Object} layout0\r\n * @param {Number} kbdBitmask Keyboard modifier bitmask\r\n * Description Generates the base visual keyboard element, prepping for attachment to KMW\r\n */\r\n constructor(config: VisualKeyboardConfiguration) {\r\n super();\r\n\r\n this.config = config; // TODO: replace related parameters.\r\n\r\n this.config.device = config.device || config.hostDevice;\r\n this.config.isEmbedded = config.isEmbedded || false;\r\n\r\n if (config.isStatic) {\r\n this.isStatic = config.isStatic;\r\n }\r\n\r\n this.config.gestureParams ||= {\r\n ...DEFAULT_GESTURE_PARAMS,\r\n };\r\n\r\n this._fixedWidthScaling = this.device.touchable && !this.isStatic;\r\n this._fixedHeightScaling = this.device.touchable && !this.isStatic;\r\n\r\n // Create the collection of HTML elements from the device-dependent layout object\r\n var Lkbd = document.createElement('div');\r\n this.config.styleSheetManager = config.styleSheetManager || new StylesheetManager(Lkbd);\r\n\r\n let layout: ActiveLayout;\r\n if (config.keyboard) {\r\n layout = this.kbdLayout = config.keyboard.layout(config.device.formFactor);\r\n this.layoutKeyboardProperties = config.keyboardMetadata;\r\n this.isRTL = config.keyboard.isRTL;\r\n } else {\r\n // This COULD be called with no backing keyboard; KMW will try to force-show the OSK even without\r\n // a backing keyboard on mobile, using the most generic default layout as the OSK's base.\r\n //\r\n // In KMW's current state, it'd take a major break, though - Processor always has an activeKeyboard,\r\n // even if it's \"hollow\".\r\n let rawLayout = Layouts.buildDefaultLayout(null, null, config.device.formFactor);\r\n layout = this.kbdLayout = ActiveLayout.polyfill(rawLayout, null, config.device.formFactor);\r\n // null will probably need to be replaced with a defined value.\r\n this.layoutKeyboardProperties = null;\r\n this.isRTL = false;\r\n }\r\n\r\n // Override font if specified by keyboard\r\n if ('font' in layout) {\r\n this.fontFamily = layout['font'];\r\n } else {\r\n this.fontFamily = '';\r\n }\r\n\r\n // Now to build the actual layout.\r\n const formFactor = config.device.formFactor;\r\n this.layoutKeyboard = config.keyboard;\r\n if (!this.layoutKeyboard) {\r\n // May occasionally be null in embedded contexts; have seen this when iOS engine sets\r\n // keyboard height during change of keyboards.\r\n this.layoutKeyboard = new Keyboard(null);\r\n }\r\n\r\n this.layerGroup = new OSKLayerGroup(this, this.layoutKeyboard, formFactor);\r\n\r\n // Now that we've properly processed the keyboard's layout, mark it as calibrated.\r\n // TODO: drop the whole 'calibration' thing. The newer layout system supersedes the\r\n // need for it. (Is no longer really used, so the drop ought be clean.)\r\n this.layoutKeyboard.markLayoutCalibrated(formFactor);\r\n\r\n // Append the OSK layer group container element to the containing element\r\n //osk.keyMap = divLayerContainer;\r\n Lkbd.appendChild(this.layerGroup.element);\r\n\r\n // Set base class - OS and keyboard added for Build 360\r\n this.kbdDiv = Lkbd;\r\n\r\n // For 'live' touch keyboards, attach touch-based event handling.\r\n // Needs to occur AFTER this.kbdDiv is initialized.\r\n if (!this.isStatic) {\r\n this.gestureEngine = this.constructGestureEngine();\r\n }\r\n\r\n Lkbd.classList.add(config.device.formFactor, 'kmw-osk-inner-frame');\r\n\r\n // Tag the VisualKeyboard with a CSS class corresponding to its ID.\r\n let kbdID: string = this.layoutKeyboard?.id.replace('Keyboard_','') ?? '';\r\n\r\n const separatorIndex = kbdID.indexOf('::');\r\n if(separatorIndex != -1) { // We used to also test if we were in embedded mode, but... whatever.\r\n // De-namespaces the ID for use with CSS classes.\r\n // Assumes that keyboard IDs may not contain the ':' symbol.\r\n kbdID = kbdID.substring(separatorIndex + 2);\r\n }\r\n\r\n const kbdClassSuffix = 'kmw-keyboard-' + kbdID;\r\n this.element.classList.add(kbdClassSuffix);\r\n }\r\n\r\n private constructGestureEngine(): GestureRecognizer {\r\n const config: GestureRecognizerConfiguration = {\r\n targetRoot: this.element,\r\n // document.body is the event root for mouse interactions b/c we need to track\r\n // when the mouse leaves the VisualKeyboard's hierarchy.\r\n mouseEventRoot: document.body,\r\n // Note: at this point in execution, the value will evaluate to NaN! Height hasn't been set yet.\r\n // BUT: we need to establish the instance now; we can update it later when height _is_ set.\r\n //\r\n // Allow keys to be preserved while the contact point is within banner space + a small fudge-factor.\r\n maxRoamingBounds: new PaddedZoneSource(this.topContainer, [NaN]),\r\n // touchEventRoot: this.element, // is the default\r\n itemIdentifier: (sample, target) => {\r\n /* ALWAYS use the findNearestKey function.\r\n * MDN spec for `target`, which comes from Touch.target for touch-based interactions:\r\n *\r\n * > The read-only target property of the Touch interface returns the (EventTarget) on which the touch contact\r\n * started when it was first placed on the surface, even if the touch point has since moved outside the\r\n * interactive area of that element[...]\r\n *\r\n * Therefore, `target` is for the initial element, not necessarily the one currently under\r\n * the touchpoint - which matters during a 'touchmove'.\r\n */\r\n\r\n return this.layerGroup.findNearestKey(sample);\r\n },\r\n /* When enabled, facilitates investigation of perceived odd behaviors observed on Android devices\r\n in association with issues like #11221 and #11183. \"Recordings\" are only accessible within\r\n the mobile apps via WebView inspection and outside the apps via Developer mode in the browser;\r\n they are not transmitted or uploaded automatically.\r\n */\r\n recordingMode: DEBUG_GESTURES,\r\n historyLength: DEBUG_HISTORY_COUNT\r\n };\r\n\r\n const recognizer = new GestureRecognizer(gestureSetForLayout(this.kbdLayout, this.gestureParams), config);\r\n recognizer.stateToken = this.layerId;\r\n\r\n const sourceTrackingMap: Record,\r\n roamingHighlightHandler: (sample: InputSample) => void,\r\n key: KeyElement,\r\n previewHost: GesturePreviewHost\r\n }> = {};\r\n\r\n const clearActiveGestures = (excludedTouchpointId?: string) => {\r\n for(const identifier of Object.keys(sourceTrackingMap)) {\r\n // Filter out the exclusion if one exists.\r\n if(identifier == excludedTouchpointId) {\r\n continue;\r\n }\r\n\r\n // Any _other_ gesture, though - yeah, that should cancel out.\r\n // Note: this can cancel ongoing modipress gestures, which may trigger an unexpected layer shift.\r\n const entry = sourceTrackingMap[identifier];\r\n entry.source.terminate(true);\r\n }\r\n }\r\n\r\n const gestureHandlerMap = new Map, GestureHandler[]>();\r\n\r\n // Now to set up event-handling links.\r\n // This handler should probably vary based on the keyboard: do we allow roaming touches or not?\r\n recognizer.on('inputstart', (source) => {\r\n // Yay for closure-capture mechanics: we can \"keep a lock\" on this newly-starting\r\n // gesture's highlighted key here.\r\n const previewHost = this.highlightKey(source.currentSample.item, true);\r\n if(previewHost) {\r\n this.gesturePreviewHost?.cancel();\r\n this.gesturePreviewHost = previewHost;\r\n }\r\n\r\n // Make sure we're tracking the source and its currently-selected item (the latter, as we're\r\n // highlighting it)\r\n sourceTrackingMap[source.identifier] = {\r\n source: source,\r\n roamingHighlightHandler: null,\r\n key: source.currentSample.item,\r\n previewHost: previewHost\r\n }\r\n const trackingEntry = sourceTrackingMap[source.identifier];\r\n\r\n const endHighlighting = () => {\r\n // The base call will occur before our \"is this a multitap?\" check otherwise.\r\n // That check will unset the field so that it's unaffected by this check.\r\n timedPromise(0).then(() => {\r\n const previewHost = trackingEntry.previewHost;\r\n\r\n // If we ever allow concurrent previews, check if it exists and matches\r\n // a VisualKeyboard-tracked entry; if so, clear that too.\r\n if(previewHost) {\r\n previewHost.cancel();\r\n this.gesturePreviewHost = null;\r\n trackingEntry.previewHost = null;\r\n }\r\n if(trackingEntry.key) {\r\n this.highlightKey(trackingEntry.key, false);\r\n trackingEntry.key = null;\r\n }\r\n })\r\n }\r\n\r\n // Fix: if flicks enabled, no roaming.\r\n\r\n // Note: GestureSource does not currently auto-terminate if there are no\r\n // remaining matchable gestures. Though, we shouldn't facilitate roaming\r\n // anyway if we've turned it off.\r\n trackingEntry.roamingHighlightHandler = (sample) => {\r\n // Maintain highlighting\r\n const key = sample.item;\r\n const oldKey = sourceTrackingMap[source.identifier].key;\r\n\r\n if(!this.kbdLayout.hasFlicks && key != oldKey) {\r\n this.highlightKey(oldKey, false);\r\n this.gesturePreviewHost?.cancel();\r\n this.gesturePreviewHost = null;\r\n trackingEntry.previewHost = null;\r\n\r\n const previewHost = this.highlightKey(key, true);\r\n if(previewHost) {\r\n this.gesturePreviewHost = previewHost;\r\n trackingEntry.previewHost = previewHost;\r\n }\r\n sourceTrackingMap[source.identifier].key = key;\r\n }\r\n }\r\n\r\n source.path.on('invalidated', endHighlighting);\r\n source.path.on('complete', endHighlighting);\r\n source.path.on('step', trackingEntry.roamingHighlightHandler);\r\n });\r\n\r\n //\r\n recognizer.on('recognizedgesture', (gestureSequence) => {\r\n // If we receive a new gesture while there's an active modipress state, 'lock' it immediately;\r\n // the state has been utilized, so we want to return to the original layer when the modipress\r\n // key is released.\r\n this.activeModipress?.setLocked();\r\n\r\n // The highlighting-disablement part of `onRoamingSourceEnd` is 100% safe, so we can leave\r\n // that running.\r\n\r\n // Drop any roaming-touch specific behaviors here.\r\n\r\n gestureSequence.on('complete', () => {\r\n // Do cleanup - we'll no longer be tracking these, but that's only confirmed now.\r\n // Multitouch does reference tracking data for a source after its completion,\r\n // but only while still permitting new touches. If we're here, that time is over.\r\n for(let id of gestureSequence.allSourceIds) {\r\n // If the original preview host lives on, ensure it's cancelled now.\r\n if(sourceTrackingMap[id]?.previewHost) {\r\n this.gesturePreviewHost = null;\r\n sourceTrackingMap[id].previewHost.cancel();\r\n }\r\n delete sourceTrackingMap[id];\r\n }\r\n });\r\n\r\n // This should probably vary based on the type of gesture.\r\n gestureSequence.on('stage', (gestureStage, configChanger) => {\r\n const existingPreviewHost = gestureSequence.allSourceIds.map((id) => {\r\n return sourceTrackingMap[id]?.previewHost;\r\n }).find((obj) => !!obj);\r\n\r\n const clearPreviewHost = () => {\r\n if(existingPreviewHost) {\r\n existingPreviewHost.cancel();\r\n this.gesturePreviewHost = null;\r\n }\r\n }\r\n\r\n let handlers: GestureHandler[] = gestureHandlerMap.get(gestureSequence);\r\n if(!handlers && existingPreviewHost && !gestureStage.matchedId.includes('flick')) {\r\n existingPreviewHost.clearFlick();\r\n }\r\n\r\n let trackingEntry: typeof sourceTrackingMap[string];\r\n // Disable roaming-touch highlighting (and current highlighting) for all\r\n // touchpoints included in a gesture, even newly-included ones as they occur.\r\n for(let id of gestureStage.allSourceIds) {\r\n const clearRoaming = (trackingEntry: typeof sourceTrackingMap['']) => {\r\n if(trackingEntry.key) {\r\n this.highlightKey(trackingEntry.key, false);\r\n trackingEntry.key = null;\r\n }\r\n\r\n trackingEntry.source.path.off('step', trackingEntry.roamingHighlightHandler);\r\n }\r\n\r\n trackingEntry = sourceTrackingMap[id];\r\n\r\n if(trackingEntry) {\r\n clearRoaming(trackingEntry);\r\n } else {\r\n // May arise during multitaps, as the 'wait' stage instantly accepts new incoming\r\n // sources before they are reported fully to the `inputstart` event.\r\n const _id = id;\r\n timedPromise(0).then(() => {\r\n const tracker = sourceTrackingMap[_id];\r\n if(tracker) {\r\n clearRoaming(tracker);\r\n }\r\n });\r\n }\r\n }\r\n\r\n\r\n // First, if we've configured the gesture to generate a keystroke, let's handle that.\r\n const gestureKey = gestureStage.item;\r\n\r\n const coordSource = gestureStage.sources[0];\r\n const coord: InputSample = coordSource ? coordSource.currentSample : null;\r\n\r\n let keyResult: KeyRuleEffects = null;\r\n\r\n // Longpresses, multitaps and flicks do special key-mapping stuff internally and produce + raise\r\n // their key events directly.\r\n if(gestureKey && !(handlers && handlers[0].directlyEmitsKeys)) {\r\n let correctionKeyDistribution: KeyDistribution;\r\n const baseDistanceMap = this.getSimpleTapCorrectionDistances(coordSource.currentSample, gestureKey.key.spec as ActiveKey);\r\n\r\n if(handlers) {\r\n // Certain gestures (especially flicks) like to consider the base layout as part\r\n // of their corrective-distribution calculations.\r\n //\r\n // May be `null` for gestures that don't need custom correction handling,\r\n // such as modipresses or initial/simple-tap keystrokes.\r\n correctionKeyDistribution = handlers[0].currentStageKeyDistribution(baseDistanceMap);\r\n }\r\n\r\n if(!correctionKeyDistribution) {\r\n correctionKeyDistribution = distributionFromDistanceMaps(baseDistanceMap);\r\n }\r\n\r\n // If there's no active modipress, but there WAS one when the longpress started,\r\n // keep the layer locked for the keystroke.\r\n const shouldLockLayer = !this.layerLocked && handlers && (handlers[0] instanceof SubkeyPopup) && handlers[0].shouldLockLayer;\r\n try {\r\n shouldLockLayer && this.lockLayer(true);\r\n // Once the best coord to use for fat-finger calculations has been determined:\r\n keyResult = this.modelKeyClick(gestureStage.item, coord, correctionKeyDistribution);\r\n } finally {\r\n shouldLockLayer && this.lockLayer(false);\r\n }\r\n\r\n }\r\n\r\n // Outside of passing keys along... the handling of later stages is delegated\r\n // to gesture-specific handling classes.\r\n if(gestureSequence.stageReports.length > 1 && gestureStage.matchedId != 'modipress-end') {\r\n return;\r\n }\r\n\r\n // So, if this is the first stage, this is where we need to perform that delegation.\r\n const baseItem = gestureSequence.stageReports[0].item;\r\n\r\n // -- Scratch-space as gestures start becoming integrated --\r\n // Reordering may follow at some point.\r\n //\r\n // Potential long-term idea: only handle the first stage; delegate future stages to\r\n // specialized handlers for the remainder of the sequence.\r\n // Should work for modipresses, too... I think.\r\n if(gestureStage.matchedId == 'special-key-start') {\r\n if(gestureKey.key.spec.baseKeyID == 'K_BKSP') {\r\n // There shouldn't be a preview host for special keys... but it doesn't hurt to add the check.\r\n clearPreviewHost();\r\n\r\n // Possible enhancement: maybe update the held location for the backspace if there's movement?\r\n // But... that seems pretty low-priority.\r\n //\r\n // Merely constructing the instance is enough; it'll link into the sequence's events and\r\n // handle everything that remains for the backspace from here.\r\n handlers = [new HeldRepeater(gestureSequence, () => this.modelKeyClick(gestureKey, coord))];\r\n } else if(gestureKey.key.spec.baseKeyID == \"K_LOPT\") { // globe key\r\n gestureSequence.on('complete', () => {\r\n gestureKey.key.highlight(false);\r\n this.emit('globekey', gestureKey, false);\r\n });\r\n\r\n // Cancel all other gesture sources; a language-menu interaction voids all previously-active\r\n // gestures that haven't completed.\r\n clearActiveGestures(coordSource.identifier);\r\n\r\n // Re-highlight the key - it was auto de-highlighted upon stage-select.\r\n gestureKey.key.highlight(true);\r\n }\r\n } else if(gestureStage.matchedId.indexOf('longpress') > -1) {\r\n clearPreviewHost();\r\n\r\n // Matches: 'longpress', 'longpress-reset'.\r\n // Likewise.\r\n handlers = [new SubkeyPopup(\r\n gestureSequence,\r\n configChanger,\r\n this,\r\n gestureSequence.stageReports[0].sources[0].baseItem,\r\n this.gestureParams\r\n )];\r\n\r\n // baseItem is sometimes null during a keyboard-swap... for app/browser touch-based language menus.\r\n // not ideal, but it is what it is; just let it pass by for now.\r\n } else if(baseItem?.key.spec.multitap && (gestureStage.matchedId == 'initial-tap' || gestureStage.matchedId == 'multitap' || gestureStage.matchedId == 'modipress-start')) {\r\n // Detach the lifetime of the preview from the current touch.\r\n trackingEntry.previewHost = null;\r\n\r\n gestureSequence.on('complete', () => {\r\n clearPreviewHost();\r\n })\r\n\r\n // Past that, mere construction of the class for delegation is enough.\r\n handlers = [new Multitap(gestureSequence, this, baseItem, keyResult.contextToken, existingPreviewHost)];\r\n } else if(gestureStage.matchedId.indexOf('flick') > -1) {\r\n handlers = [new Flick(\r\n gestureSequence,\r\n configChanger,\r\n this,\r\n gestureSequence.stageReports[0].sources[0].baseItem,\r\n this.gestureParams,\r\n existingPreviewHost\r\n )];\r\n } else if(gestureStage.matchedId.includes('modipress') && gestureStage.matchedId.includes('-start')) {\r\n // There shouldn't be a preview host for modipress keys... but it doesn't hurt to add the check.\r\n clearPreviewHost();\r\n\r\n if(this.layerLocked) {\r\n console.warn(\"Unexpected state: modipress start attempt during an active modipress\");\r\n } else {\r\n handlers ||= [];\r\n\r\n const modipressHandler = new Modipress(gestureSequence, this, () => {\r\n const index = handlers.indexOf(modipressHandler);\r\n if(index > -1) {\r\n handlers.splice(index, 1);\r\n }\r\n this.activeModipress = null;\r\n });\r\n\r\n handlers.push(modipressHandler);\r\n this.activeModipress = modipressHandler;\r\n }\r\n } else {\r\n // Probably an initial-tap or a simple-tap.\r\n clearPreviewHost();\r\n }\r\n\r\n if(handlers) {\r\n this.activeGestures = this.activeGestures.concat(handlers);\r\n gestureHandlerMap.set(gestureSequence, handlers);\r\n gestureSequence.on('complete', () => {\r\n const completingHandlers = this.activeGestures.filter(handler => handlers.includes(handler));\r\n this.activeGestures = this.activeGestures.filter((handler) => !handlers.includes(handler));\r\n\r\n // Robustness check; make extra-sure that we can safely leave a modipress state.\r\n completingHandlers.forEach((handler) => {\r\n if(handler instanceof Modipress) {\r\n handler.cancel();\r\n }\r\n });\r\n });\r\n }\r\n })\r\n });\r\n\r\n return recognizer;\r\n }\r\n\r\n public get element(): HTMLDivElement {\r\n return this.kbdDiv;\r\n }\r\n\r\n public get device(): DeviceSpec {\r\n return this.config.device;\r\n }\r\n\r\n public get hostDevice(): DeviceSpec {\r\n return this.config.hostDevice;\r\n }\r\n\r\n public get fontRootPath(): string {\r\n return this.config.pathConfig.fonts;\r\n }\r\n\r\n public get styleSheetManager(): StylesheetManager {\r\n return this.config.styleSheetManager;\r\n }\r\n\r\n public get topContainer(): HTMLElement {\r\n return this.config.topContainer;\r\n }\r\n\r\n public get isEmbedded(): boolean {\r\n return this.config.isEmbedded;\r\n }\r\n\r\n public postInsert(): void { }\r\n\r\n /**\r\n * The configured width for this VisualKeyboard. May be `undefined` or `null`\r\n * to allow automatic width scaling.\r\n */\r\n get width(): number {\r\n return this._width;\r\n }\r\n\r\n /**\r\n * The configured height for this VisualKeyboard. May be `undefined` or `null`\r\n * to allow automatic height scaling.\r\n */\r\n get height(): number {\r\n return this._height;\r\n }\r\n\r\n get layoutWidth(): ParsedLengthStyle {\r\n if (this.usesFixedWidthScaling) {\r\n let baseWidth = this.width;\r\n baseWidth -= this._borderWidth * 2;\r\n return ParsedLengthStyle.inPixels(baseWidth);\r\n } else {\r\n return ParsedLengthStyle.forScalar(1);\r\n }\r\n }\r\n\r\n get layoutHeight(): ParsedLengthStyle {\r\n if (this.usesFixedHeightScaling) {\r\n let baseHeight = this.height;\r\n baseHeight -= this._borderWidth * 2;\r\n return ParsedLengthStyle.inPixels(baseHeight);\r\n } else {\r\n return ParsedLengthStyle.forScalar(1);\r\n }\r\n }\r\n\r\n get internalHeight(): ParsedLengthStyle {\r\n if (this.usesFixedHeightScaling) {\r\n // Touch OSKs may apply internal padding to prevent row cropping at the edges.\r\n // ... why not precompute both, rather than recalculate each time?\r\n // - appears to contribute to layout reflow costs on layer swaps!\r\n return ParsedLengthStyle.inPixels(this.layoutHeight.val - this._borderWidth * 2 - this.layerGroup.verticalPadding);\r\n } else {\r\n return ParsedLengthStyle.forScalar(1);\r\n }\r\n }\r\n\r\n get fontSize(): ParsedLengthStyle {\r\n if (!this._fontSize) {\r\n this._fontSize = new ParsedLengthStyle('1em');\r\n }\r\n return this._fontSize;\r\n }\r\n\r\n set fontSize(value: ParsedLengthStyle) {\r\n this._fontSize = value;\r\n this.kbdDiv.style.fontSize = value.styleString;\r\n }\r\n\r\n /**\r\n * Uses fixed scaling for widths of internal elements, rather than relative,\r\n * percent-based scaling.\r\n */\r\n public get usesFixedWidthScaling(): boolean {\r\n return this._fixedWidthScaling;\r\n }\r\n\r\n public set usesFixedWidthScaling(val: boolean) {\r\n this._fixedWidthScaling = val;\r\n }\r\n\r\n /**\r\n * Uses fixed scaling for heights of internal elements, rather than relative,\r\n * percent-based scaling.\r\n */\r\n public get usesFixedHeightScaling(): boolean {\r\n return this._fixedHeightScaling;\r\n }\r\n\r\n public set usesFixedHeightScaling(val: boolean) {\r\n this._fixedHeightScaling = val;\r\n }\r\n\r\n /**\r\n * Denotes if the VisualKeyboard or its containing OSKView / OSKManager uses\r\n * fixed positioning.\r\n */\r\n public get usesFixedPositioning(): boolean {\r\n let node: HTMLElement = this.element;\r\n while (node) {\r\n if (getComputedStyle(node).position == 'fixed') {\r\n return true;\r\n } else {\r\n node = node.offsetParent as HTMLElement;\r\n }\r\n }\r\n\r\n return false;\r\n }\r\n\r\n /**\r\n * Sets & tracks the size of the VisualKeyboard's primary element.\r\n * @param width\r\n * @param height\r\n * @param pending Set to `true` if called during a resizing interaction\r\n */\r\n public setSize(width?: number, height?: number, pending?: boolean) {\r\n this._width = width;\r\n this._height = height;\r\n\r\n if (this.kbdDiv) {\r\n this.kbdDiv.style.width = width ? this._width + 'px' : '';\r\n this.kbdDiv.style.height = height ? this._height + 'px' : '';\r\n\r\n if (!this.device.touchable && height) {\r\n this.fontSize = new ParsedLengthStyle((this._height / 8) + 'px');\r\n }\r\n\r\n if (!pending) {\r\n this.refreshLayout();\r\n }\r\n }\r\n }\r\n //#endregion\r\n\r\n //#region VisualKeyboard - OSK touch handlers\r\n getTouchCoordinatesOnKeyboard(input: InputSample) {\r\n // `input` is already in keyboard-local coordinates. It's not scaled, though.\r\n let offsetCoords = { x: input.targetX, y: input.targetY };\r\n\r\n // The layer group's element always has the proper width setting, unlike kbdDiv itself.\r\n offsetCoords.x /= this.layerGroup.element.offsetWidth;\r\n offsetCoords.y /= this.kbdDiv.offsetHeight;\r\n\r\n return offsetCoords;\r\n }\r\n\r\n /**\r\n * Builds the fat-finger distribution used by predictive text as its source for likelihood\r\n * of alternate keystroke sequences.\r\n * @param input The input coordinate of the event that led to use of this function\r\n * @param keySpec The spec of the key directly triggered by the input event. May be for a subkey.\r\n * @returns\r\n */\r\n getSimpleTapCorrectionDistances(input: InputSample, keySpec?: ActiveKey): Map {\r\n // Note: if subkeys are active, they will still be displayed at this time.\r\n let touchKbdPos = this.getTouchCoordinatesOnKeyboard(input);\r\n let layerGroup = this.layerGroup.element; // Always has proper dimensions, unlike kbdDiv itself.\r\n const width = layerGroup.offsetWidth, height = this.kbdDiv.offsetHeight;\r\n\r\n // Prevent NaN breakages.\r\n if (!width || !height) {\r\n return new Map();\r\n }\r\n\r\n let kbdAspectRatio = width / height;\r\n\r\n const correctiveLayout = buildCorrectiveLayout(this.kbdLayout.getLayer(this.layerId), kbdAspectRatio);\r\n return keyTouchDistances(touchKbdPos, correctiveLayout);\r\n }\r\n\r\n /**\r\n * Get the current key target from the touch point element within the key\r\n *\r\n * @param {Object} t element at touch point\r\n * @return {Object} the key element (or null)\r\n **/\r\n keyTarget(target: HTMLElement | EventTarget): KeyElement {\r\n let t = target;\r\n\r\n try {\r\n if (t) {\r\n if (t.classList.contains('kmw-key')) {\r\n return getKeyFrom(t);\r\n }\r\n if (t.parentNode && (t.parentNode as HTMLElement).classList.contains('kmw-key')) {\r\n return getKeyFrom(t.parentNode);\r\n }\r\n if (t.firstChild && (t.firstChild as HTMLElement).classList.contains('kmw-key')) {\r\n return getKeyFrom(t.firstChild);\r\n }\r\n }\r\n } catch (ex) { }\r\n return null;\r\n }\r\n\r\n /**\r\n * Repeat backspace as long as the backspace key is held down\r\n **/\r\n repeatDelete: () => void = function (this: VisualKeyboard) {\r\n if (this.deleting) {\r\n this.modelKeyClick(this.deleteKey);\r\n this.deleting = window.setTimeout(this.repeatDelete, 100);\r\n }\r\n }.bind(this);\r\n\r\n /**\r\n * Cancels any active repeatDelete() timeouts, ensuring that\r\n * repeating backspace operations are properly terminated.\r\n */\r\n cancelDelete() {\r\n // Clears the delete-repeating timeout.\r\n if (this.deleting) {\r\n window.clearTimeout(this.deleting);\r\n }\r\n this.deleting = 0;\r\n }\r\n //#endregion\r\n\r\n modelKeyClick(e: KeyElement, input?: InputSample, keyDistribution?: KeyDistribution) {\r\n let keyEvent = this.initKeyEvent(e);\r\n\r\n if (input) {\r\n keyEvent.source = input;\r\n }\r\n if(keyDistribution) {\r\n keyEvent.keyDistribution = keyDistribution;\r\n }\r\n\r\n return this.raiseKeyEvent(keyEvent, e);\r\n }\r\n\r\n initKeyEvent(e: KeyElement) {\r\n // Turn off key highlighting (or preview)\r\n this.highlightKey(e, false);\r\n\r\n // Future note: we need to refactor osk.OSKKeySpec to instead be a 'tag field' for\r\n // keyboards.ActiveKey. (Prob with generics, allowing the Web-only parts to\r\n // be fully specified within the tag.)\r\n //\r\n // Would avoid the type shenanigans needed here because of our current type-abuse setup\r\n // for key spec tracking.\r\n let keySpec = (e['key'] ? e['key'].spec : null) as unknown as ActiveKey;\r\n if (!keySpec) {\r\n return null;\r\n }\r\n\r\n // Return the event object.\r\n return this.keyEventFromSpec(keySpec);\r\n }\r\n\r\n keyEventFromSpec(keySpec: ActiveKey | ActiveSubKey) {\r\n //let core = com.keyman.singleton.core; // only singleton-based ref currently needed here.\r\n\r\n // Start: mirrors _GetKeyEventProperties\r\n\r\n // First check the virtual key, and process shift, control, alt or function keys\r\n //let Lkc = keySpec.constructKeyEvent(core.keyboardProcessor, this.device);\r\n let Lkc = this.layoutKeyboard.constructKeyEvent(keySpec, this.device, this.stateKeys);\r\n\r\n /* In case of \"fun\" edge cases caused by JS's single-threadedness & event processing queue.\r\n *\r\n * Should a touch occur on an OSK key during active JS execution that results in a change\r\n * of the active keyboard, it's possible for an OSK key to be evaluated against an\r\n * unexpected, non-matching keyboard - one that could even be `null`!\r\n *\r\n * So, we mark the keyboard backing the OSK as the 'correct' keyboard for this key.\r\n */\r\n Lkc.srcKeyboard = this.layoutKeyboard;\r\n\r\n // End - mirrors _GetKeyEventProperties\r\n\r\n // Return the event object.\r\n return Lkc;\r\n }\r\n\r\n // cancel = function(e) {} //cancel event is never generated by iOS\r\n\r\n /**\r\n * Function _UpdateVKShiftStyle\r\n * Scope Private\r\n * @param {string=} layerId\r\n * Description Updates the OSK's visual style for any toggled state keys\r\n */\r\n _UpdateVKShiftStyle(layerId?: string) {\r\n var i;\r\n //let core = com.keyman.singleton.core;\r\n\r\n if (!layerId) {\r\n layerId = this.layerId;\r\n }\r\n\r\n const layer = this.layerGroup.getLayer(layerId);\r\n if (!layer) {\r\n return;\r\n }\r\n\r\n if(this.gestureEngine) {\r\n this.gestureEngine.stateToken = layerId;\r\n }\r\n\r\n // So... through KMW 14, we actually never tracked the capsKey, numKey, and scrollKey\r\n // properly for keyboard-defined layouts - only _default_, desktop-style layouts.\r\n //\r\n // We _could_ remedy this, but then... touch keyboards like khmer_angkor actually\r\n // repurpose certain state keys, and in an inconsistent manner at that.\r\n // Considering the potential complexity of touch layouts, with multiple possible\r\n // layer-shift keys, it's likely best to just leave things as they are for now.\r\n if (!this.layoutKeyboard?.usesDesktopLayoutOnDevice(this.device)) {\r\n return;\r\n }\r\n\r\n // Set the on/off state of any visible state keys.\r\n const states = ['K_CAPS', 'K_NUMLOCK', 'K_SCROLL'] as const;\r\n const keys = [layer.capsKey, layer.numKey, layer.scrollKey];\r\n\r\n for (i = 0; i < keys.length; i++) {\r\n // Skip any keys not in the OSK!\r\n if (keys[i] == null) {\r\n continue;\r\n }\r\n\r\n keys[i].setToggleState(this.stateKeys[states[i]]);\r\n }\r\n }\r\n\r\n updateStateKeys(stateKeys: StateKeyMap) {\r\n for(let key of Object.keys(this.stateKeys)) {\r\n this.stateKeys[key as keyof StateKeyMap] = stateKeys[key as keyof StateKeyMap];\r\n }\r\n\r\n this._UpdateVKShiftStyle();\r\n }\r\n\r\n /**\r\n * Add or remove a class from a keyboard key (when touched or clicked)\r\n * or add a key preview for phone devices\r\n *\r\n * @param {Object} key key affected\r\n * @param {boolean} on add or remove highlighting\r\n **/\r\n highlightKey(key: KeyElement, on: boolean): GesturePreviewHost {\r\n // Do not change element class unless a key\r\n if (!key || !key.key || (key.className == '') || (key.className.indexOf('kmw-key-row') >= 0)) {\r\n return null;\r\n }\r\n\r\n // For phones, use key preview rather than highlighting the key,\r\n const usePreview = key.key.allowsKeyTip();\r\n const modalVizActive = this.activeGestures.find((handler) => handler.hasModalVisualization);\r\n\r\n // If the subkey menu (or a different modal visualization) is active, do not show the key tip -\r\n // even if for a different contact point.\r\n on = modalVizActive ? false : on;\r\n\r\n key.key.highlight(on);\r\n if(!on) {\r\n return null;\r\n }\r\n\r\n if (usePreview) {\r\n if(this.gesturePreviewHost) {\r\n return null; // do not override lingering previews for still-active gestures.\r\n } else {\r\n return this.showGesturePreview(key);\r\n }\r\n } else {\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Use of `getComputedStyle` is ideal, but in many of our use cases its preconditions are not met.\r\n * This function allows us to calculate the font size in those situations.\r\n */\r\n getKeyEmFontSize(): ParsedLengthStyle {\r\n if (!this.fontSize) {\r\n return new ParsedLengthStyle('0px');\r\n }\r\n\r\n if (this.device.formFactor == 'desktop') {\r\n let keySquareScale = 0.8; // Set in kmwosk.css, is relative.\r\n return this.fontSize.scaledBy(keySquareScale);\r\n } else {\r\n const emSizeStr = getComputedStyle(document.body).fontSize;\r\n const emSize = new ParsedLengthStyle(emSizeStr);\r\n\r\n let emScale = 1;\r\n if (!this.isStatic) {\r\n // Double-check against the font scaling applied to the _Box element.\r\n if (this.fontSize.absolute) {\r\n return this.fontSize;\r\n } else {\r\n emScale = this.fontSize.val;\r\n }\r\n }\r\n return emSize.scaledBy(emScale);\r\n }\r\n }\r\n\r\n updateState() {\r\n // May happen for desktop-oriented keyboards that neglect to specify a touch layout.\r\n // See `test_chirality.js` from the unit-test keyboard suite, which tests keystrokes\r\n // using modifiers that lack corresponding visual-layout representation.\r\n if (!this.currentLayer) {\r\n return;\r\n }\r\n\r\n this.nextLayer = this.layerId;\r\n\r\n if (this.currentLayer.nextlayer) {\r\n this.nextLayer = this.currentLayer.nextlayer;\r\n }\r\n\r\n // Will toggle the CSS style `display` attribute for affected layers.\r\n this.layerGroup.activeLayerId = this.layerId;\r\n\r\n // Most functions that call this one often indicate a change in modifier\r\n // or state key state. Keep it updated!\r\n this._UpdateVKShiftStyle();\r\n }\r\n\r\n /**\r\n * Used to refresh the VisualKeyboard's geometric layout and key sizes\r\n * when needed.\r\n */\r\n refreshLayout() {\r\n if(this.deferLayout) {\r\n return;\r\n }\r\n\r\n /*\r\n Phase 1: calculations possible at the start without triggering _any_ additional layout reflow.\r\n (A single, initial reflow may happen depending on DOM manipulations before this method...,\r\n but no extras until phase 2.)\r\n */\r\n let device = this.device;\r\n\r\n var fs = 1.0;\r\n // TODO: Logically, this should be needed for Android, too - may need to be changed for the next version!\r\n if (device.OS == DeviceSpec.OperatingSystem.iOS && !this.isEmbedded) {\r\n fs = fs / getViewportScale(this.device.formFactor);\r\n }\r\n\r\n /*\r\n Phase 2: first self-triggered reflow - locking in the keyboard's base property styling.\r\n */\r\n let gs = this.kbdDiv.style;\r\n if (this.usesFixedHeightScaling && this.height) {\r\n // Sets the layer group to the correct height.\r\n gs.height = gs.maxHeight = this.height + 'px';\r\n }\r\n\r\n // The font-scaling applied by default for this instance on its root element.\r\n // Layer-group font-scaling is applied separately.\r\n gs.fontSize = this.fontSize.scaledBy(fs).styleString;\r\n\r\n // Phase 3: reflow from top-level getComputedStyle calls\r\n\r\n // Step 1: have the necessary conditions been met?\r\n const fixedSize = this.width && this.height;\r\n const computedStyle = getComputedStyle(this.kbdDiv);\r\n const groupStyle = getComputedStyle(this.layerGroup.element);\r\n\r\n const isInDOM = computedStyle.height != '' && computedStyle.height != 'auto';\r\n const isGroupInDOM = groupStyle.height != '' && groupStyle.height != 'auto';\r\n\r\n if (computedStyle.border) {\r\n this._borderWidth = new ParsedLengthStyle(computedStyle.borderWidth).val;\r\n }\r\n\r\n // Step 2: determine basic layout geometry, refresh things that might update.\r\n\r\n if (fixedSize) {\r\n this._computedWidth = this.width;\r\n this._computedHeight = this.height;\r\n } else if (isInDOM) {\r\n this._computedWidth = parseInt(computedStyle.width, 10);\r\n this._computedHeight = parseInt(computedStyle.height, 10);\r\n } else if (isGroupInDOM) {\r\n // May occur for documentation-keyboards, which are detached from their VisualKeyboard base.\r\n this._computedWidth = parseInt(groupStyle.width, 10);\r\n this._computedHeight = parseInt(groupStyle.height, 10);\r\n } else {\r\n // Cannot perform layout operations!\r\n return;\r\n }\r\n\r\n // Phase 3: Refresh the layout of the layer-group and active layer.\r\n this.layerGroup.refreshLayout(this.constructLayoutParams());\r\n\r\n // Step 4: recalculate gesture parameter values\r\n // We do this _after_ \"Phase 3\" so that this.currentLayer.rowHeight is guaranteed\r\n // to be set. Also, skip for doc-keyboards, since they don't do gestures.\r\n if(!this.isStatic) {\r\n const paddingZone = this.gestureEngine.config.maxRoamingBounds as PaddedZoneSource;\r\n paddingZone.updatePadding([-0.333 * this.currentLayer.rowHeight]);\r\n\r\n /*\r\n Note: longpress.flickDist needs to be no greater than flick.startDist.\r\n Otherwise, the longpress up-flick shortcut will not work on keys that\r\n support flick gestures. (Such as sil_euro_latin 3.0+)\r\n\r\n Since it's also based on the purely northward component, it's best to\r\n have it be slightly lower. 80% of flick.startDist gives a range of\r\n about 37 degrees to each side before a flick-start would win, while\r\n 70.7% gives 45 degrees.\r\n\r\n (The range _will_ be notably tighter on keys with both longpresses and\r\n flicks as a result.)\r\n */\r\n this.gestureParams.longpress.flickDistStart = 0.24 * this.currentLayer.rowHeight;\r\n this.gestureParams.flick.startDist = 0.30 * this.currentLayer.rowHeight;\r\n this.gestureParams.flick.dirLockDist = 0.35 * this.currentLayer.rowHeight;\r\n this.gestureParams.flick.triggerDist = 0.75 * this.currentLayer.rowHeight;\r\n this.gestureParams.longpress.flickDistFinal = 0.75 * this.currentLayer.rowHeight;\r\n }\r\n }\r\n\r\n private constructLayoutParams(): LayerLayoutParams {\r\n return {\r\n keyboardWidth: this._computedWidth - 2 * this._borderWidth,\r\n keyboardHeight: this._computedHeight - 2 * this._borderWidth - this.layerGroup.verticalPadding,\r\n widthStyle: this.layoutWidth,\r\n heightStyle: this.internalHeight,\r\n baseEmFontSize: this.getKeyEmFontSize(),\r\n layoutFontSize: new ParsedLengthStyle(this.layerGroup.element.style.fontSize),\r\n spacebarText: this.layoutKeyboardProperties?.displayName ?? '(System keyboard)'\r\n };\r\n }\r\n\r\n // Appears to be abandoned now - candidate for removal in future.\r\n /*private*/ computedAdjustedOskHeight(allottedHeight: number): number {\r\n if (!this.layerGroup) {\r\n return allottedHeight;\r\n }\r\n\r\n /*\r\n Note: these may not be fully preprocessed yet!\r\n\r\n However, any \"empty row bug\" preprocessing has been applied, and that's\r\n what we care about here.\r\n */\r\n const layers = this.layerGroup.spec.layer;\r\n let oskHeight = 0;\r\n\r\n // In case the keyboard's layers have differing row counts, we check them all for the maximum needed oskHeight.\r\n for (const layerID in layers) {\r\n const layer = layers[layerID];\r\n let nRows = layer.row.length;\r\n let rowHeight = Math.floor(allottedHeight / (nRows == 0 ? 1 : nRows));\r\n let layerHeight = nRows * rowHeight;\r\n\r\n if (layerHeight > oskHeight) {\r\n oskHeight = layerHeight;\r\n }\r\n }\r\n\r\n // This isn't set anywhere else; it's a legacy part of the original methods.\r\n const oskPad = 0;\r\n let oskPaddedHeight = oskHeight + oskPad;\r\n\r\n return oskPaddedHeight;\r\n }\r\n\r\n /**\r\n * Append a style sheet for the current keyboard if needed for specifying an embedded font\r\n * or to re-apply the default element font\r\n *\r\n **/\r\n appendStyleSheet() {\r\n //let util = com.keyman.singleton.util;\r\n\r\n var activeKeyboard = this.layoutKeyboard;\r\n var activeStub = this.layoutKeyboardProperties;\r\n\r\n // First remove any existing keyboard style sheet\r\n if (this.styleSheet && this.styleSheet.parentNode) {\r\n this.styleSheet.parentNode.removeChild(this.styleSheet);\r\n }\r\n\r\n // For help.keyman.com, sometimes we aren't given a stub for the keyboard.\r\n // We can't get the keyboard's fonts correct in that case, but we can\r\n // at least proceed safely.\r\n var kfd = activeStub?.textFont, ofd = activeStub?.oskFont;\r\n\r\n // Add and define style sheets for embedded fonts if necessary (each font-face style will only be added once)\r\n this.styleSheetManager.addStyleSheetForFont(kfd, this.fontRootPath, this.device.OS);\r\n this.styleSheetManager.addStyleSheetForFont(ofd, this.fontRootPath, this.device.OS);\r\n\r\n if(this.config.specialFont) {\r\n this.styleSheetManager.addStyleSheetForFont(this.config.specialFont, '', this.device.OS);\r\n }\r\n\r\n // Build the style string to USE the fonts and append (or replace) the font style sheet\r\n // Note: Some browsers do not download the font-face font until it is applied,\r\n // so must apply style before testing for font availability\r\n // Extended to allow keyboard-specific custom styles for Build 360\r\n var customStyle = this.addFontStyle(kfd, ofd);\r\n if (activeKeyboard != null && typeof (activeKeyboard.oskStyling) == 'string') // KMEW-129\r\n customStyle = customStyle + activeKeyboard.oskStyling;\r\n\r\n if(customStyle) {\r\n this.styleSheet = createStyleSheet(customStyle); //Build 360\r\n this.styleSheetManager.linkStylesheet(this.styleSheet);\r\n }\r\n\r\n // Once any related fonts are loaded, we can re-adjust key-cap scaling.\r\n this.styleSheetManager.allLoadedPromise().then(() => {\r\n // All existing font-precalculations will need to be reset, as the font\r\n // was previously unavailable.\r\n this.layerGroup.resetPrecalcFontSizes();\r\n this.refreshLayout()\r\n });\r\n }\r\n\r\n /**\r\n * Add or replace the style sheet used to set the font for input elements and OSK\r\n *\r\n * @param {Object} kfd KFont font descriptor\r\n * @param {Object} ofd OSK font descriptor (if any)\r\n * @return {string}\r\n *\r\n **/\r\n addFontStyle(kfd: InternalKeyboardFont, ofd: InternalKeyboardFont): string {\r\n let s: string = '';\r\n\r\n let family = (fd: InternalKeyboardFont) => fd.family.replace(/\\u0022/g, '').replace(/,/g, '\",\"');\r\n\r\n // Set font family for OSK text, suggestion text\r\n if (kfd || ofd) {\r\n s = `\r\n.kmw-key-text {\r\n font-family: \"${family(ofd || kfd)}\";\r\n}\r\n\r\n.kmw-suggestion-text {\r\n font-family: \"${family(kfd || ofd)}\";\r\n}\r\n`;\r\n }\r\n\r\n // Return the style string\r\n return s;\r\n }\r\n\r\n /**\r\n * Create copy of the OSK that can be used for embedding in documentation or help\r\n * The currently active keyboard will be returned if PInternalName is null\r\n *\r\n * @param {Keyboard} PKbd the keyboard object to be displayed\r\n * @param {KeyboardProperties} kbdProperties the metadata stub for the keyboard\r\n * @param {Object} pathConfig an OSK path-configuration instance\r\n * @param {string=} argFormFactor layout form factor, defaulting to 'desktop'\r\n * @param {(string|number)=} argLayerId name or index of layer to show, defaulting to 'default'\r\n * @param {number} height Target height for the rendered keyboard\r\n * (currently required for legacy reasons)\r\n * @return {Object} DIV object with filled keyboard layer content\r\n */\r\n static buildDocumentationKeyboard(\r\n PKbd: Keyboard,\r\n kbdProperties: KeyboardProperties,\r\n pathConfig: OSKResourcePathConfiguration,\r\n argFormFactor: DeviceSpec.FormFactor,\r\n argLayerId: string,\r\n height: number\r\n ): HTMLElement { // I777\r\n if (!PKbd) {\r\n return null;\r\n }\r\n\r\n var formFactor = (typeof (argFormFactor) == 'undefined' ? 'desktop' : argFormFactor) as DeviceSpec.FormFactor,\r\n layerId = (typeof (argLayerId) == 'undefined' ? 'default' : argLayerId),\r\n device: {\r\n formFactor?: DeviceSpec.FormFactor,\r\n OS?: DeviceSpec.OperatingSystem,\r\n touchable?: boolean\r\n } = {};\r\n\r\n // Device emulation for target documentation.\r\n device.formFactor = formFactor;\r\n if (formFactor != 'desktop') {\r\n device.OS = DeviceSpec.OperatingSystem.iOS;\r\n device.touchable = true;\r\n } else {\r\n device.OS = DeviceSpec.OperatingSystem.Windows;\r\n device.touchable = false;\r\n }\r\n\r\n let layout = PKbd.layout(formFactor);\r\n\r\n const deviceSpec = new DeviceSpec('other', device.formFactor, device.OS, device.touchable);\r\n let kbdObj = new VisualKeyboard({\r\n keyboard: PKbd,\r\n keyboardMetadata: kbdProperties,\r\n hostDevice: deviceSpec,\r\n isStatic: true,\r\n topContainer: null,\r\n pathConfig: pathConfig,\r\n styleSheetManager: null,\r\n specialFont: {\r\n family: 'SpecialOSK',\r\n files: [`${pathConfig.resources}/osk/keymanweb-osk.ttf`],\r\n path: '' // Not actually used.\r\n }\r\n });\r\n\r\n kbdObj.layerGroup.element.className = kbdObj.kbdDiv.className; // may contain multiple classes\r\n kbdObj.layerGroup.element.classList.add(device.formFactor + '-static');\r\n\r\n let kbd = kbdObj.kbdDiv.childNodes[0] as HTMLDivElement; // Gets the layer group.\r\n\r\n // Models CSS classes hosted on the OSKView in normal operation. We can't do this on the main\r\n // layer-group element because of the CSS rule structure for keyboard styling.\r\n //\r\n // For example, `.ios .kmw-keyboard-sil_cameroon_azerty` requires the element with the keyboard\r\n // ID to be in a child of an element with the .ios class.\r\n let classWrapper = document.createElement('div');\r\n classWrapper.classList.add(device.OS.toLowerCase(), device.formFactor);\r\n\r\n // Select the layer to display, and adjust sizes\r\n if (layout != null) {\r\n kbdObj.layerId = layerId;\r\n kbdObj.layerGroup.activeLayerId = layerId;\r\n\r\n // This still feels fairly hacky... but something IS needed to constrain the height.\r\n // There are plans to address related concerns through some of the later aspects of\r\n // the Web OSK-Core design.\r\n kbdObj.setSize(800, height); // Probably need something for width, too, rather than\r\n kbdObj.fontSize = defaultFontSize(deviceSpec, height, false);\r\n classWrapper.style.fontSize = kbdObj.element.style.fontSize;\r\n\r\n // assuming 100%.\r\n kbdObj.refreshLayout(); // Necessary for the row heights to be properly set!\r\n kbd.style.height = kbdObj.kbdDiv.style.height;\r\n kbd.style.maxHeight = kbdObj.kbdDiv.style.maxHeight;\r\n } else {\r\n kbd.innerHTML = \"

No \" + formFactor + \" layout is defined for \" + PKbd.name + \".

\";\r\n }\r\n // Add a faint border\r\n kbd.style.border = '1px solid #ccc';\r\n\r\n kbdObj.updateState(); // double-ensure that the 'default' layer is properly displayed.\r\n\r\n // Once the element is inserted into the DOM, refresh the layout so that proper text scaling may apply.\r\n const detectAndHandleInsertion = async () => {\r\n if(document.contains(kbd)) {\r\n // Yay, insertion!\r\n\r\n try {\r\n // Wait for full loading/connection before manipulating stylesheet locations.\r\n await kbdObj.styleSheetManager.allLoadedPromise();\r\n\r\n const mainSheet = kbdObj.styleSheet;\r\n if(mainSheet) {\r\n kbd.appendChild(mainSheet);\r\n }\r\n\r\n // Unlinking sheets will mutate the original array; make a backup\r\n // copy of the array to iterate over.\r\n const sheets = [].concat(kbdObj.styleSheetManager.sheets);\r\n\r\n /*\r\n * Re-attach the font stylesheets... to the element.\r\n * They need re-attachment for the fonts to work properly for inactive keyboards.\r\n *\r\n * For future reference: as of early 2024, Chrome does not support\r\n * @font-face style declaration within Shadow DOM elements. The\r\n * declaration needs to be part of the main HTML doc.\r\n *\r\n * References:\r\n * - https://stackoverflow.com/q/63710162\r\n * - https://github.com/mdn/interactive-examples/issues/887#issuecomment-432418008\r\n */\r\n for(let sheet of sheets) {\r\n if(sheet == mainSheet) {\r\n // Don't need to relocate the custom stylesheet.\r\n continue;\r\n } else if(sheet.href) {\r\n // Don't relocate kmwosk.css or similar.\r\n continue;\r\n }\r\n kbdObj.styleSheetManager.unlink(sheet);\r\n document.head.appendChild(sheet);\r\n }\r\n\r\n // // Should we ever remove ALL related stylesheets during .shutdown()...\r\n // kbdObj.config.styleSheetManager = new StylesheetManager(kbdObj.element);\r\n\r\n // We refresh the full layout so that font-size is properly detected & stored\r\n // on the documentation keyboard.\r\n kbdObj.refreshLayout();\r\n\r\n // We no longer need a reference to the constructing VisualKeyboard, so we should let\r\n // it clean up its stylesheet links. This detaches the stylesheet, though.\r\n kbdObj.styleSheet = null; // is directly checked in shutdown; prevent removal.\r\n kbdObj.shutdown();\r\n } finally {\r\n insertionObserver.disconnect();\r\n }\r\n }\r\n }\r\n\r\n const insertionObserver = new MutationObserver(detectAndHandleInsertion);\r\n insertionObserver.observe(document.body, {\r\n childList: true,\r\n subtree: true\r\n });\r\n\r\n // Ensure the main keyboard root is the first child element of the top-level div.\r\n classWrapper.append(kbd);\r\n\r\n // Ensure that the OSK's style-sheet is included by the top-level div standing in for the OSKView.\r\n for(let sheetFile of OSKView.STYLESHEET_FILES) {\r\n const sheetHref = `${pathConfig.resources}/osk/${sheetFile}`;\r\n const sheet = kbdObj.styleSheetManager.linkExternalSheet(sheetHref, true);\r\n sheet.parentNode.removeChild(sheet);\r\n classWrapper.appendChild(sheet);\r\n }\r\n\r\n // Make sure that the stylesheet is attached, now that the keyboard-doc's been inserted.\r\n // The stylesheet is currently built + constructed in the same code that attaches it to\r\n // the page.\r\n kbdObj.appendStyleSheet();\r\n\r\n // Unset the width + height we used thus far; this method's consumer may choose to rescale\r\n // the returned element. If so, we don't want to use our outdated value by mistake.\r\n //\r\n // While `kbdObj.setSize()` could be used in theory, it _also_ unsets the element styling.\r\n // We actually wish to _leave_ this styling in place - one of our parameters is `height`, and\r\n // it should remain in place in the styling on the output element as the default in case\r\n // the consumer _doesn't_ add styling afterward.\r\n delete kbdObj._width;\r\n delete kbdObj._height;\r\n\r\n return classWrapper;\r\n }\r\n\r\n onHide() {\r\n // Remove highlighting from hide keyboard key, if applied\r\n if (this.hkKey) {\r\n this.highlightKey(this.hkKey, false);\r\n }\r\n }\r\n\r\n optionKey(e: KeyElement, keyName: string, keyDown: boolean) {\r\n if (keyName.indexOf('K_LOPT') >= 0) {\r\n this.emit('globekey', e, keyDown);\r\n } else if (keyName.indexOf('K_ROPT') >= 0) {\r\n if (keyDown) {\r\n this.emit('hiderequested', e);\r\n }\r\n }\r\n };\r\n\r\n /**\r\n * Add (or remove) the gesture preview (if KeymanWeb on a phone device)\r\n *\r\n * @param {Object} key HTML key element\r\n * @param {boolean} on show or hide\r\n * @returns A GesturePreviewHost instance usable for visualizing a gesture.\r\n */\r\n showGesturePreview(key: KeyElement) {\r\n const tip = this.keytip;\r\n\r\n const layoutParams = this.constructLayoutParams();\r\n const keyWidth = layoutParams.keyboardWidth * key.key.spec.proportionalWidth;\r\n const keyHeight = layoutParams.keyboardHeight / this.currentLayer.rows.length;\r\n const previewHost = new GesturePreviewHost(key, this.device.formFactor == 'phone', keyWidth, keyHeight);\r\n\r\n if (tip == null) {\r\n const baseKey = key.key as OSKBaseKey;\r\n baseKey.setPreview(previewHost);\r\n } else {\r\n tip.show(key, true, previewHost);\r\n }\r\n\r\n previewHost.refreshLayout();\r\n\r\n return previewHost;\r\n };\r\n\r\n /**\r\n * Create a key preview element for phone devices\r\n */\r\n createKeyTip() {\r\n if (this.keytip == null) {\r\n if(this.device.formFactor == 'phone') {\r\n // For now, should only be true (in production) when keyman.isEmbedded == true.\r\n let constrainPopup = this.isEmbedded;\r\n this.keytip = new PhoneKeyTip(this, constrainPopup);\r\n } else {\r\n this.keytip = new TabletKeyTip(this);\r\n }\r\n }\r\n\r\n // Always append to _Box (since cleared during OSK Load)\r\n if (this.keytip && this.keytip.element) {\r\n this.element.appendChild(this.keytip.element);\r\n }\r\n };\r\n\r\n createGlobeHint(): GlobeHint {\r\n if(this.config.embeddedGestureConfig.createGlobeHint) {\r\n return this.config.embeddedGestureConfig.createGlobeHint(this);\r\n } else {\r\n return null;\r\n }\r\n }\r\n\r\n shutdown() {\r\n // Prevents style-sheet pollution from multiple keyboard swaps.\r\n if(this.styleSheet && this.styleSheet.parentNode) {\r\n this.styleSheet.parentNode.removeChild(this.styleSheet);\r\n }\r\n\r\n this.activeGestures.forEach((handler) => handler.cancel());\r\n\r\n if(this.gestureEngine) {\r\n this.gestureEngine.destroy();\r\n }\r\n\r\n if(this.deleting) {\r\n window.clearTimeout(this.deleting);\r\n }\r\n\r\n this.keytip?.show(null, false, null);\r\n }\r\n\r\n lockLayer(enable: boolean) {\r\n this.layerLocked = enable;\r\n }\r\n\r\n raiseKeyEvent(keyEvent: KeyEvent, e: KeyElement): KeyRuleEffects {\r\n // Exclude menu and OSK hide keys from normal click processing\r\n if(keyEvent.kName == 'K_LOPT' || keyEvent.kName == 'K_ROPT') {\r\n this.optionKey(e, keyEvent.kName, true);\r\n return {};\r\n }\r\n\r\n let callbackData: KeyRuleEffects = {};\r\n\r\n const keyEventCallback: KeyEventResultCallback = (result, error) => {\r\n callbackData.contextToken = result?.transcription?.token;\r\n const transform = result?.transcription?.transform;\r\n callbackData.alteredText = result && (!transform || isEmptyTransform(transform));\r\n }\r\n\r\n if(this.layerLocked) {\r\n keyEvent.kNextLayer = this.layerId;\r\n }\r\n\r\n this.emit('keyevent', keyEvent, keyEventCallback);\r\n\r\n return callbackData;\r\n }\r\n // #endregion VisualKeyboard\r\n}\r\n", + "import { EventEmitter } from 'eventemitter3';\r\n\r\ninterface EventMap {\r\n activate: (flag: boolean) => void;\r\n}\r\n\r\n/**\r\n * Used to encapsulate activation logic for the on-screen keyboadr, conditionally activating\r\n * and deactivating it based on specified conditions.\r\n */\r\nexport default abstract class Activator extends EventEmitter {\r\n /**\r\n * For certain sub-types, this may be set to `false` to \"turn activation off\", putting\r\n * the `Activator` in a state that ignores changes to any other conditions.\r\n */\r\n abstract get enabled(): boolean;\r\n\r\n abstract set enabled(flag: boolean);\r\n\r\n /**\r\n * When `true`, indicates that the listener should activate / become visible.\r\n */\r\n abstract get activate(): boolean;\r\n\r\n /**\r\n * When `true` and `activate` is `false`, indicates that changing the value of `enabled`\r\n * will result in activation.\r\n */\r\n abstract get conditionsMet(): boolean;\r\n}\r\n\r\nexport class StaticActivator extends Activator {\r\n get enabled(): boolean {\r\n return true;\r\n }\r\n\r\n set enabled(value: boolean) {\r\n // does nothing; it's static.\r\n }\r\n\r\n get activate(): boolean {\r\n return true;\r\n }\r\n\r\n get conditionsMet(): boolean {\r\n return true;\r\n }\r\n}", + "import { ManagedPromise } from \"@keymanapp/web-utils\";\r\n\r\nexport default class TouchEventPromiseMap {\r\n private map: Map> = new Map();\r\n\r\n // Used to\r\n public promiseForTouchpoint(id: number): ManagedPromise {\r\n if(!this.map.get(id)) {\r\n this.map.set(id, new ManagedPromise());\r\n }\r\n\r\n return this.map.get(id); // touchpoint identifiers are unique during a page's lifetime.\r\n }\r\n\r\n public maintainTouches(list: TouchList) {\r\n let keys = Array.from(this.map.keys());\r\n\r\n for(let i=0; i < list.length; i++) {\r\n let pos = keys.indexOf(list.item(i).identifier);\r\n if(pos != -1) {\r\n keys.splice(pos, 1);\r\n }\r\n }\r\n\r\n // Any remaining entries of `keys` are no longer in the map!\r\n for(let endedKey of keys) {\r\n this.map.get(endedKey).resolve();\r\n this.map.delete(endedKey);\r\n }\r\n }\r\n}", + "import { EventEmitter } from 'eventemitter3';\r\n\r\nimport { BannerView } from '../banner/bannerView.js';\r\nimport { BannerController } from '../banner/bannerController.js';\r\nimport OSKViewComponent from '../components/oskViewComponent.interface.js';\r\nimport EmptyView from '../components/emptyView.js';\r\nimport HelpPageView from '../components/helpPageView.js';\r\nimport KeyboardView from '../components/keyboardView.interface.js';\r\nimport VisualKeyboard from '../visualKeyboard.js';\r\nimport { LengthStyle, ParsedLengthStyle } from '../lengthStyle.js';\r\nimport { type KeyElement } from '../keyElement.js';\r\n\r\nimport {\r\n Codes,\r\n DeviceSpec,\r\n Keyboard,\r\n KeyboardProperties,\r\n ManagedPromise,\r\n type MinimalCodesInterface\r\n} from 'keyman/engine/keyboard';\r\nimport { createUnselectableElement, getAbsoluteX, getAbsoluteY, StylesheetManager } from 'keyman/engine/dom-utils';\r\nimport { EventListener, LegacyEventEmitter } from 'keyman/engine/events';\r\nimport { type MutableSystemStore, type SystemStoreMutationHandler } from 'keyman/engine/js-processor';\r\n\r\nimport Configuration from '../config/viewConfiguration.js';\r\nimport Activator, { StaticActivator } from './activator.js';\r\nimport TouchEventPromiseMap from './touchEventPromiseMap.js';\r\nimport { KeyEventHandler, KeyEventSourceInterface } from './keyEventSource.interface.js';\r\nimport { DEFAULT_GESTURE_PARAMS, GestureParams } from '../input/gestures/specsForLayout.js';\r\n\r\n// These will likely be eliminated from THIS file at some point.\\\r\n\r\nexport type OSKPos = {'left'?: number, 'top'?: number};\r\n\r\nexport type OSKRect = {\r\n 'left'?: number,\r\n 'top'?: number,\r\n 'width'?: number,\r\n 'height'?: number,\r\n 'nosize'?: boolean,\r\n 'nomove'?: boolean\r\n};\r\n\r\n/**\r\n * Definition for OSK events documented at\r\n * https://help.keyman.com/DEVELOPER/ENGINE/WEB/16.0/reference/events/.\r\n */\r\nexport interface LegacyOSKEventMap {\r\n 'configclick'(obj: {}): void;\r\n 'helpclick'(obj: {}): void;\r\n 'resizemove'(obj: {}): void;\r\n 'show'(obj: {\r\n x?: number,\r\n y?: number,\r\n userLocated?: boolean\r\n }): void;\r\n 'hide'(obj: {\r\n HiddenByUser?: boolean\r\n }): void;\r\n}\r\n\r\n/**\r\n * For now, these will serve as undocumented, internal events. We need a proper\r\n * design round and discussion before we consider promoting them to long-term,\r\n * documented official API events.\r\n */\r\nexport interface EventMap {\r\n /**\r\n * Designed to pass key events off to any consuming modules/libraries.\r\n *\r\n * Note: the following code block was originally used to integrate with the keyboard & input\r\n * processors, but it requires entanglement with components external to this OSK module.\r\n */\r\n 'keyevent': KeyEventHandler,\r\n\r\n /**\r\n * Indicates that the globe key has either been pressed (`on` == `true`)\r\n * or released (`on` == `false`).\r\n */\r\n globekey: (e: KeyElement, on: boolean) => void;\r\n\r\n /**\r\n * A virtual keystroke corresponding to a \"hide\" command has been received.\r\n */\r\n hiderequested: (key: KeyElement) => void;\r\n\r\n /**\r\n * Signals the special command to display the engine's version + build number.\r\n */\r\n showbuild: () => void;\r\n\r\n // While the next two are near-duplicates of the legacy event `resizemove`, these\r\n // have the advantage of providing a Promise for the end of the ongoing user\r\n // interaction. We need that Promise for focus-management.\r\n\r\n /**\r\n * Signals that the OSK is being moved by the user via a drag operation.\r\n *\r\n * The provided Promise will resolve once the drag operation is complete.\r\n *\r\n * Note that position-restoration (unpinning the OSK) is treated as a drag-move\r\n * event. It resolves near-instantly.\r\n */\r\n dragmove: (promise: Promise) => void;\r\n\r\n /**\r\n * Signals that the OSK is being resized via a drag operation (on a resize 'handle').\r\n *\r\n * The provided Promise will resolve once the resize operation is complete.\r\n */\r\n resizemove: (promise: Promise) => void;\r\n\r\n /**\r\n * Signals that either the mouse or an active touchpoint is interacting with the OSK.\r\n *\r\n * The provided `Promise` will resolve once the corresponding interaction is complete.\r\n * Note that for touch events, more than one touchpoint may coexist, each with its own\r\n * corresponding call of this event and corresponding `Promise`.\r\n */\r\n pointerinteraction: (promise: Promise) => void;\r\n}\r\n\r\nexport function getResourcePath(config: Configuration) {\r\n let resourcePathExt = 'osk/';\r\n if(config.isEmbedded) {\r\n resourcePathExt = '';\r\n }\r\n return `${config.pathConfig.resources}/${resourcePathExt}`\r\n}\r\n\r\nexport default abstract class OSKView\r\n extends EventEmitter\r\n implements MinimalCodesInterface, KeyEventSourceInterface {\r\n _Box: HTMLDivElement;\r\n readonly legacyEvents = new LegacyEventEmitter();\r\n\r\n // #region Key code definition aliases for legacy keyboards (that expect window['keyman']['osk'].___)\r\n get keyCodes() {\r\n return Codes.keyCodes;\r\n }\r\n\r\n get modifierCodes() {\r\n return Codes.modifierCodes;\r\n }\r\n\r\n get modifierBitmasks() {\r\n return Codes.modifierBitmasks;\r\n }\r\n\r\n get stateBitmasks() {\r\n return Codes.stateBitmasks;\r\n }\r\n // #endregion\r\n\r\n headerView: OSKViewComponent;\r\n bannerView: BannerView; // Which implements OSKViewComponent\r\n keyboardView: KeyboardView; // Which implements OSKViewComponent\r\n footerView: OSKViewComponent;\r\n\r\n private _bannerController: BannerController;\r\n\r\n private kbdStyleSheetManager: StylesheetManager;\r\n private uiStyleSheetManager: StylesheetManager;\r\n\r\n private config: Configuration;\r\n private deferLayout: boolean;\r\n\r\n private _boxBaseTouchStart: (e: TouchEvent) => boolean;\r\n private _boxBaseTouchEventCancel: (e: TouchEvent) => boolean;\r\n\r\n private keyboardData: {\r\n keyboard: Keyboard,\r\n metadata: KeyboardProperties\r\n };\r\n\r\n /**\r\n * Provides the current parameterization for timings and distances used by\r\n * any gesture-supporting keyboards. Changing properties of its objects will\r\n * automatically update keyboards to use the new configuration.\r\n *\r\n * If `gestureParams` was set in the configuration object passed in at\r\n * construction time, this will be the same instance.\r\n */\r\n get gestureParams(): GestureParams {\r\n return this.config.gestureParams;\r\n }\r\n\r\n /**\r\n * The configured width for this OSKManager. May be `undefined` or `null`\r\n * to allow automatic width scaling.\r\n */\r\n private _width: ParsedLengthStyle;\r\n\r\n /**\r\n * The configured height for this OSKManager. May be `undefined` or `null`\r\n * to allow automatic height scaling.\r\n */\r\n private _height: ParsedLengthStyle;\r\n\r\n /**\r\n * The computed width for this OSKManager. May be null if auto sizing\r\n * is allowed and the OSKManager is not currently in the DOM hierarchy.\r\n */\r\n private _computedWidth: number;\r\n\r\n /**\r\n * The computed height for this OSKManager. May be null if auto sizing\r\n * is allowed and the OSKManager is not currently in the DOM hierarchy.\r\n */\r\n private _computedHeight: number;\r\n\r\n /**\r\n * The base font size to use for hosted `Banner`s and `VisualKeyboard`\r\n * instances.\r\n */\r\n private _baseFontSize: ParsedLengthStyle;\r\n\r\n private needsLayout: boolean = true;\r\n\r\n private _animatedHideTimeout: number;\r\n\r\n private mouseEnterPromise?: ManagedPromise;\r\n private touchEventPromiseManager = new TouchEventPromiseMap();\r\n\r\n static readonly STYLESHEET_FILES = ['kmwosk.css', 'globe-hint.css'];\r\n\r\n constructor(configuration: Configuration) {\r\n super();\r\n\r\n // Clone the config; do not allow object references to be altered later.\r\n this.config = configuration = {...configuration};\r\n // If gesture parameters were not provided in advance, initialize them from defaults.\r\n this.config.gestureParams ||= DEFAULT_GESTURE_PARAMS;\r\n\r\n // `undefined` is falsy, but we want a `true` default behavior for this config property.\r\n if(this.config.allowHideAnimations === undefined) {\r\n this.config.allowHideAnimations = true;\r\n }\r\n\r\n this.config.device = configuration.device || configuration.hostDevice;\r\n\r\n this.config.isEmbedded = configuration.isEmbedded || false;\r\n this.config.embeddedGestureConfig = configuration.embeddedGestureConfig || {};\r\n this.config.activator.on('activate', this.activationListener);\r\n\r\n // OSK initialization - create DIV and set default styles\r\n this._Box = createUnselectableElement('div'); // Container for OSK (Help DIV, displayed when user clicks Help icon)\r\n this.kbdStyleSheetManager = new StylesheetManager(this._Box, this.config.doCacheBusting || false);\r\n this.uiStyleSheetManager = new StylesheetManager(this._Box);\r\n\r\n // Initializes the two constant OSKComponentView fields.\r\n this.bannerView = new BannerView();\r\n this.bannerView.events.on('bannerchange', () => this.refreshLayout());\r\n this._Box.appendChild(this.bannerView.element);\r\n\r\n this._bannerController = new BannerController(this.bannerView, this.hostDevice, this.config.predictionContextManager);\r\n\r\n this.keyboardView = this._GenerateKeyboardView(null, null);\r\n this._Box.appendChild(this.keyboardView.element);\r\n\r\n // Install the default OSK stylesheets - but don't have it managed by the keyboard-specific stylesheet manager.\r\n // We wish to maintain kmwosk.css whenever keyboard-specific styles are reset/removed.\r\n // Temp-hack: embedded products prefer their stylesheet, etc linkages without the /osk path component.\r\n const resourcePath = getResourcePath(this.config);\r\n\r\n for(let sheetFile of OSKView.STYLESHEET_FILES) {\r\n const sheetHref = `${resourcePath}${sheetFile}`;\r\n this.uiStyleSheetManager.linkExternalSheet(sheetHref);\r\n }\r\n\r\n this.setBaseMouseEventListeners();\r\n if(this.hostDevice.touchable) {\r\n this.setBaseTouchEventListeners();\r\n }\r\n\r\n this._Box.style.display = 'none';\r\n }\r\n\r\n protected get configuration(): Configuration {\r\n return this.config;\r\n }\r\n\r\n public get bannerController(): BannerController {\r\n return this._bannerController;\r\n }\r\n\r\n public get hostDevice(): DeviceSpec {\r\n return this.config.hostDevice;\r\n }\r\n\r\n public get fontRootPath(): string {\r\n return this.config.pathConfig.fonts;\r\n }\r\n\r\n public get isEmbedded(): boolean {\r\n return this.config.isEmbedded;\r\n }\r\n\r\n private setBaseMouseEventListeners() {\r\n this._Box.onmouseenter = (e) => {\r\n if(this.mouseEnterPromise) {\r\n // The chain was somehow interrupted, with the mouseleave never occurring!\r\n this.mouseEnterPromise.resolve();\r\n }\r\n\r\n this.mouseEnterPromise = new ManagedPromise();\r\n this.emit('pointerinteraction', this.mouseEnterPromise.corePromise);\r\n };\r\n\r\n this._Box.onmouseleave = (e) => {\r\n this.mouseEnterPromise.resolve();\r\n this.mouseEnterPromise = null;\r\n // focusAssistant.setMaintainingFocus(false);\r\n };\r\n }\r\n\r\n private removeBaseMouseEventListeners() {\r\n this._Box.onmouseenter = null;\r\n this._Box.onmouseleave = null;\r\n }\r\n\r\n private setBaseTouchEventListeners() {\r\n // To prevent touch event default behaviour on mobile devices\r\n let commonPrevention = function(e: TouchEvent) {\r\n if(e.cancelable) {\r\n e.preventDefault();\r\n }\r\n e.stopPropagation();\r\n return false;\r\n }\r\n\r\n this._boxBaseTouchEventCancel = (e) => {\r\n this.touchEventPromiseManager.maintainTouches(e.touches);\r\n return commonPrevention(e);\r\n };\r\n\r\n this._boxBaseTouchStart = (e) => {\r\n for(let i = 0; i < e.changedTouches.length; i++) {\r\n let promise = this.touchEventPromiseManager.promiseForTouchpoint(e.changedTouches[i].identifier);\r\n this.emit('pointerinteraction', promise.corePromise);\r\n }\r\n\r\n this.touchEventPromiseManager.maintainTouches(e.touches);\r\n return commonPrevention(e);\r\n }\r\n\r\n this._Box.addEventListener('touchstart', this._boxBaseTouchStart, false);\r\n this._Box.addEventListener('touchmove', this._boxBaseTouchEventCancel, false);\r\n this._Box.addEventListener('touchend', this._boxBaseTouchEventCancel, false);\r\n this._Box.addEventListener('touchcancel', this._boxBaseTouchEventCancel, false);\r\n }\r\n\r\n private removeBaseTouchEventListeners() {\r\n if(!this._boxBaseTouchEventCancel) {\r\n return;\r\n }\r\n\r\n this._Box.removeEventListener('touchstart', this._boxBaseTouchStart, false);\r\n this._Box.removeEventListener('touchmove', this._boxBaseTouchEventCancel, false);\r\n this._Box.removeEventListener('touchend', this._boxBaseTouchEventCancel, false);\r\n this._Box.removeEventListener('touchcancel', this._boxBaseTouchEventCancel, false);\r\n\r\n this._boxBaseTouchEventCancel = null;\r\n this._boxBaseTouchStart = null;\r\n }\r\n\r\n // TODO: activeTarget has been 'moved' to activationModel.activationCondition (for TwoStateActivation instances).\r\n // Loosely speaking, anyway.\r\n\r\n\r\n\r\n public get targetDevice(): DeviceSpec {\r\n return this.config.device;\r\n }\r\n\r\n public set targetDevice(spec: DeviceSpec) {\r\n if(this.allowsDeviceChange(spec)) {\r\n this.config.device = spec;\r\n this.loadActiveKeyboard();\r\n } else {\r\n console.error(\"May not change target device for this OSKView type.\");\r\n }\r\n }\r\n\r\n protected allowsDeviceChange(newSpec: DeviceSpec): boolean {\r\n return false;\r\n }\r\n\r\n /**\r\n * Gets and sets the activation state model used to control presentation of the OSK.\r\n */\r\n get activationModel(): Activator {\r\n return this.config.activator;\r\n }\r\n\r\n set activationModel(model: Activator) {\r\n if(!model) {\r\n throw new Error(\"The activation model may not be set to null or undefined!\");\r\n }\r\n\r\n this.config.activator.off('activate', this.activationListener);\r\n model.on('activate', this.activationListener);\r\n\r\n this.config.activator = model;\r\n\r\n this.commonCheckAndDisplay();\r\n }\r\n\r\n public get mayDisable(): boolean {\r\n if(this.hostDevice.touchable) {\r\n return false;\r\n }\r\n\r\n if(this.activeKeyboard?.keyboard.isCJK) {\r\n return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n private readonly activationListener = (flag: boolean) => {\r\n // CJK override: may not be disabled, as the CJK elements are required.\r\n if(!this.mayDisable && !this.activationModel.enabled) {\r\n this.activationModel.off('activate', this.activationListener);\r\n try {\r\n this.activationModel.enabled = true;\r\n } finally {\r\n this.activationModel.on('activate', this.activationListener);\r\n }\r\n }\r\n this.commonCheckAndDisplay();\r\n };\r\n\r\n /**\r\n * A property denoting whether or not the OSK will be presented when it meets all\r\n * other activation conditions.\r\n *\r\n * Is equivalent to `.activationModel.enabled`.\r\n */\r\n get displayIfActive(): boolean {\r\n return this.activationModel.enabled;\r\n }\r\n\r\n /**\r\n * Used by the activation model's event listenerss and properties as a common helper;\r\n * they rely on this function to manage presentation (showing / hiding) of the OSK.\r\n */\r\n private commonCheckAndDisplay() {\r\n if(this.activationModel.activate && this.activeKeyboard) {\r\n this.present();\r\n } else {\r\n this.startHide(false);\r\n }\r\n }\r\n\r\n public get vkbd(): VisualKeyboard {\r\n if(this.keyboardView instanceof VisualKeyboard) {\r\n return this.keyboardView;\r\n } else {\r\n return null;\r\n }\r\n }\r\n\r\n public get banner(): BannerView { // Maintains old reference point used by embedding apps.\r\n return this.bannerView;\r\n }\r\n\r\n /**\r\n * The configured width for this VisualKeyboard. May be `undefined` or `null`\r\n * to allow automatic width scaling.\r\n */\r\n get width(): ParsedLengthStyle {\r\n return this._width;\r\n }\r\n\r\n /**\r\n * The configured height for this VisualKeyboard. May be `undefined` or `null`\r\n * to allow automatic height scaling.\r\n */\r\n get height(): ParsedLengthStyle {\r\n return this._height;\r\n }\r\n\r\n /**\r\n * The computed width for this VisualKeyboard. May be null if auto sizing\r\n * is allowed and the VisualKeyboard is not currently in the DOM hierarchy.\r\n */\r\n get computedWidth(): number {\r\n // Computed during layout operations; allows caching instead of continuous recomputation.\r\n if(this.needsLayout) {\r\n this.refreshLayout();\r\n }\r\n return this._computedWidth;\r\n }\r\n\r\n /**\r\n * The computed height for this VisualKeyboard. May be null if auto sizing\r\n * is allowed and the VisualKeyboard is not currently in the DOM hierarchy.\r\n */\r\n get computedHeight(): number {\r\n // Computed during layout operations; allows caching instead of continuous recomputation.\r\n if(this.needsLayout) {\r\n this.refreshLayout();\r\n }\r\n return this._computedHeight;\r\n }\r\n\r\n /**\r\n * The top-level style string for the font size used by the predictive banner\r\n * and the primary keyboard visualization elements.\r\n */\r\n get baseFontSize(): string {\r\n return this.parsedBaseFontSize?.styleString || '';\r\n }\r\n\r\n protected get parsedBaseFontSize(): ParsedLengthStyle {\r\n if(!this._baseFontSize) {\r\n this._baseFontSize = OSKView.defaultFontSize(this.targetDevice, this.computedHeight, this.isEmbedded);\r\n }\r\n\r\n return this._baseFontSize;\r\n }\r\n\r\n public static defaultFontSize(device: DeviceSpec, computedHeight: number, isEmbedded: boolean): ParsedLengthStyle {\r\n if(device.touchable) {\r\n const fontScale = device.formFactor == 'phone'\r\n ? 1.6 * (isEmbedded ? 0.65 : 0.6) * 1.2 // Combines original scaling factor with one previously applied to the layer group.\r\n : 2; // iPad or Android tablet\r\n return ParsedLengthStyle.special(fontScale, 'em');\r\n } else {\r\n return computedHeight ? ParsedLengthStyle.inPixels(computedHeight / 8) : undefined;\r\n }\r\n }\r\n\r\n public get activeKeyboard(): {\r\n keyboard: Keyboard,\r\n metadata: KeyboardProperties\r\n } {\r\n return this.keyboardData;\r\n }\r\n\r\n public set activeKeyboard(keyboardData: {\r\n keyboard: Keyboard,\r\n metadata: KeyboardProperties\r\n }) {\r\n this.keyboardData = keyboardData;\r\n this.loadActiveKeyboard();\r\n\r\n if(this.keyboardData?.keyboard.isCJK) {\r\n this.activationModel.enabled = true;\r\n }\r\n }\r\n\r\n private computeFrameHeight(): number {\r\n return (this.headerView?.layoutHeight.val || 0) + (this.footerView?.layoutHeight.val || 0);\r\n }\r\n\r\n setSize(width?: number | LengthStyle, height?: number | LengthStyle, pending?: boolean) {\r\n let mutatedFlag = false;\r\n\r\n let parsedWidth: ParsedLengthStyle;\r\n let parsedHeight: ParsedLengthStyle;\r\n\r\n if(!width && width !== 0) {\r\n return;\r\n }\r\n\r\n if(!height && height !== 0) {\r\n return;\r\n }\r\n\r\n if(Number.isFinite(width as number)) {\r\n parsedWidth = ParsedLengthStyle.inPixels(width as number);\r\n } else {\r\n parsedWidth = new ParsedLengthStyle(width as LengthStyle);\r\n }\r\n\r\n if(Number.isFinite(height as number)) {\r\n parsedHeight = ParsedLengthStyle.inPixels(height as number);\r\n } else {\r\n parsedHeight = new ParsedLengthStyle(height as LengthStyle);\r\n }\r\n\r\n if(width && height) {\r\n mutatedFlag = !this._width || !this._height;\r\n\r\n mutatedFlag = mutatedFlag || parsedWidth.styleString != this._width.styleString;\r\n mutatedFlag = mutatedFlag || parsedHeight.styleString != this._height.styleString;\r\n\r\n this._width = parsedWidth;\r\n this._height = parsedHeight;\r\n }\r\n\r\n this.needsLayout = this.needsLayout || mutatedFlag;\r\n this.refreshLayoutIfNeeded(pending);\r\n }\r\n\r\n public setNeedsLayout() {\r\n this.needsLayout = true;\r\n }\r\n\r\n public batchLayoutAfter(closure: () => void) {\r\n /*\r\n Is there already an ongoing batch? If so, just run the closure and don't\r\n adjust the tracking variables. The outermost call will finalize layout.\r\n */\r\n if(this.deferLayout) {\r\n closure();\r\n return;\r\n }\r\n\r\n try {\r\n this.deferLayout = true;\r\n if(this.vkbd) {\r\n this.vkbd.deferLayout = true;\r\n }\r\n closure();\r\n } finally {\r\n this.deferLayout = false;\r\n if(this.vkbd) {\r\n this.vkbd.deferLayout = false;\r\n }\r\n this.refreshLayout();\r\n }\r\n }\r\n\r\n public refreshLayout(pending?: boolean): void {\r\n if(!this.keyboardView || this.deferLayout) {\r\n return;\r\n }\r\n\r\n // Step 1: have the necessary conditions been met?\r\n const hasDimensions = this.width && this.height;\r\n\r\n if(!hasDimensions) {\r\n // If dimensions haven't been set yet, we have no basis for layout calculations.\r\n // We do not emit a warning here; if we did, at the time of writing this, we'd\r\n // consistently get Sentry events from the Keyman mobile apps.\r\n //\r\n // See #9206 & https://github.com/keymanapp/keyman/pull/9206#issuecomment-1627917615\r\n // for context and history.\r\n return;\r\n }\r\n\r\n const fixedSize = this.width.absolute && this.height.absolute;\r\n const computedStyle = getComputedStyle(this._Box);\r\n const isInDOM = computedStyle.height != '' && computedStyle.height != 'auto';\r\n\r\n // Step 2: determine basic layout geometry\r\n if(fixedSize) {\r\n this._computedWidth = this.width.val;\r\n this._computedHeight = this.height.val;\r\n } else if(isInDOM) {\r\n // Note: %-based auto-detect for dimensions currently has some issues; the stylesheets load\r\n // asynchronously, causing the format to be VERY off before the stylesheets fully load.\r\n //\r\n // Depending on initial effects, changes to the OSK size could cause changes to the _parent_ size,\r\n // too... so this potential bit likely needs something of a redesign.\r\n const parent = this._Box.parentElement as HTMLElement;\r\n this._computedWidth = this.width.val * (this.width.absolute ? 1 : parent.offsetWidth);\r\n this._computedHeight = this.height.val * (this.height.absolute ? 1 : parent.offsetHeight);\r\n } else {\r\n console.warn(\"Unable to properly perform layout - specification uses a relative spec, thus relies upon insertion into the DOM for layout.\");\r\n return;\r\n }\r\n\r\n // Must be set before any references to the .computedWidth and .computedHeight properties!\r\n this.needsLayout = false;\r\n\r\n // Step 3: perform layout operations.\r\n this.banner.element.style.fontSize = this.baseFontSize;\r\n if(this.vkbd) {\r\n this.vkbd.fontSize = this.parsedBaseFontSize;\r\n }\r\n\r\n if(!pending) {\r\n this.headerView?.refreshLayout();\r\n this.bannerView.width = this.computedWidth;\r\n this.bannerView.refreshLayout();\r\n this.footerView?.refreshLayout();\r\n }\r\n\r\n if(this.vkbd) {\r\n let availableHeight = this.computedHeight - this.computeFrameHeight();\r\n\r\n // +5: from kmw-banner-bar's 'top' attribute when active\r\n if(this.bannerView.height > 0) {\r\n availableHeight -= this.bannerView.height + 5;\r\n }\r\n // Triggers the VisualKeyboard.refreshLayout() method, which includes a showLanguage() call.\r\n this.vkbd.setSize(this.computedWidth, availableHeight, pending);\r\n\r\n const bs = this._Box.style;\r\n // OSK size settings can only be reliably applied to standard VisualKeyboard\r\n // visualizations, not to help text or empty views.\r\n bs.width = bs.maxWidth = this.computedWidth + 'px';\r\n bs.height = bs.maxHeight = this.computedHeight + 'px';\r\n } else {\r\n const bs = this._Box.style;\r\n bs.width = 'auto';\r\n bs.height = 'auto';\r\n bs.maxWidth = bs.maxHeight = '';\r\n }\r\n }\r\n\r\n public refreshLayoutIfNeeded(pending?: boolean) {\r\n if(this.needsLayout) {\r\n this.refreshLayout(pending);\r\n }\r\n }\r\n\r\n public abstract getDefaultWidth(): number;\r\n public abstract getDefaultKeyboardHeight(): number;\r\n\r\n // /**\r\n // * Function _Load\r\n // * Scope Private\r\n // * Description OSK initialization when keyboard selected\r\n // */\r\n // _Load() { // Load Help - maintained only temporarily.\r\n // let keymanweb = com.keyman.singleton;\r\n // this.activeKeyboard = keymanweb.core.activeKeyboard;\r\n // }\r\n\r\n public postKeyboardLoad() {\r\n this._Visible = false; // I3363 (Build 301)\r\n\r\n // Perform any needed restructuring and/or layout tweaks (depending on the OSKView type).\r\n this.postKeyboardAdjustments();\r\n\r\n if(this.displayIfActive) {\r\n this.present();\r\n }\r\n }\r\n\r\n protected abstract postKeyboardAdjustments(): void;\r\n\r\n protected abstract setBoxStyling(): void;\r\n\r\n private loadActiveKeyboard() {\r\n this.setBoxStyling();\r\n this.needsLayout = true;\r\n // Save references to the old kbd & its styles for shutdown after replacement.\r\n const oldKbd = this.keyboardView;\r\n const oldKbdStyleManager = this.kbdStyleSheetManager;\r\n\r\n // Create new ones for the new, incoming kbd.\r\n this.kbdStyleSheetManager = new StylesheetManager(this._Box, this.config.doCacheBusting || false);\r\n const kbdView = this.keyboardView = this._GenerateKeyboardView(this.keyboardData?.keyboard, this.keyboardData?.metadata);\r\n\r\n // Perform the replacement.\r\n this._Box.replaceChild(kbdView.element, oldKbd.element);\r\n kbdView.postInsert();\r\n this.bannerController?.configureForKeyboard(this.keyboardData?.keyboard, this.keyboardData?.metadata);\r\n\r\n // Now that the swap has occurred, it's safe to shutdown the old VisualKeyboard and any related stylesheets.\r\n if(oldKbd instanceof VisualKeyboard) {\r\n oldKbd.shutdown();\r\n }\r\n oldKbdStyleManager.unlinkAll();\r\n\r\n // END: construction of the actual internal layout for the overall OSK\r\n // Footer element management is handled within FloatingOSKView.\r\n\r\n this.banner.appendStyles();\r\n\r\n if(this.vkbd) {\r\n // Create the key preview (for phones)\r\n this.vkbd.createKeyTip();\r\n\r\n // Create the globe hint (for embedded contexts; has a stub for other contexts)\r\n const globeHint = this.vkbd.createGlobeHint();\r\n if(globeHint) {\r\n this._Box.appendChild(globeHint.element);\r\n }\r\n\r\n // Append a stylesheet for this keyboard for keyboard specific styles\r\n // or if needed to specify an embedded font\r\n this.vkbd.appendStyleSheet();\r\n }\r\n\r\n this.postKeyboardLoad();\r\n }\r\n\r\n private _GenerateKeyboardView(keyboard: Keyboard, keyboardMetadata: KeyboardProperties): KeyboardView {\r\n let device = this.targetDevice;\r\n\r\n this._Box.className = \"\";\r\n\r\n // Case 1: since we hide the system keyboard on touch devices, we need\r\n // to display SOMETHING that can accept input.\r\n if(keyboard == null && !device.touchable) {\r\n // We do not (currently) allow selecting the default system keyboard on\r\n // touch form-factors. Likely b/c mnemonic difficulties.\r\n return new EmptyView();\r\n } else {\r\n // Generate a visual keyboard from the layout (or layout default)\r\n // Condition is false if no key definitions exist, formFactor == desktop, AND help text exists. All three.\r\n if(keyboard && keyboard.layout(device.formFactor as DeviceSpec.FormFactor)) {\r\n return this._GenerateVisualKeyboard(keyboard, keyboardMetadata);\r\n } else if(!keyboard /* && device.touchable (implied) */ || !keyboardMetadata) {\r\n // Show a basic, \"hollow\" OSK that at least allows input, since we're\r\n // on a touch device and hiding the system keyboard\r\n return this._GenerateVisualKeyboard(null, null);\r\n } else {\r\n // A keyboard help-page or help-text is still a visualization, even not a standard OSK.\r\n return new HelpPageView(keyboard);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Function _GenerateVisualKeyboard\r\n * Scope Private\r\n * @param {Object} keyboard The keyboard to visualize\r\n * Description Generates the visual keyboard element and attaches it to KMW\r\n */\r\n private _GenerateVisualKeyboard(keyboard: Keyboard, keyboardMetadata: KeyboardProperties): VisualKeyboard {\r\n let device = this.targetDevice;\r\n\r\n const resourcePath = getResourcePath(this.config);\r\n\r\n // Root element sets its own classes, one of which is 'kmw-osk-inner-frame'.\r\n let vkbd = new VisualKeyboard({\r\n keyboard: keyboard,\r\n keyboardMetadata: keyboardMetadata,\r\n device: device,\r\n hostDevice: this.hostDevice,\r\n topContainer: this._Box,\r\n styleSheetManager: this.kbdStyleSheetManager,\r\n pathConfig: this.config.pathConfig,\r\n embeddedGestureConfig: this.config.embeddedGestureConfig,\r\n isEmbedded: this.config.isEmbedded,\r\n specialFont: {\r\n family: 'SpecialOSK',\r\n files: [`${resourcePath}/keymanweb-osk.ttf`],\r\n path: '' // Not actually used.\r\n },\r\n gestureParams: this.config.gestureParams\r\n });\r\n\r\n vkbd.on('keyevent', (keyEvent, callback) => this.emit('keyevent', keyEvent, callback));\r\n vkbd.on('globekey', (keyElement, on) => this.emit('globekey', keyElement, on));\r\n vkbd.on('hiderequested', (keyElement) => {\r\n this.doHide(true);\r\n this.emit('hiderequested', keyElement);\r\n });\r\n\r\n // Set box class - OS and keyboard added for Build 360\r\n this._Box.className=device.formFactor+' '+ device.OS.toLowerCase() + ' kmw-osk-frame';\r\n\r\n // Add primary keyboard element to OSK\r\n return vkbd;\r\n }\r\n\r\n /**\r\n * This function may be provided to event sources to trigger changes in keyboard layer.\r\n * It is pre-bound to its OSKView instance.\r\n *\r\n ```\r\n {\r\n let core = com.keyman.singleton.core;\r\n core.keyboardProcessor.layerStore.handler = this.layerChangeHandler;\r\n }\r\n ```\r\n *\r\n * @param source\r\n * @param newValue\r\n * @returns\r\n */\r\n public layerChangeHandler: SystemStoreMutationHandler = (source: MutableSystemStore,\r\n newValue: string) => {\r\n // This handler is also triggered on state-key state changes (K_CAPS) that\r\n // may not actually change the layer.\r\n if(this.vkbd) {\r\n this.vkbd._UpdateVKShiftStyle(newValue);\r\n }\r\n\r\n if((this.vkbd && this.vkbd.layerId != newValue) || source.value != newValue) {\r\n // Prevents console errors when a keyboard only displays help.\r\n // Can occur when using SHIFT with sil_euro_latin on a desktop form-factor.\r\n //\r\n // Also, only change the layer ID itself if there is an actual corresponding layer\r\n // in the OSK.\r\n if(this.vkbd?.layerGroup.getLayer(newValue) && !this.vkbd?.layerLocked) {\r\n // triggers state-update + layer refresh automatically.\r\n this.vkbd.layerId = newValue;\r\n }\r\n }\r\n\r\n return false;\r\n }\r\n\r\n /**\r\n * The main function for presenting the OSKView.\r\n *\r\n * This includes:\r\n * - refreshing its layout\r\n * - displaying it\r\n * - positioning it\r\n */\r\n public present(): void {\r\n // Do not try to display OSK if no active element\r\n if(!this.mayShow()) {\r\n return;\r\n }\r\n\r\n // Ensure the keyboard view is modeling the correct state. (Correct layer, etc.)\r\n this.keyboardView.updateState(); // get current state keys!\r\n\r\n this._Box.style.display='block'; // Is 'none' when hidden.\r\n\r\n // First thing after it's made visible.\r\n this.refreshLayoutIfNeeded();\r\n\r\n this._Visible=true;\r\n\r\n /* In case it's still '0' from a hide() operation.\r\n *\r\n * (Opacity is only modified when device.touchable = true,\r\n * though a couple of extra conditions may apply.)\r\n */\r\n this._Box.style.opacity = '1';\r\n\r\n // If OSK still hidden, make visible only after all calculation finished\r\n if(this._Box.style.visibility == 'hidden') {\r\n let _this = this;\r\n window.setTimeout(function() {\r\n _this._Box.style.visibility = 'visible';\r\n }, 0);\r\n }\r\n\r\n this.setDisplayPositioning();\r\n\r\n // Each subclass is responsible for raising the 'show' event on its own, since\r\n // certain ones supply extra information in their event param object.\r\n }\r\n\r\n /**\r\n * Method usable by subclasses of OSKView to control that OSKView type's\r\n * positioning behavior when needed by the present() method.\r\n */\r\n protected abstract setDisplayPositioning(): void;\r\n\r\n /**\r\n * Method used to start a potentially-asynchronous hide of the OSK.\r\n * @param hiddenByUser `true` if this hide operation was directly requested by the user.\r\n */\r\n public startHide(hiddenByUser: boolean): void {\r\n if(!this.mayHide(hiddenByUser)) {\r\n return;\r\n }\r\n\r\n if(hiddenByUser) {\r\n // The one location outside of the `displayIfActive` property that bypasses the setter.\r\n // Avoids needless recursion that could be triggered by it, as we're already in the\r\n // process of hiding the OSK anyway.\r\n this.activationModel.enabled = ((this.keyboardData.keyboard.isCJK || this.hostDevice.touchable) ? true : false); // I3363 (Build 301)\r\n }\r\n\r\n let promise: Promise = null;\r\n if(this._Box && this.hostDevice.touchable && !(this.keyboardView instanceof EmptyView) && this.config.allowHideAnimations) {\r\n /**\r\n * Note: this refactored code appears to reflect a currently-dead code path. 14.0's\r\n * equivalent is either extremely niche or is actually inaccessible.\r\n */\r\n promise = this.useHideAnimation();\r\n } else {\r\n promise = Promise.resolve(true);\r\n }\r\n\r\n const _this = this;\r\n promise.then(function(shouldHide: boolean) {\r\n if(shouldHide) {\r\n _this.finalizeHide();\r\n }\r\n });\r\n\r\n // Allow UI to execute code when hiding the OSK\r\n this.doHide(hiddenByUser);\r\n }\r\n\r\n /**\r\n * Performs the _actual_ logic and functionality involved in hiding the OSK.\r\n */\r\n protected finalizeHide() {\r\n if (document.body.className.indexOf('osk-always-visible') >= 0) {\r\n if (this.hostDevice.formFactor == 'desktop') {\r\n return;\r\n }\r\n }\r\n\r\n if(this._Box) {\r\n let bs=this._Box.style;\r\n bs.display = 'none';\r\n bs.transition = '';\r\n bs.opacity = '1';\r\n this._Visible=false;\r\n }\r\n\r\n if(this.vkbd) {\r\n this.vkbd.onHide();\r\n }\r\n }\r\n\r\n /**\r\n *\r\n * @returns `false` if the OSK is in an invalid state for being presented to the user.\r\n */\r\n protected mayShow(): boolean {\r\n if(!this.activationModel.conditionsMet) {\r\n return false;\r\n }\r\n\r\n // Never display the OSK for desktop browsers unless KMW element is focused, and a keyboard selected\r\n if(!this.keyboardView || this.keyboardView instanceof EmptyView || !this.activationModel.enabled) {\r\n return false;\r\n }\r\n\r\n if(!this._Box) {\r\n return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n *\r\n * @param hiddenByUser\r\n * @returns `false` if the OSK is in an invalid state for being hidden from the user.\r\n */\r\n protected mayHide(hiddenByUser: boolean): boolean {\r\n if(this.activationModel.conditionsMet && !this.mayDisable) {\r\n return false;\r\n }\r\n\r\n if(this.activationModel instanceof StaticActivator) {\r\n return false;\r\n }\r\n\r\n if(!hiddenByUser && this.hostDevice.formFactor == 'desktop') {\r\n //Allow desktop OSK to remain visible on blur if body class set\r\n if(document.body.className.indexOf('osk-always-visible') >= 0) {\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Applies CSS styling and handling needed to perform a fade animation when\r\n * hiding the OSK.\r\n *\r\n * Note: currently reflects an effectively-dead code path, though this is\r\n * likely not intentional. Other parts of the KMW engine seem to call hideNow()\r\n * synchronously after each and every part of the engine that calls this function,\r\n * cancelling the Promise.\r\n *\r\n * @returns A Promise denoting either cancellation of the hide (`false`) or\r\n * completion of the hide & its animation (`true`)\r\n */\r\n\r\n protected useHideAnimation(): Promise {\r\n const os = this._Box.style;\r\n const _this = this;\r\n\r\n return new Promise(function(resolve) {\r\n const cleanup = function() {\r\n // TODO(lowpri): attach event listeners on create and leave them there\r\n _this._Box.removeEventListener('transitionend', cleanup, false);\r\n _this._Box.removeEventListener('webkitTransitionEnd', cleanup, false);\r\n _this._Box.removeEventListener('transitioncancel', cleanup, false);\r\n _this._Box.removeEventListener('webkitTransitionCancel', cleanup, false);\r\n if(_this._animatedHideTimeout != 0) {\r\n window.clearTimeout(_this._animatedHideTimeout);\r\n }\r\n _this._animatedHideTimeout = 0;\r\n\r\n if(_this._Visible && _this.activationModel.conditionsMet) {\r\n // Leave opacity alone and clear transition if another element activated\r\n os.transition='';\r\n os.opacity='1';\r\n resolve(false);\r\n return false;\r\n } else {\r\n resolve(true);\r\n return true;\r\n }\r\n }, startup = function() {\r\n _this._Box.removeEventListener('transitionrun', startup, false);\r\n _this._Box.removeEventListener('webkitTransitionRun', startup, false);\r\n _this._Box.addEventListener('transitionend', cleanup, false);\r\n _this._Box.addEventListener('webkitTransitionEnd', cleanup, false);\r\n _this._Box.addEventListener('transitioncancel', cleanup, false);\r\n _this._Box.addEventListener('webkitTransitionCancel', cleanup, false);\r\n };\r\n\r\n _this._Box.addEventListener('transitionrun', startup, false);\r\n _this._Box.addEventListener('webkitTransitionRun', startup, false);\r\n\r\n os.transition='opacity 0.5s linear 0';\r\n os.opacity='0';\r\n\r\n // Cannot hide the OSK smoothly using a transitioned drop, since for\r\n // position:fixed elements transitioning is incompatible with translate3d(),\r\n // and also does not work with top, bottom or height styles.\r\n // Opacity can be transitioned and is probably the simplest alternative.\r\n // We must condition on osk._Visible in case focus has since been moved to another\r\n // input (in which case osk._Visible will be non-zero)\r\n _this._animatedHideTimeout = window.setTimeout(cleanup,\r\n 200); // Wait a bit before starting, to allow for moving to another element\r\n });\r\n }\r\n\r\n /**\r\n * Used to synchronously hide the OSK, cancelling any async hide animations that have\r\n * not started and immediately completing the hide of any hide ops pending completion\r\n * of their animation.\r\n */\r\n public hideNow() {\r\n if(!this.mayHide(false) || !this._Box) {\r\n return;\r\n }\r\n\r\n // Two possible uses for _animatedHideResolver:\r\n // - _animatedHideTimeout is set: animation is waiting to start\r\n // - _animatedHideTimeout is null: animation has already started.\r\n\r\n // Was an animated hide waiting to start? Just cancel it.\r\n if(this._animatedHideTimeout) {\r\n window.clearTimeout(this._animatedHideTimeout);\r\n this._animatedHideTimeout = 0;\r\n }\r\n\r\n // Was an animated hide already in progress? If so, just trigger it early.\r\n const os = this._Box.style;\r\n os.transition='';\r\n os.opacity='0';\r\n this.finalizeHide();\r\n }\r\n\r\n ['shutdown']() {\r\n // Disable the OSK's event handlers.\r\n this.removeBaseMouseEventListeners();\r\n this.removeBaseTouchEventListeners();\r\n\r\n // Remove the OSK's elements from the document, allowing them to be properly cleaned up.\r\n // Necessary for clean engine testing.\r\n var _box = this._Box;\r\n if(_box.parentElement) {\r\n _box.parentElement.removeChild(_box);\r\n }\r\n\r\n this.kbdStyleSheetManager.unlinkAll();\r\n this.uiStyleSheetManager.unlinkAll();\r\n\r\n this.bannerController.shutdown();\r\n }\r\n\r\n /**\r\n * Function getRect\r\n * Scope Public\r\n * @return {Object.} Array object with position and size of OSK container\r\n * Description Get rectangle containing KMW Virtual Keyboard\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/osk/getRect\r\n */\r\n public getRect(): OSKRect {\t\t// I2405\r\n var p: OSKRect = {};\r\n\r\n // Always return these based upon _Box; using this.vkbd will fail to account for banner and/or\r\n // the desktop OSK border.\r\n p['left'] = p.left = getAbsoluteX(this._Box);\r\n p['top'] = p.top = getAbsoluteY(this._Box);\r\n\r\n p['width'] = this.computedWidth;\r\n p['height'] = this.computedHeight;\r\n return p;\r\n }\r\n\r\n /* ---- Legacy interfacing methods and fields ----\r\n *\r\n * The endgoal is to eliminate the need for these entirely, but extra work and care\r\n * will be necessary to achieve said endgoal for these methods.\r\n *\r\n * The simplest way forward is to maintain them, then resolve them independently,\r\n * one at a time.\r\n */\r\n\r\n // OSK state fields & events\r\n //\r\n // These are relatively stable and may be preserved as they are.\r\n _Visible: boolean = false;\r\n\r\n /**\r\n * Function enabled\r\n * Scope Public\r\n * @return {boolean|number} True if KMW OSK enabled\r\n * Description Test if KMW OSK is enabled\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/osk/isEnabled\r\n */\r\n public isEnabled(): boolean {\r\n return this.displayIfActive;\r\n }\r\n\r\n /**\r\n * Function isVisible\r\n * Scope Public\r\n * @return {boolean|number} True if KMW OSK visible\r\n * Description Test if KMW OSK is actually visible\r\n * Note that this will usually return false after any UI event that results in (temporary) loss of input focus\r\n *\r\n * https://help.keyman.com/developer/engine/web/current-version/reference/osk/isVisible\r\n */\r\n public isVisible(): boolean {\r\n return this._Visible;\r\n }\r\n\r\n /**\r\n * Function hide\r\n * Scope Public\r\n * Description Prevent display of OSK window on focus\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/osk/hide\r\n */\r\n public hide() {\r\n this.activationModel.enabled = false;\r\n this.startHide(true);\r\n }\r\n\r\n /**\r\n * Description Display KMW OSK (at position set in callback to UI)\r\n * Function show\r\n * Scope Public\r\n * @param {(boolean|number)=} bShow True to display, False to hide, omitted to toggle\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/osk/show\r\n */\r\n public show(bShow?: boolean) {\r\n if(arguments.length > 0) {\r\n this.activationModel.enabled = bShow;\r\n } else {\r\n if(this.activationModel.conditionsMet) {\r\n this.activationModel.enabled = !this.activationModel.enabled;\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Allow UI to respond to OSK being shown (passing position and properties)\r\n *\r\n * @param {Object=} p object with coordinates and userdefined flag\r\n * @return {boolean}\r\n *\r\n */\r\n doShow(p: {\r\n x: number,\r\n y: number,\r\n userLocated: boolean\r\n }) {\r\n // Newer style 'doShow' emitted from .present by default.\r\n this.legacyEvents.callEvent('show', p);\r\n }\r\n\r\n /**\r\n * Allow UI to update respond to OSK being hidden\r\n *\r\n * @param {boolean} p object with coordinates and userdefined flag\r\n * @return {void}\r\n *\r\n */\r\n doHide(hiddenByUser: boolean) {\r\n const p={\r\n HiddenByUser: hiddenByUser\r\n };\r\n this.legacyEvents.callEvent('hide', p);\r\n }\r\n\r\n /**\r\n * Function addEventListener\r\n * Scope Public\r\n * @param {string} event event name\r\n * @param {function(Object)} func event handler\r\n * Description Wrapper function to add and identify OSK-specific event handlers\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/osk/addEventListener\r\n */\r\n public addEventListener(\r\n event: T,\r\n fn: EventListener\r\n ): void {\r\n this.legacyEvents.addEventListener(event, fn);\r\n }\r\n\r\n /**\r\n * Function removeEventListener\r\n * Scope Public\r\n * @param {string} event event name\r\n * @param {function(Object)} func event handler\r\n * Description Wrapper function to remove previously-added OSK-specific event handlers\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/osk/removeEventListener\r\n */\r\n public removeEventListener(\r\n event: T,\r\n fn: EventListener\r\n ): void {\r\n this.legacyEvents.removeEventListener(event, fn);\r\n }\r\n}", + "import { EventEmitter } from 'eventemitter3';\r\n\r\nimport { Keyboard } from 'keyman/engine/keyboard';\r\n\r\nimport OSKViewComponent from './oskViewComponent.interface.js';\r\nimport { ParsedLengthStyle } from '../lengthStyle.js';\r\nimport MouseDragOperation from '../input/mouseDragOperation.js';\r\n\r\nimport { createUnselectableElement } from 'keyman/engine/dom-utils';\r\n\r\ninterface EventMap {\r\n /**\r\n * The close button (to request that the OSK hide) has been clicked.\r\n */\r\n close: () => void,\r\n\r\n /**\r\n * The config button has been clicked.\r\n */\r\n config: () => void,\r\n\r\n /**\r\n * The help button has been clicked.\r\n */\r\n help: () => void,\r\n\r\n /**\r\n * The pin button was visible and has been clicked.\r\n */\r\n unpin: () => void\r\n}\r\n\r\nexport default class TitleBar extends EventEmitter implements OSKViewComponent {\r\n private _element: HTMLDivElement;\r\n private _unpinButton: HTMLDivElement;\r\n private _closeButton: HTMLDivElement;\r\n private _helpButton: HTMLDivElement;\r\n private _configButton: HTMLDivElement;\r\n private _caption: HTMLSpanElement;\r\n\r\n private _helpEnabled: boolean;\r\n private _configEnabled: boolean;\r\n\r\n public get helpEnabled(): boolean {\r\n return this._helpEnabled;\r\n }\r\n\r\n public set helpEnabled(val) {\r\n this._helpEnabled = val;\r\n\r\n this._helpButton.style.display = val ? 'inline' : 'none';\r\n }\r\n\r\n public get configEnabled(): boolean {\r\n return this._configEnabled;\r\n }\r\n\r\n public set configEnabled(val) {\r\n this._configEnabled = val;\r\n\r\n this._configButton.style.display = val ? 'inline' : 'none';\r\n }\r\n\r\n private static readonly DISPLAY_HEIGHT = ParsedLengthStyle.inPixels(20); // As set in kmwosk.css\r\n\r\n public constructor(dragHandler?: MouseDragOperation) {\r\n super();\r\n\r\n this._element = this.buildTitleBar();\r\n\r\n this.helpEnabled = false;\r\n this.configEnabled = false;\r\n\r\n if(dragHandler) {\r\n this.element.onmousedown = dragHandler.mouseDownHandler;\r\n }\r\n }\r\n\r\n public get layoutHeight(): ParsedLengthStyle {\r\n return TitleBar.DISPLAY_HEIGHT;\r\n }\r\n\r\n private mouseCancellingHandler: (ev: MouseEvent) => boolean = function(ev: MouseEvent) {\r\n ev.preventDefault();\r\n ev.cancelBubble = true;\r\n return false;\r\n };\r\n\r\n public get element(): HTMLDivElement {\r\n return this._element;\r\n }\r\n\r\n public setPinCJKOffset() {\r\n this._unpinButton.style.left = '15px';\r\n }\r\n\r\n public showPin(visible: boolean) {\r\n this._unpinButton.style.display = visible ? 'block' : 'none';\r\n }\r\n\r\n public setTitle(str: string) {\r\n this._caption.innerHTML = str;\r\n }\r\n\r\n public setTitleFromKeyboard(keyboard: Keyboard) {\r\n let title = \"\" + keyboard?.name + ''; // I1972 // I2186\r\n this._caption.innerHTML = title;\r\n }\r\n\r\n /**\r\n * Create a control bar with title and buttons for the desktop OSK\r\n */\r\n buildTitleBar(): HTMLDivElement {\r\n let bar = createUnselectableElement('div');\r\n bar.id='keymanweb_title_bar';\r\n bar.className='kmw-title-bar';\r\n\r\n var Ltitle = this._caption = createUnselectableElement('span');\r\n Ltitle.className='kmw-title-bar-caption';\r\n Ltitle.style.color='#fff';\r\n bar.appendChild(Ltitle);\r\n\r\n var Limg = this._closeButton = this.buildCloseButton();\r\n this._closeButton.onclick = () => {\r\n this.emit('close');\r\n return false;\r\n };\r\n bar.appendChild(Limg);\r\n\r\n Limg = this._helpButton = this.buildHelpButton()\r\n this._helpButton.onclick = () => {\r\n this.emit('help');\r\n return false;\r\n }\r\n bar.appendChild(Limg);\r\n\r\n Limg = this._configButton = this.buildConfigButton();\r\n this._configButton.onclick = () => {\r\n this.emit('config');\r\n return false;\r\n }\r\n bar.appendChild(Limg);\r\n\r\n Limg = this._unpinButton = this.buildUnpinButton();\r\n this._unpinButton.onclick = () => {\r\n this.emit('unpin');\r\n return false;\r\n }\r\n bar.appendChild(Limg);\r\n\r\n return bar;\r\n }\r\n\r\n private buildCloseButton(): HTMLDivElement {\r\n var Limg = createUnselectableElement('div');\r\n\r\n Limg.id='kmw-close-button';\r\n Limg.className='kmw-title-bar-image';\r\n Limg.onmousedown = this.mouseCancellingHandler;\r\n\r\n return Limg;\r\n }\r\n\r\n private buildHelpButton(): HTMLDivElement {\r\n let Limg = createUnselectableElement('div');\r\n Limg.id='kmw-help-image';\r\n Limg.className='kmw-title-bar-image';\r\n Limg.title='KeymanWeb Help';\r\n Limg.onmousedown = this.mouseCancellingHandler;\r\n return Limg;\r\n }\r\n\r\n private buildConfigButton(): HTMLDivElement {\r\n let Limg = createUnselectableElement('div');\r\n\r\n Limg.id='kmw-config-image';\r\n Limg.className='kmw-title-bar-image';\r\n Limg.title='KeymanWeb Configuration Options';\r\n Limg.onmousedown = this.mouseCancellingHandler;\r\n\r\n return Limg;\r\n }\r\n\r\n /**\r\n * Builds an 'unpin' button for restoring OSK to default location, handle mousedown and click events\r\n */\r\n private buildUnpinButton(): HTMLDivElement {\r\n let Limg = createUnselectableElement('div'); //I2186\r\n\r\n Limg.id = 'kmw-pin-image';\r\n Limg.className = 'kmw-title-bar-image';\r\n Limg.title='Pin the On Screen Keyboard to its default location on the active text box';\r\n\r\n Limg.onmousedown = this.mouseCancellingHandler;\r\n\r\n return Limg;\r\n }\r\n\r\n public refreshLayout() {\r\n // The title bar is adaptable as it is and needs no adjustments.\r\n }\r\n}", + "import { EventEmitter } from 'eventemitter3';\r\n\r\nimport OSKViewComponent from './oskViewComponent.interface.js';\r\nimport { ParsedLengthStyle } from '../lengthStyle.js';\r\nimport MouseDragOperation from '../input/mouseDragOperation.js';\r\n\r\nimport { createUnselectableElement } from 'keyman/engine/dom-utils';\r\n\r\ninterface EventMap {\r\n /**\r\n * Triggered when the user inputs a special command to show the engine's current version number.\r\n */\r\n showbuild: () => void;\r\n}\r\n\r\nexport default class ResizeBar extends EventEmitter implements OSKViewComponent {\r\n private _element: HTMLDivElement;\r\n private _resizeHandle: HTMLDivElement;\r\n\r\n private static readonly DISPLAY_HEIGHT = ParsedLengthStyle.inPixels(16); // As set in kmwosk.css\r\n\r\n private mouseCancellingHandler: (ev: MouseEvent) => boolean = function(ev: MouseEvent) {\r\n ev.preventDefault();\r\n ev.cancelBubble = true;\r\n return false;\r\n };\r\n\r\n public constructor(dragHandler?: MouseDragOperation) {\r\n super();\r\n this._element = this.buildResizeBar();\r\n\r\n if(dragHandler) {\r\n this._resizeHandle.onmousedown = dragHandler.mouseDownHandler;\r\n }\r\n }\r\n\r\n public get layoutHeight(): ParsedLengthStyle {\r\n return ResizeBar.DISPLAY_HEIGHT;\r\n }\r\n\r\n public get element(): HTMLDivElement {\r\n return this._element;\r\n }\r\n\r\n public get handle(): HTMLDivElement {\r\n return this._resizeHandle;\r\n }\r\n\r\n public allowResizing(flag: boolean) {\r\n this._resizeHandle.style.display = flag ? 'block' : 'none';\r\n }\r\n\r\n /**\r\n * Create a bottom bar with a resizing icon for the desktop OSK\r\n */\r\n buildResizeBar(): HTMLDivElement {\r\n var bar = createUnselectableElement('div');\r\n bar.className='kmw-footer';\r\n bar.onmousedown = this.mouseCancellingHandler;\r\n\r\n // Add caption\r\n var Ltitle=createUnselectableElement('div');\r\n Ltitle.className='kmw-footer-caption';\r\n Ltitle.innerHTML='KeymanWeb';\r\n Ltitle.id='keymanweb-osk-footer-caption';\r\n\r\n // Display build number on shift+double click\r\n Ltitle.addEventListener('dblclick', (e) => {\r\n this.emit('showbuild');\r\n\r\n return false;\r\n }, false);\r\n\r\n bar.appendChild(Ltitle);\r\n\r\n var Limg = createUnselectableElement('div');\r\n Limg.className='kmw-footer-resize';\r\n bar.appendChild(Limg);\r\n this._resizeHandle=Limg;\r\n\r\n return bar;\r\n }\r\n\r\n public refreshLayout() {\r\n // The title bar is adaptable as it is and needs no adjustments.\r\n }\r\n}", + "type MouseHandler = (this: GlobalEventHandlers, ev: MouseEvent) => any;\r\n\r\n/**\r\n * Represents the current location of the current cursor / touchpoint during\r\n * an ongoing contact-point event series. This class standardizes to .pageX\r\n * (document) coordinates, rather than .clientX (viewport) coordinates.\r\n */\r\nclass InputEventCoordinate {\r\n public readonly x: number;\r\n public readonly y: number;\r\n\r\n public constructor(x: number, y: number, source?: MouseEvent | TouchEvent) {\r\n this.x = x;\r\n this.y = y;\r\n }\r\n\r\n // Converts a MouseEvent or TouchEvent into the base coordinates needed\r\n // by the mouse-dragging operations.\r\n public static fromEvent(e: MouseEvent | TouchEvent) {\r\n let coordSource: MouseEvent | Touch;\r\n\r\n // Desktop Safari versions as recent as 14.1 do not support TouchEvents.\r\n // So, just in case, a two-fold conditional check to avoid issues with a direct\r\n // 'instanceof' against the type.\r\n if(window['TouchEvent'] && e instanceof TouchEvent) {\r\n coordSource = e.changedTouches[0];\r\n } else if((e as TouchEvent).changedTouches) {\r\n coordSource = (e as TouchEvent).changedTouches[0] as Touch;\r\n } else {\r\n coordSource = e as MouseEvent;\r\n }\r\n\r\n // For MouseEvents, .pageX is slightly less supported in older browsers when\r\n // compared to .clientX. They're about equally supported for TouchEvents.\r\n if (coordSource.pageX) {\r\n return new InputEventCoordinate(coordSource.pageX, coordSource.pageY, e);\r\n } else if (coordSource.clientX) {\r\n const x = coordSource.clientX + document.body.scrollLeft;\r\n const y = coordSource.clientY + document.body.scrollTop;\r\n\r\n return new InputEventCoordinate(x, y, e);\r\n } else {\r\n return new InputEventCoordinate(null, null, e);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Used to store the page's original mouse handlers and properties\r\n * when temporarily overridden by OSK moving or resizing handlers due\r\n * to user interaction.\r\n */\r\nclass MouseStartSnapshot {\r\n private readonly _VPreviousMouseMove: MouseHandler;\r\n private readonly _VPreviousMouseUp: MouseHandler;\r\n private readonly _VPreviousCursor: string;\r\n private readonly _VPreviousMouseButton: number;\r\n\r\n constructor(e: MouseEvent) {\r\n this._VPreviousMouseMove = document.onmousemove;\r\n this._VPreviousMouseUp = document.onmouseup;\r\n\r\n this._VPreviousCursor = document.body.style.cursor;\r\n this._VPreviousMouseButton = (typeof(e.which)=='undefined' ? e.button : e.which);\r\n }\r\n\r\n restore() {\r\n document.onmousemove = this._VPreviousMouseMove;\r\n document.onmouseup = this._VPreviousMouseUp;\r\n\r\n if(document.body.style.cursor) {\r\n document.body.style.cursor = this._VPreviousCursor;\r\n }\r\n }\r\n\r\n matchesCausingClick(e: MouseEvent): boolean {\r\n return this._VPreviousMouseButton == (typeof(e.which)=='undefined' ? e.button : e.which);\r\n }\r\n}\r\n\r\nexport default abstract class MouseDragOperation {\r\n private _enabled: boolean;\r\n private _startCoord: InputEventCoordinate;\r\n private _mouseStartSnapshot: MouseStartSnapshot;\r\n\r\n private startHandler: (e: MouseEvent) => void;\r\n private cursorType: string;\r\n\r\n public constructor(cursorType?: string) {\r\n this.startHandler = this._VMoveMouseDown.bind(this);\r\n this.cursorType = cursorType;\r\n }\r\n\r\n /**\r\n * Denotes whether or not this object should handle incoming events.\r\n */\r\n public get enabled(): boolean {\r\n return this._enabled;\r\n }\r\n\r\n public set enabled(flag: boolean) {\r\n this._enabled = flag;\r\n }\r\n\r\n /**\r\n * Denotes whether or not this object is currently handling an ongoing drag event.\r\n */\r\n public get isActive(): boolean {\r\n return !!this._mouseStartSnapshot;\r\n }\r\n\r\n public get mouseDownHandler(): (e: MouseEvent) => void {\r\n return this.startHandler;\r\n }\r\n\r\n /**\r\n * Function _VMoveMouseDown\r\n * Scope Private\r\n * @param {Object} e event\r\n * Description Process mouse down on OSK\r\n */\r\n private _VMoveMouseDown(e: MouseEvent) {\r\n if(!e) {\r\n return true;\r\n }\r\n\r\n if(!this._enabled) {\r\n return true;\r\n }\r\n\r\n if(!this._mouseStartSnapshot) { // I1472 - Dragging off edge of browser window causes muckup\r\n this._mouseStartSnapshot = new MouseStartSnapshot(e);\r\n }\r\n\r\n this._startCoord = InputEventCoordinate.fromEvent(e);\r\n\r\n document.onmousemove = this._VMoveMouseMove.bind(this);\r\n document.onmouseup = this._VMoveMouseUp.bind(this);\r\n if(document.body.style.cursor) {\r\n document.body.style.cursor = this.cursorType;\r\n }\r\n\r\n e.preventDefault();\r\n e.cancelBubble = true;\r\n\r\n this.onDragStart();\r\n return false;\r\n }\r\n\r\n protected abstract onDragStart(): void;\r\n\r\n /**\r\n * Process mouse drag on OSK\r\n *\r\n * @param {Object} e event\r\n */\r\n private _VMoveMouseMove(e: MouseEvent) {\r\n if(!e) {\r\n return true;\r\n }\r\n\r\n if(!this.enabled) {\r\n return true;\r\n }\r\n\r\n e.preventDefault();\r\n e.cancelBubble = true;\r\n\r\n if(!this._mouseStartSnapshot.matchesCausingClick(e)) { // I1472 - Dragging off edge of browser window causes muckup\r\n return this._VMoveMouseUp(e);\r\n } else {\r\n const coord = InputEventCoordinate.fromEvent(e);\r\n const deltaX = coord.x - this._startCoord.x;\r\n const deltaY = coord.y - this._startCoord.y;\r\n\r\n this.onDragMove(deltaX, deltaY);\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n *\r\n * @param deltaX The total horizontal distance moved, in pixels, since the start of the drag\r\n * @param deltaY The total vertical distance moved, in pixels, since the start of the drag\r\n */\r\n protected abstract onDragMove(deltaX: number, deltaY: number): void;\r\n\r\n /**\r\n * Function _VMoveMouseUp\r\n * Scope Private\r\n * @param {Object} e event\r\n * Description Process mouse up during movement of KMW OSK UI\r\n */\r\n private _VMoveMouseUp(e: MouseEvent) {\r\n if(!e) {\r\n return true;\r\n }\r\n\r\n this._mouseStartSnapshot.restore();\r\n this._mouseStartSnapshot = null;\r\n\r\n e.preventDefault();\r\n e.cancelBubble = true;\r\n\r\n this.onDragRelease();\r\n return false;\r\n }\r\n\r\n protected abstract onDragRelease(): void;\r\n}", + "import Activator from './activator.js';\r\n\r\ninterface TriggerEventMap {\r\n triggerchange: (trigger: Type) => void;\r\n}\r\n\r\nexport default class TwoStateActivator extends Activator> {\r\n private _enabled: boolean = true;\r\n private actValue: Type = null;\r\n\r\n get activate(): boolean {\r\n return this._enabled && !!this.actValue;\r\n }\r\n\r\n private checkState(oldValue: boolean) {\r\n if(this.activate != oldValue) {\r\n this.emit('activate', this.activate);\r\n }\r\n }\r\n\r\n get enabled(): boolean {\r\n return this._enabled;\r\n }\r\n\r\n set enabled(flag: boolean) {\r\n const oldState = this.activate;\r\n this._enabled = flag; // may change this.value!\r\n\r\n this.checkState(oldState);\r\n }\r\n\r\n get activationTrigger(): Type {\r\n return this.actValue;\r\n }\r\n\r\n set activationTrigger(value: Type) {\r\n const oldState = this.activate;\r\n const oldValue = this.actValue;\r\n this.actValue = value; // may change this.value!\r\n\r\n this.checkState(oldState);\r\n if(oldValue != value) {\r\n this.emit('triggerchange', value);\r\n }\r\n }\r\n\r\n get conditionsMet(): boolean {\r\n return !!this.activationTrigger;\r\n }\r\n}", + "import { CookieSerializer } from 'keyman/engine/dom-utils';\r\n\r\nexport interface FloatingOSKCookie {\r\n /**\r\n * Notes whether or not the OSK was hidden at the end of the previous session.\r\n */\r\n visible: 0 | 1;\r\n\r\n /**\r\n * Notes whether or not the OSK was pinned (located by the user) at the end\r\n * of the previous session.\r\n */\r\n userSet: 0 | 1;\r\n\r\n /**\r\n * Denotes the left-position of the OSK at the end of the previous session if pinned.\r\n * Defaults to -1 if the value was undefined.\r\n */\r\n left: number;\r\n\r\n /**\r\n * Denotes the left-position of the OSK at the end of the previous session if pinned.\r\n * Defaults to -1 if the value was undefined.\r\n */\r\n top: number;\r\n\r\n /**\r\n * The previously-set OSK width.\r\n */\r\n width?: number;\r\n\r\n /**\r\n * The previously-set OSK height.\r\n */\r\n height?: number;\r\n\r\n /**\r\n * The version of KeymanWeb active when this cookie was generated.\r\n */\r\n _version: string;\r\n}\r\n\r\nexport class FloatingOSKCookieSerializer extends CookieSerializer> {\r\n constructor() {\r\n super('KeymanWeb_OnScreenKeyboard');\r\n }\r\n\r\n loadWithDefaults(defaults: Required) {\r\n return {...defaults, ...this.load()};\r\n }\r\n\r\n load() {\r\n const cookie = super.load((value, key) => {\r\n switch(key) {\r\n case 'version':\r\n return value;\r\n default:\r\n return Number.parseInt(value, 10);\r\n }\r\n });\r\n\r\n if(!cookie.width) {\r\n delete cookie.width; // in case of a '' entry.\r\n }\r\n if(!cookie.height) {\r\n delete cookie.height; // in case of a '' entry.\r\n }\r\n\r\n return cookie;\r\n }\r\n\r\n save(cookie: Required) {\r\n super.save(cookie);\r\n }\r\n}", + "import { DeviceSpec, ManagedPromise, Version } from 'keyman/engine/keyboard';\r\nimport { getAbsoluteX, getAbsoluteY, landscapeView } from 'keyman/engine/dom-utils';\r\nimport { EmitterListenerSpy } from 'keyman/engine/events';\r\n\r\nimport OSKView, { EventMap, type LegacyOSKEventMap, OSKPos, OSKRect } from './oskView.js';\r\nimport TitleBar from '../components/titleBar.js';\r\nimport ResizeBar from '../components/resizeBar.js';\r\n\r\nimport MouseDragOperation from '../input/mouseDragOperation.js';\r\nimport { getViewportScale } from '../screenUtils.js';\r\nimport Configuration from '../config/viewConfiguration.js';\r\nimport TwoStateActivator from './twoStateActivator.js';\r\nimport { FloatingOSKCookie, FloatingOSKCookieSerializer } from './floatingOskCookie.js';\r\n\r\n/***\r\n KeymanWeb 10.0\r\n Copyright 2017 SIL International\r\n***/\r\n\r\nexport interface FloatingOSKViewConfiguration extends Configuration {\r\n activator?: TwoStateActivator;\r\n}\r\n\r\nexport default class FloatingOSKView extends OSKView {\r\n // OSK positioning fields\r\n userPositioned: boolean = false;\r\n specifiedPosition: boolean = false;\r\n x: number;\r\n y: number;\r\n noDrag: boolean = false;\r\n dfltX: string;\r\n dfltY: string;\r\n\r\n layoutSerializer = new FloatingOSKCookieSerializer();\r\n\r\n private titleBar: TitleBar;\r\n private resizeBar: ResizeBar;\r\n\r\n // Encapsulations of the drag behaviors for OSK movement & resizing\r\n private _moveHandler: MouseDragOperation;\r\n private _resizeHandler: MouseDragOperation;\r\n\r\n public constructor(config: FloatingOSKViewConfiguration) {\r\n config.activator = config.activator || new TwoStateActivator();\r\n\r\n super(config);\r\n\r\n this.typedActivationModel.on('triggerchange', () => this.setDisplayPositioning());\r\n\r\n document.body.appendChild(this._Box);\r\n\r\n // Add header element to OSK only for desktop browsers\r\n this.titleBar = new TitleBar(this.titleDragHandler);\r\n //\r\n this.titleBar.on('help', () => {\r\n this.legacyEvents.callEvent('helpclick', {});\r\n });\r\n this.titleBar.on('config', () => {\r\n this.legacyEvents.callEvent('configclick', {});\r\n });\r\n this.titleBar.on('close', () => this.startHide(true));\r\n this.titleBar.on('unpin', () => this.restorePosition(true));\r\n\r\n this.resizeBar = new ResizeBar(this.resizeDragHandler);\r\n this.resizeBar.on('showbuild', () => this.emit('showbuild'));\r\n\r\n this.headerView = this.titleBar;\r\n this._Box.insertBefore(this.headerView.element, this._Box.firstChild);\r\n\r\n const onListenedEvent = (eventName: keyof EventMap | keyof LegacyOSKEventMap) => {\r\n // As the following title bar buttons (for desktop / FloatingOSKView) do nothing unless a site\r\n // designer uses these events, we disable / hide them unless an event-handler is attached.\r\n let titleBar = this.headerView;\r\n if(titleBar && titleBar instanceof TitleBar) {\r\n switch(eventName) {\r\n case 'configclick':\r\n titleBar.configEnabled = this.legacyEvents.listenerCount('configclick') > 0;\r\n break;\r\n case 'helpclick':\r\n titleBar.helpEnabled = this.legacyEvents.listenerCount('helpclick') > 0;\r\n break;\r\n default:\r\n return;\r\n }\r\n }\r\n }\r\n\r\n const listenerSpyNew = new EmitterListenerSpy(this);\r\n const listenerSpyOld = new EmitterListenerSpy(this.legacyEvents);\r\n for(let listenerSpy of [listenerSpyNew, listenerSpyOld]) {\r\n listenerSpy.on('listeneradded', onListenedEvent);\r\n listenerSpy.on('listenerremoved', onListenedEvent);\r\n }\r\n\r\n if(this.activeKeyboard) {\r\n // If the keyboard was loaded during OSK init, we may need to set the\r\n // title in place now, as it wasn't possible at the standard time.\r\n this.postKeyboardAdjustments();\r\n }\r\n\r\n this.loadPersistedLayout();\r\n }\r\n\r\n private get typedActivationModel(): TwoStateActivator {\r\n return this.activationModel as TwoStateActivator;\r\n }\r\n\r\n /**\r\n * Function _Unload\r\n * Scope Private\r\n * Description Clears OSK variables prior to exit (JMD 1.9.1 - relocation of local variables 3/9/10)\r\n */\r\n _Unload() {\r\n this.keyboardView = null;\r\n this.bannerView = null;\r\n this._Box = null;\r\n }\r\n\r\n protected setBoxStyling() {\r\n const s = this._Box.style;\r\n\r\n s.zIndex = '9999';\r\n s.display = 'none';\r\n s.width = 'auto';\r\n s.position = 'absolute';\r\n }\r\n\r\n protected postKeyboardAdjustments() {\r\n // It is possible for this to be called during OSK initialization,\r\n // when `this.titleBar` has not yet been initialized.\r\n if(!this.titleBar) {\r\n return;\r\n }\r\n\r\n // Add header element to OSK only for desktop browsers\r\n this.enableMoveResizeHandlers();\r\n if(this.activeKeyboard) {\r\n this.titleBar.setTitleFromKeyboard(this.activeKeyboard.keyboard);\r\n }\r\n\r\n if(this.vkbd) {\r\n this.footerView = this.resizeBar;\r\n this._Box.appendChild(this.footerView.element);\r\n } else {\r\n if(this.footerView) {\r\n this._Box.removeChild(this.footerView.element);\r\n }\r\n this.footerView = null;\r\n }\r\n\r\n this.loadPersistedLayout();\r\n this.setNeedsLayout();\r\n }\r\n\r\n /**\r\n * Function restorePosition\r\n * Scope Public\r\n * @param {boolean?} keepDefaultPosition If true, does not reset the default x,y set by `setRect`.\r\n * If false or omitted, resets the default x,y as well.\r\n * Description Move OSK back to default position, floating under active input element\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/osk/restorePosition\r\n */\r\n public restorePosition: (keepDefaultPosition?: boolean) => void = function(this: FloatingOSKView, keepDefaultPosition?: boolean) {\r\n let isVisible = this._Visible;\r\n\r\n let dragPromise = new ManagedPromise();\r\n this.emit('dragmove', dragPromise.corePromise);\r\n\r\n this.loadPersistedLayout();\r\n this.userPositioned=false;\r\n if(!keepDefaultPosition) {\r\n delete this.dfltX;\r\n delete this.dfltY;\r\n }\r\n this.savePersistedLayout();\r\n\r\n if(isVisible) {\r\n this.present();\r\n }\r\n\r\n this.titleBar.showPin(false);\r\n dragPromise.resolve();\r\n this.doResizeMove(); //allow the UI to respond to OSK movements\r\n }.bind(this);\r\n\r\n /**\r\n * Function enabled\r\n * Scope Public\r\n * @return {boolean|number} True if KMW OSK enabled\r\n * Description Test if KMW OSK is enabled\r\n */\r\n ['isEnabled'](): boolean {\r\n return this.displayIfActive;\r\n }\r\n\r\n /**\r\n * Function isVisible\r\n * Scope Public\r\n * @return {boolean|number} True if KMW OSK visible\r\n * Description Test if KMW OSK is actually visible\r\n * Note that this will usually return false after any UI event that results in (temporary) loss of input focus\r\n */\r\n ['isVisible'](): boolean {\r\n return this._Visible;\r\n }\r\n\r\n /**\r\n * Save size, position, font size and visibility of OSK\r\n */\r\n private savePersistedLayout() {\r\n var p = this.getPos();\r\n\r\n const c: FloatingOSKCookie = {\r\n visible: this.displayIfActive ? 1 : 0,\r\n userSet: this.userPositioned ? 1 : 0,\r\n left: p.left,\r\n top: p.top,\r\n _version: Version.CURRENT.toString()\r\n }\r\n\r\n if(this.vkbd) {\r\n c.width = this.width.val;\r\n c.height = this.height.val;\r\n }\r\n\r\n this.layoutSerializer.save(c as Required);\r\n }\r\n\r\n /**\r\n * Restore size, position, font size and visibility of desktop OSK\r\n *\r\n * @return {boolean}\r\n */\r\n private loadPersistedLayout(): void {\r\n let c = this.layoutSerializer.loadWithDefaults({\r\n visible: 1,\r\n userSet: 0,\r\n left: -1,\r\n top: -1,\r\n _version: undefined,\r\n width: 0.3*screen.width,\r\n height: 0.15*screen.height\r\n });\r\n\r\n this.activationModel.enabled = c.visible == 1;\r\n this.userPositioned = c.userSet == 1;\r\n this.x = c.left;\r\n this.y = c.top;\r\n const cookieVersionString = c._version;\r\n\r\n // Restore OSK size - font size now fixed in relation to OSK height, unless overridden (in em) by keyboard\r\n const isNewCookie = cookieVersionString === undefined;\r\n let newWidth = c.width;\r\n let newHeight = c.height;\r\n\r\n // Limit the OSK dimensions to reasonable values\r\n if(newWidth < 0.2*screen.width) {\r\n newWidth = 0.2*screen.width;\r\n }\r\n if(newHeight < 0.1*screen.height) {\r\n newHeight = 0.1*screen.height;\r\n }\r\n if(newWidth > 0.9*screen.width) {\r\n newWidth=0.9*screen.width;\r\n }\r\n if(newHeight > 0.5*screen.height) {\r\n newHeight=0.5*screen.height;\r\n }\r\n\r\n // if(!cookieVersionString) - this component was not tracked until 15.0.\r\n // Before that point, the OSK's title bar and resize bar heights were not included\r\n // in the OSK's cookie-persisted height.\r\n if(isNewCookie || !cookieVersionString) {\r\n // Adds some space to account for the OSK's header and footer, should they exist.\r\n if(this.headerView && this.headerView.layoutHeight.absolute) {\r\n newHeight += this.headerView.layoutHeight.val;\r\n }\r\n\r\n if(this.footerView && this.footerView.layoutHeight.absolute) {\r\n newHeight += this.footerView.layoutHeight.val;\r\n }\r\n }\r\n\r\n this.setSize(newWidth, newHeight);\r\n\r\n // and OSK position if user located\r\n if(this.x == -1 || this.y == -1 || (!this._Box)) {\r\n this.userPositioned = false;\r\n }\r\n\r\n if(this.x < window.pageXOffset-0.8*newWidth) {\r\n this.x=window.pageXOffset-0.8*newWidth;\r\n }\r\n if(this.y < 0) {\r\n this.x=-1;\r\n this.y=-1;\r\n this.userPositioned=false;\r\n }\r\n\r\n if(this.userPositioned && this._Box) {\r\n this.setPos({'left': this.x, 'top': this.y});\r\n }\r\n }\r\n\r\n /**\r\n * Get the wanted height of the OSK for touch devices (does not include banner height)\r\n * @return {number} height in pixels\r\n **/\r\n getDefaultKeyboardHeight(): number {\r\n // KeymanTouch - get OSK height from device\r\n if(this.configuration.heightOverride) {\r\n return this.configuration.heightOverride();\r\n }\r\n\r\n var oskHeightLandscapeView=Math.floor(Math.min(screen.availHeight,screen.availWidth)/2),\r\n height=oskHeightLandscapeView;\r\n\r\n if(this.targetDevice.formFactor == 'phone') {\r\n var sx=Math.min(screen.height,screen.width),\r\n sy=Math.max(screen.height,screen.width);\r\n\r\n if(!landscapeView())\r\n height=Math.floor(Math.max(screen.availHeight,screen.availWidth)/3);\r\n else\r\n height=height*(sy/sx)/1.6; //adjust for aspect ratio, increase slightly for iPhone 5\r\n }\r\n\r\n // Correct for viewport scaling (iOS - Android 4.2 does not want this, at least on Galaxy Tab 3))\r\n if(this.targetDevice.OS == DeviceSpec.OperatingSystem.iOS) {\r\n height=height/getViewportScale(this.targetDevice.formFactor);\r\n }\r\n\r\n return height;\r\n }\r\n\r\n /**\r\n * Get the wanted width of the OSK for touch devices\r\n *\r\n * @return {number} height in pixels\r\n **/\r\n getDefaultWidth(): number {\r\n // KeymanTouch - get OSK height from device\r\n if(this.configuration.widthOverride) {\r\n return this.configuration.widthOverride();\r\n }\r\n\r\n var width: number;\r\n if(this.targetDevice.OS == DeviceSpec.OperatingSystem.iOS) {\r\n // iOS does not interchange these values when the orientation changes!\r\n //width = util.portraitView() ? screen.width : screen.height;\r\n width = window.innerWidth;\r\n } else if(this.targetDevice.OS == DeviceSpec.OperatingSystem.Android) {\r\n try {\r\n width=document.documentElement.clientWidth;\r\n } catch(ex) {\r\n width=screen.availWidth;\r\n }\r\n } else {\r\n width=screen.width;\r\n }\r\n\r\n return width;\r\n }\r\n\r\n /**\r\n * Allow UI to update OSK position and properties\r\n *\r\n * @param {Object=} p object with coordinates and userdefined flag\r\n *\r\n */\r\n doResizeMove(p?: any) {\r\n this.legacyEvents.callEvent('resizemove', p);\r\n }\r\n\r\n /**\r\n * Allow the UI or page to set the position and size of the OSK\r\n * and (optionally) override user repositioning or sizing\r\n *\r\n * @param {Object.} p Array object with position and size of OSK container\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/osk/setRect\r\n **/\r\n public setRect(p: OSKRect) {\r\n if(this._Box == null || this.targetDevice.formFactor != 'desktop') {\r\n return;\r\n }\r\n\r\n var b = this._Box, bs = b.style;\r\n if('left' in p) {\r\n this.x = p['left'] - getAbsoluteX(b) + b.offsetLeft;\r\n bs.left= this.x + 'px';\r\n this.dfltX=bs.left;\r\n }\r\n\r\n if('top' in p) {\r\n this.y = p['top'] - getAbsoluteY(b) + b.offsetTop;\r\n bs.top = this.y + 'px';\r\n this.dfltY=bs.top;\r\n }\r\n\r\n //Do not allow user resizing for non-standard keyboards (e.g. EuroLatin)\r\n if(this.vkbd != null) {\r\n var d=this.vkbd.kbdDiv, ds=d.style;\r\n\r\n // Set width, but limit to reasonable value\r\n if('width' in p) {\r\n var w=(p['width']-(b.offsetWidth-d.offsetWidth));\r\n if(w < 0.2*screen.width) {\r\n w=0.2*screen.width;\r\n }\r\n if(w > 0.9*screen.width) {\r\n w=0.9*screen.width;\r\n }\r\n ds.width=w+'px';\r\n // Use of the `computed` variant is here temporary.\r\n // Shouldn't use `setSize` for this in the long-term.\r\n this.setSize(w, this.computedHeight, true);\r\n }\r\n\r\n // Set height, but limit to reasonable value\r\n // This sets the default font size for the OSK in px, but that\r\n // can be modified at the key text level by setting\r\n // the font size in em in the kmw-key-text class\r\n if('height' in p) {\r\n var h=(p['height']-(b.offsetHeight-d.offsetHeight));\r\n if(h < 0.1*screen.height) {\r\n h=0.1*screen.height;\r\n }\r\n if(h > 0.5*screen.height) {\r\n h=0.5*screen.height;\r\n }\r\n ds.height=h+'px'; ds.fontSize=(h/8)+'px';\r\n // Use of the `computed` variant is here temporary.\r\n // Shouldn't use `setSize` for this in the long-term.\r\n this.setSize(this.computedWidth, h, true);\r\n }\r\n\r\n // Fix or release user resizing\r\n if('nosize' in p) {\r\n this.resizingEnabled = !p['nosize'];\r\n }\r\n\r\n }\r\n // Fix or release user dragging\r\n if('nomove' in p) {\r\n this.noDrag=p['nomove'];\r\n this.movementEnabled = !this.noDrag;\r\n }\r\n // Save the user-defined OSK size\r\n this.savePersistedLayout();\r\n }\r\n\r\n /**\r\n * Get position of OSK window\r\n *\r\n * @return {Object.} Array object with OSK window position\r\n **/\r\n getPos(): OSKPos {\r\n var Lkbd=this._Box, p={\r\n left: this._Visible ? Lkbd.offsetLeft : this.x,\r\n top: this._Visible ? Lkbd.offsetTop : this.y\r\n };\r\n\r\n return p;\r\n }\r\n\r\n /**\r\n * Function setPos\r\n * Scope Private\r\n * @param {Object.} p Array object with OSK left, top\r\n * Description Set position of OSK window, but limit to screen\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/osk/setPos\r\n */\r\n public setPos(p: OSKPos) {\r\n if(typeof(this._Box) == 'undefined') {\r\n return; // I3363 (Build 301)\r\n }\r\n\r\n if(this.userPositioned) {\r\n var Px=p['left'], Py=p['top'];\r\n\r\n if(typeof(Px) != 'undefined') {\r\n if(Px < -0.8*this._Box.offsetWidth) {\r\n Px = -0.8*this._Box.offsetWidth;\r\n }\r\n if(this.userPositioned) {\r\n this._Box.style.left=Px+'px';\r\n this.x = Px;\r\n }\r\n }\r\n // May not be needed - vertical positioning is handled differently and defaults to input field if off screen\r\n if(typeof(Py) != 'undefined') {\r\n if(Py < 0) {\r\n Py = 0;\r\n }\r\n\r\n if(this.userPositioned) {\r\n this._Box.style.top=Py+'px';\r\n this.y = Py;\r\n }\r\n }\r\n }\r\n\r\n this.titleBar.showPin(this.userPositioned);\r\n }\r\n\r\n public setDisplayPositioning() {\r\n var Ls = this._Box.style;\r\n\r\n Ls.position='absolute';\r\n // Keep it hidden if not currently displayed.\r\n if(this.activationModel.activate) {\r\n Ls.display='block'; //Ls.visibility='visible';\r\n }\r\n Ls.left='0px';\r\n if(this.specifiedPosition || this.userPositioned) {\r\n Ls.left = this.x+'px';\r\n Ls.top = this.y+'px';\r\n } else {\r\n let el: HTMLElement = this.typedActivationModel.activationTrigger || null;\r\n\r\n if(this.dfltX) {\r\n Ls.left=this.dfltX;\r\n } else if(typeof el != 'undefined' && el != null) {\r\n Ls.left=getAbsoluteX(el) + 'px';\r\n }\r\n\r\n if(this.dfltY) {\r\n Ls.top=this.dfltY;\r\n } else if(typeof el != 'undefined' && el != null) {\r\n Ls.top=(getAbsoluteY(el) + el.offsetHeight)+'px';\r\n }\r\n }\r\n\r\n // Unset the flag, keeping 'specified position' specific to single\r\n // presentAtPosition calls.\r\n this.specifiedPosition = false;\r\n }\r\n\r\n /**\r\n * Display KMW OSK at specified position (returns nothing)\r\n *\r\n * @param {number=} Px x-coordinate for OSK rectangle\r\n * @param {number=} Py y-coordinate for OSK rectangle\r\n */\r\n presentAtPosition(Px?: number, Py?: number) {\r\n if(!this.mayShow()) {\r\n return;\r\n }\r\n\r\n this.specifiedPosition = Px >= 0 || Py >= 0; //probably never happens, legacy support only\r\n if(this.specifiedPosition) {\r\n this.x = Px;\r\n this.y = Py;\r\n }\r\n\r\n // Combines the two paths with set positioning.\r\n this.specifiedPosition = this.specifiedPosition || this.userPositioned;\r\n\r\n this.present();\r\n }\r\n\r\n present() {\r\n if(!this.mayShow()) {\r\n return;\r\n }\r\n\r\n this.titleBar.showPin(this.userPositioned);\r\n\r\n super.present();\r\n\r\n // Allow desktop UI to execute code when showing the OSK\r\n this.doShow({\r\n x: this._Box.offsetLeft,\r\n y: this._Box.offsetTop,\r\n userLocated: this.userPositioned\r\n });\r\n }\r\n\r\n public startHide(hiddenByUser: boolean) {\r\n super.startHide(hiddenByUser);\r\n\r\n if(hiddenByUser) {\r\n this.savePersistedLayout(); // Save current OSK state, size and position (desktop only)\r\n }\r\n }\r\n\r\n ['show'](bShow?: boolean) {\r\n if(bShow !== undefined) {\r\n super['show'](bShow);\r\n } else {\r\n super['show']();\r\n }\r\n this.savePersistedLayout();\r\n }\r\n\r\n /**\r\n * Function userPositioned\r\n * Scope Public\r\n * @return {(boolean|number)} true if user located\r\n * Description Test if OSK window has been repositioned by user\r\n *\r\n * See https://help.keyman.com/developer/engine/web/current-version/reference/osk/userLocated\r\n */\r\n public userLocated() {\r\n return this.userPositioned;\r\n }\r\n\r\n public get movementEnabled(): boolean {\r\n return this.titleDragHandler.enabled;\r\n }\r\n\r\n public set movementEnabled(flag: boolean) {\r\n this.titleDragHandler.enabled = flag;\r\n this.titleBar.showPin(flag && this.userPositioned);\r\n }\r\n\r\n public get resizingEnabled(): boolean {\r\n return this.resizeDragHandler.enabled;\r\n }\r\n\r\n public set resizingEnabled(flag: boolean) {\r\n this.resizeDragHandler.enabled = flag;\r\n this.resizeBar.allowResizing(flag);\r\n }\r\n\r\n public get isBeingMoved(): boolean {\r\n return this.titleDragHandler.isActive;\r\n }\r\n\r\n public get isBeingResized(): boolean {\r\n return this.resizeDragHandler.isActive;\r\n }\r\n\r\n private enableMoveResizeHandlers() {\r\n this.titleDragHandler.enabled = !this.noDrag;\r\n this.resizeDragHandler.enabled = true; // by default.\r\n }\r\n\r\n private get titleDragHandler(): MouseDragOperation {\r\n const _this = this;\r\n\r\n if(this._moveHandler) {\r\n return this._moveHandler;\r\n }\r\n\r\n this._moveHandler = new class extends MouseDragOperation {\r\n startX: number;\r\n startY: number;\r\n\r\n dragPromise: ManagedPromise;\r\n\r\n constructor() {\r\n super('move'); // The type of cursor to use while 'active'.\r\n }\r\n\r\n onDragStart() {\r\n this.startX = _this._Box.offsetLeft;\r\n this.startY = _this._Box.offsetTop;\r\n\r\n if(_this.activeKeyboard.keyboard.isCJK) {\r\n _this.titleBar.setPinCJKOffset();\r\n }\r\n\r\n if(this.dragPromise) {\r\n // We got interrupted during the previous one; allow it to reset, at least!\r\n this.dragPromise.resolve();\r\n }\r\n\r\n this.dragPromise = new ManagedPromise();\r\n _this.emit('dragmove', this.dragPromise.corePromise);\r\n }\r\n\r\n onDragMove(cumulativeX: number, cumulativeY: number) {\r\n _this.titleBar.showPin(true);\r\n _this.userPositioned = true;\r\n\r\n _this._Box.style.left = (this.startX + cumulativeX) + 'px';\r\n _this._Box.style.top = (this.startY + cumulativeY) + 'px';\r\n\r\n var r=_this.getRect();\r\n _this.setSize(r.width, r.height, true);\r\n _this.x = r.left;\r\n _this.y = r.top;\r\n }\r\n\r\n onDragRelease() {\r\n if(_this.vkbd) {\r\n _this.vkbd.currentKey=null;\r\n }\r\n\r\n this.dragPromise.resolve();\r\n\r\n // Remainder should be done after anything else pending on the Promise.\r\n this.dragPromise.then(() => {\r\n _this.userPositioned = true;\r\n _this.doResizeMove();\r\n _this.savePersistedLayout();\r\n });\r\n this.dragPromise = null;\r\n }\r\n }\r\n\r\n return this._moveHandler;\r\n }\r\n\r\n private get resizeDragHandler(): MouseDragOperation {\r\n const _this = this;\r\n\r\n if(this._resizeHandler) {\r\n return this._resizeHandler;\r\n }\r\n\r\n this._resizeHandler = new class extends MouseDragOperation {\r\n startWidth: number;\r\n startHeight: number;\r\n\r\n dragPromise: ManagedPromise;\r\n\r\n constructor() {\r\n super('se-resize'); // The type of cursor to use while 'active'.\r\n }\r\n\r\n onDragStart() {\r\n this.startWidth = _this.computedWidth;\r\n this.startHeight = _this.computedHeight;\r\n\r\n if(this.dragPromise) {\r\n // We got interrupted during the previous one; allow it to reset, at least!\r\n this.dragPromise.resolve();\r\n }\r\n\r\n this.dragPromise = new ManagedPromise();\r\n _this.emit('resizemove', this.dragPromise.corePromise);\r\n }\r\n\r\n onDragMove(cumulativeX: number, cumulativeY: number) {\r\n let newWidth = this.startWidth + cumulativeX;\r\n let newHeight = this.startHeight + cumulativeY;\r\n\r\n // Set the smallest and largest OSK size\r\n if(newWidth < 0.2*screen.width) {\r\n newWidth = 0.2*screen.width;\r\n }\r\n if(newHeight < 0.1*screen.height) {\r\n newHeight = 0.1*screen.height;\r\n }\r\n if(newWidth > 0.9*screen.width) {\r\n newWidth = 0.9*screen.width;\r\n }\r\n if(newHeight > 0.5*screen.height) {\r\n newHeight = 0.5*screen.height;\r\n }\r\n\r\n // Explicitly set OSK width, height, and font size - cannot safely rely on scaling from font\r\n _this.setSize(newWidth, newHeight, true);\r\n }\r\n\r\n onDragRelease() {\r\n if(_this.vkbd) {\r\n _this.vkbd.currentKey=null;\r\n }\r\n\r\n if(_this.vkbd) {\r\n this.startWidth = _this.computedWidth;\r\n this.startHeight = _this.computedHeight;\r\n }\r\n\r\n _this.refreshLayout(); // Finalize the resize.\r\n\r\n this.dragPromise.resolve();\r\n\r\n // Remainder should be done after anything else pending on the Promise.\r\n this.dragPromise.then(() => {\r\n _this.doResizeMove();\r\n _this.savePersistedLayout();\r\n });\r\n this.dragPromise = null;\r\n }\r\n }\r\n\r\n return this._resizeHandler;\r\n }\r\n}\r\n", + "import { DeviceSpec } from 'keyman/engine/keyboard';\r\nimport { landscapeView } from 'keyman/engine/dom-utils';\r\n\r\nimport OSKView, { OSKPos, OSKRect } from './oskView.js';\r\nimport { getViewportScale } from '../screenUtils.js';\r\nimport Configuration from '../config/viewConfiguration.js';\r\nimport { StaticActivator } from './activator.js';\r\nimport TwoStateActivator from './twoStateActivator.js';\r\n\r\n/***\r\n KeymanWeb 10.0\r\n Copyright 2017 SIL International\r\n***/\r\n\r\nexport default class AnchoredOSKView extends OSKView {\r\n\r\n // OSK positioning fields\r\n x: number;\r\n y: number;\r\n\r\n private isResizing: boolean = false;\r\n\r\n public constructor(config: Configuration) {\r\n if(config.isEmbedded) {\r\n config.activator = config.activator || new StaticActivator();\r\n } else {\r\n config.activator = config.activator || new TwoStateActivator();\r\n }\r\n super(config);\r\n\r\n document.body.appendChild(this._Box);\r\n\r\n }\r\n\r\n /**\r\n * Function _Unload\r\n * Scope Private\r\n * Description Clears OSK variables prior to exit (JMD 1.9.1 - relocation of local variables 3/9/10)\r\n */\r\n _Unload() {\r\n this.keyboardView = null;\r\n this.bannerView = null;\r\n this._Box = null;\r\n }\r\n\r\n protected setBoxStyling() {\r\n const s = this._Box.style;\r\n\r\n s.zIndex = '9999';\r\n s.display = 'none';\r\n s.width = '100%';\r\n s.position = 'fixed';\r\n }\r\n\r\n /**\r\n * @override\r\n */\r\n public refreshLayout(pending?: boolean): void {\r\n // This function is generally triggered whenever the OSK's dimensions change, among other\r\n // things.\r\n if(this.isResizing) {\r\n return;\r\n }\r\n\r\n try {\r\n this.isResizing = true;\r\n // This resizes the OSK to what is appropriate for the device's current orientation,\r\n // which will often trigger a resize event... which in turn triggers a layout refresh.\r\n //\r\n // So, we mark and unmark the `isResizing` flag to prevent triggering a circular\r\n // call-stack chain from this call.\r\n this.doResize();\r\n } finally {\r\n this.isResizing = false;\r\n }\r\n super.refreshLayout(pending);\r\n }\r\n\r\n protected doResize() {\r\n if(this.vkbd) {\r\n let targetOSKHeight = this.getDefaultKeyboardHeight();\r\n this.setSize(this.getDefaultWidth(), targetOSKHeight + this.banner.height);\r\n }\r\n }\r\n\r\n protected postKeyboardAdjustments() {\r\n // Initializes the size of a touch keyboard.\r\n this.doResize();\r\n }\r\n\r\n /**\r\n * Function restorePosition\r\n * Scope Public\r\n * @param {boolean?} keepDefaultPosition If true, does not reset the default x,y set by `setRect`.\r\n * If false or omitted, resets the default x,y as well.\r\n * Description Move OSK back to default position, floating under active input element\r\n */\r\n ['restorePosition']: (keepDefaultPosition?: boolean) => void = function(this: AnchoredOSKView, keepDefaultPosition?: boolean) {\r\n return;\r\n }.bind(this);\r\n\r\n /**\r\n * Get the wanted height of the OSK for touch devices (does not include banner height)\r\n * @return {number} height in pixels\r\n **/\r\n getDefaultKeyboardHeight(): number {\r\n let device = this.targetDevice;\r\n\r\n // KeymanTouch - get OSK height from device\r\n if(this.configuration.heightOverride) {\r\n return this.configuration.heightOverride();\r\n }\r\n\r\n /*\r\n * We've noticed some fairly inconsistent behavior in the past when attempting to base\r\n * this logic on window.innerWidth/Height, as there can be very unexpected behavior\r\n * on mobile devices during and after rotation.\r\n *\r\n * Online forums (such as https://stackoverflow.com/a/54812656) seem to indicate that\r\n * document.documentElement.clientWidth/Height seem to be the most stable analogues\r\n * to a window's size in the situations where it matters for Keyman Engine for Web.\r\n *\r\n * That said, an important note: this gets the dimensions of the _document element_,\r\n * not the screen or even the window.\r\n */\r\n let baseWidth = document?.documentElement?.clientWidth;\r\n let baseHeight = document?.documentElement?.clientHeight;\r\n if(typeof baseWidth == 'undefined') {\r\n /*\r\n * Fallback logic. We _shouldn't_ need this, but it's best to have _something_\r\n * for the sake of robustness.\r\n */\r\n baseWidth = Math.min(screen.height, screen.width);\r\n baseHeight = Math.max(screen.height, screen.width);\r\n\r\n if(landscapeView()) {\r\n let temp = baseWidth;\r\n baseWidth = baseHeight;\r\n baseHeight = temp;\r\n }\r\n }\r\n\r\n var oskHeightLandscapeView=Math.floor(Math.min(baseHeight, baseWidth)/2),\r\n height=oskHeightLandscapeView;\r\n\r\n if(device.formFactor == 'phone') {\r\n /**\r\n * Assuming the first-pass detection of width and height work correctly, note\r\n * that these calculations are based on the document's size, not the device's\r\n * resolution. This _particularly_ matters for height.\r\n *\r\n * - Is the mobile-device browser showing a URL bar? That's not included.\r\n * - The standard signal-strength, battery-strength, etc device status bar?\r\n * Also not included.\r\n */\r\n if(!landscapeView())\r\n height=Math.floor(baseHeight/2.4);\r\n else\r\n height=Math.floor(baseHeight/1.6); //adjust for aspect ratio, increase slightly for iPhone 5\r\n }\r\n\r\n // Correct for viewport scaling (iOS - Android 4.2 does not want this, at least on Galaxy Tab 3))\r\n if(this.targetDevice.OS == DeviceSpec.OperatingSystem.iOS) {\r\n height=height/getViewportScale(this.targetDevice.formFactor);\r\n }\r\n\r\n return height;\r\n }\r\n\r\n /**\r\n * Get the wanted width of the OSK for touch devices\r\n *\r\n * @return {number} height in pixels\r\n **/\r\n getDefaultWidth(): number {\r\n let device = this.targetDevice;\r\n\r\n // KeymanTouch - get OSK height from device\r\n if(this.configuration.widthOverride) {\r\n return this.configuration.widthOverride();\r\n }\r\n\r\n var width: number;\r\n\r\n width = document?.documentElement?.clientWidth;\r\n if(typeof width == 'undefined') {\r\n if(this.targetDevice.OS == DeviceSpec.OperatingSystem.iOS) {\r\n width = window.innerWidth;\r\n } else if(device.OS == DeviceSpec.OperatingSystem.Android) {\r\n width=screen.availWidth;\r\n } else {\r\n width=screen.width;\r\n }\r\n }\r\n\r\n return width;\r\n }\r\n\r\n /**\r\n * Allow the UI or page to set the position and size of the OSK\r\n * and (optionally) override user repositioning or sizing\r\n *\r\n * @param {Object.} p Array object with position and size of OSK container\r\n **/\r\n ['setRect'](p: OSKRect) {\r\n return;\r\n }\r\n\r\n /**\r\n * Get position of OSK window\r\n *\r\n * @return {Object.} Array object with OSK window position\r\n **/\r\n getPos(): OSKPos {\r\n var Lkbd=this._Box, p={\r\n left: this._Visible ? Lkbd.offsetLeft : this.x,\r\n top: this._Visible ? Lkbd.offsetTop : this.y\r\n };\r\n\r\n return p;\r\n }\r\n\r\n /**\r\n * Function setPos\r\n * Scope Private\r\n * @param {Object.} p Array object with OSK left, top\r\n * Description Set position of OSK window, but limit to screen, and ignore if a touch input device\r\n */\r\n ['setPos'](p: OSKPos) {\r\n return; // I3363 (Build 301)\r\n }\r\n\r\n protected setDisplayPositioning() {\r\n let Ls = this._Box.style;\r\n\r\n // The following code will always be executed except for externally created OSK such as EuroLatin\r\n if(this.vkbd) {\r\n Ls.position='fixed';\r\n Ls.left=Ls.bottom='0px';\r\n Ls.border='none';\r\n Ls.borderTop='1px solid gray';\r\n }\r\n }\r\n\r\n public present() {\r\n super.present();\r\n this.legacyEvents.callEvent('show', {});\r\n }\r\n}\r\n", + "import Activator from './activator.js';\r\n\r\nexport default class SimpleActivator extends Activator {\r\n private flag: boolean = true;\r\n\r\n get enabled(): boolean {\r\n return this.flag;\r\n }\r\n\r\n set enabled(value: boolean) {\r\n // Enabled + activated are the same thing for this class.\r\n this.activate = value;\r\n }\r\n\r\n get activate(): boolean {\r\n return this.flag;\r\n }\r\n\r\n set activate(value: boolean) {\r\n if(this.flag != value) {\r\n this.flag = value;\r\n this.emit('activate', value);\r\n }\r\n }\r\n\r\n get conditionsMet(): boolean {\r\n return true;\r\n }\r\n}", + "import { DeviceSpec } from 'keyman/engine/keyboard';\r\n\r\nimport OSKView, { OSKPos, OSKRect } from './oskView.js';\r\nimport VisualKeyboard from '../visualKeyboard.js';\r\nimport Configuration from '../config/viewConfiguration.js';\r\nimport SimpleActivator from './simpleActivator.js';\r\n\r\n/*\r\n * Keyman is copyright (c) SIL International. MIT License.\r\n */\r\n\r\n/**\r\n * Defines a version of the OSK that produces an element designed for site-controlled\r\n * insertion into the DOM. Rather than \"floating\" over the page, this version is inlined\r\n * as part of the host page's layout.\r\n */\r\nexport default class InlinedOSKView extends OSKView {\r\n public constructor(config: Configuration) {\r\n config.activator = config.activator || new SimpleActivator();\r\n super(config);\r\n }\r\n\r\n public get element(): HTMLDivElement {\r\n return this._Box;\r\n }\r\n\r\n /**\r\n * Clears OSK variables prior to exit (JMD 1.9.1 - relocation of local variables 3/9/10)\r\n *\r\n * This should probably be merged or incorporated into the `shutdown` method at some point.\r\n */\r\n _Unload() {\r\n this.keyboardView = null;\r\n this.bannerView = null;\r\n this._Box = null;\r\n }\r\n\r\n protected setBoxStyling() {\r\n const s = this._Box.style;\r\n s.display = 'none';\r\n // Positioned with no relative offset from its default position.\r\n // This allows _Box to still serve as an offsetParent for keytip & subkey menu positioning.\r\n s.position = 'relative';\r\n }\r\n\r\n protected postKeyboardAdjustments() {\r\n }\r\n\r\n /**\r\n * Moves the OSK back to default position, floating under active input element\r\n *\r\n * Is a long-published API intended solely for use with the FloatingOSKView use pattern.\r\n * @param keepDefaultPosition If true, does not reset the default x,y set by `setRect`.\r\n * If false or omitted, resets the default x,y as well.\r\n */\r\n ['restorePosition']: (keepDefaultPosition?: boolean) => void = function(this: InlinedOSKView, keepDefaultPosition?: boolean) {\r\n return;\r\n }.bind(this);\r\n\r\n /**\r\n * Get the default height for the OSK\r\n * @return height in pixels\r\n **/\r\n getDefaultKeyboardHeight(): number {\r\n if(this.keyboardView instanceof VisualKeyboard) {\r\n return this.keyboardView.height;\r\n } else {\r\n // Should probably refine, but it's a decent stopgap.\r\n return this.computedHeight;\r\n }\r\n }\r\n\r\n /**\r\n * Get the default width for the OSK\r\n * @return width in pixels\r\n **/\r\n getDefaultWidth(): number {\r\n return this.computedWidth;\r\n }\r\n\r\n /**\r\n * Allow the UI or page to set the position and size of the OSK\r\n * and (optionally) override user repositioning or sizing\r\n *\r\n * Designed solely for use with the FloatingOSKView use pattern, but is a\r\n * long-standing API endpoint that needs preservation.\r\n *\r\n * @param p Array object with position and size of OSK container\r\n **/\r\n ['setRect'](p: OSKRect) {\r\n return;\r\n }\r\n\r\n /**\r\n * Get position of OSK window\r\n *\r\n * @return Array object with OSK window position\r\n **/\r\n getPos(): OSKPos {\r\n var Lkbd=this._Box, p={\r\n left: this._Visible ? Lkbd.offsetLeft : undefined,\r\n top: this._Visible ? Lkbd.offsetTop : undefined\r\n };\r\n\r\n return p;\r\n }\r\n\r\n /**\r\n * Set position of OSK window, but limited to the screen.\r\n *\r\n * Designed solely for use with the FloatingOSKView use pattern, but is a\r\n * long-standing API endpoint that needs preservation.\r\n * @param p Array object with OSK left, top\r\n */\r\n ['setPos'](p: OSKPos) {\r\n return; // I3363 (Build 301)\r\n }\r\n\r\n public present() {\r\n super.present();\r\n\r\n this.legacyEvents.callEvent('show', {});\r\n }\r\n\r\n protected setDisplayPositioning() {\r\n // no-op; an inlined OSK cannot control its own positioning.\r\n }\r\n\r\n protected allowsDeviceChange(newSpec: DeviceSpec): boolean {\r\n return true;\r\n }\r\n}\r\n", + "import {\r\n ViewConfiguration,\r\n AnchoredOSKView,\r\n FloatingOSKView,\r\n FloatingOSKViewConfiguration,\r\n InlinedOSKView\r\n} from \"keyman/engine/osk\";\r\nimport KeymanEngine from \"./keymanEngine.js\";\r\n\r\nfunction buildBaseOskConfiguration(engine: KeymanEngine) {\r\n return {\r\n hostDevice: engine.config.hostDevice,\r\n pathConfig: engine.config.paths,\r\n predictionContextManager: engine.contextManager.predictionContext,\r\n isEmbedded: false\r\n };\r\n};\r\n\r\nclass PublishedAnchoredOSKView extends AnchoredOSKView {\r\n constructor(engine: KeymanEngine, config?: ViewConfiguration) {\r\n let finalConfig = {\r\n ...buildBaseOskConfiguration(engine),\r\n ...(config || {})\r\n };\r\n\r\n super(finalConfig);\r\n }\r\n}\r\n\r\nclass PublishedFloatingOSKView extends FloatingOSKView {\r\n constructor(engine: KeymanEngine, config?: FloatingOSKViewConfiguration) {\r\n let finalConfig: FloatingOSKViewConfiguration = {\r\n ...buildBaseOskConfiguration(engine),\r\n ...(config || {})\r\n };\r\n\r\n super(finalConfig);\r\n }\r\n}\r\n\r\nclass PublishedInlineOSKView extends InlinedOSKView {\r\n constructor(engine: KeymanEngine, config?: ViewConfiguration) {\r\n let finalConfig: ViewConfiguration = {\r\n ...buildBaseOskConfiguration(engine),\r\n ...(config || {})\r\n };\r\n\r\n super(finalConfig);\r\n }\r\n}\r\n\r\nexport { PublishedAnchoredOSKView as AnchoredOSKView };\r\nexport { PublishedFloatingOSKView as FloatingOSKView };\r\nexport { PublishedInlineOSKView as InlinedOSKView };\r\n\r\n", + "import { OutputTarget as OutputTargetBase } from \"keyman/engine/js-processor\";\r\nimport { EventEmitter } from 'eventemitter3';\r\n\r\nexport default abstract class OutputTarget extends OutputTargetBase {\r\n // JS/TS can't do true multiple inheritance, so we maintain class events on a readonly field.\r\n public readonly events: EventEmitter = new EventEmitter();\r\n\r\n /**\r\n * A field that may be used to track whether or not the represented context has changed over an\r\n * arbitrary period of time.\r\n */\r\n public changed = false;\r\n\r\n /**\r\n * Returns the underlying element / document modeled by the wrapper.\r\n */\r\n abstract getElement(): HTMLElement;\r\n\r\n public focus(): void {\r\n const ele = this.getElement();\r\n if(ele.focus) {\r\n ele.focus();\r\n }\r\n }\r\n\r\n /**\r\n * Denotes when the represented element is forcing a text scroll via focus manipulation.\r\n * As the intent is not to change the focused element, but just to have the browser update\r\n * the scroll location, standard focus handlers (for updating the active context) should\r\n * not deactivate the element while this state is active.\r\n */\r\n isForcingScroll(): boolean {\r\n return false;\r\n }\r\n\r\n /**\r\n * A helper method for doInputEvent; creates a simple common event and default dispatching.\r\n * @param elem\r\n */\r\n protected dispatchInputEventOn(elem: HTMLElement) {\r\n let event: InputEvent;\r\n\r\n // `undefined` in pre-Chrome Edge and Chrome for Android before version 60.\r\n if(window['InputEvent']) { // can't condition on the type directly; TS optimizes that out.\r\n event = new InputEvent('input', {\"bubbles\": true, \"cancelable\": false});\r\n }\r\n\r\n if(elem && event) {\r\n elem.dispatchEvent(event);\r\n }\r\n }\r\n}", + "import OutputTarget from './outputTarget.js';\r\n\r\ninterface EventMap {\r\n /**\r\n * This event will be raised when a newline is received by wrapped elements not of\r\n * the 'search' or 'submit' types.\r\n *\r\n * Original code this is replacing:\r\n ```\r\n // Allows compiling this separately from the main body of KMW.\r\n // TODO: rework class to accept a class-static 'callback' from the DOM module that this can call.\r\n // Would eliminate the need for this 'static' reference.\r\n // Only strongly matters once we better modularize KMW, with web-dom vs web-dom-targets vs web-core, etc.\r\n if(com.keyman[\"singleton\"]) {\r\n com.keyman[\"singleton\"].domManager.moveToNext(false);\r\n }\r\n ```\r\n * This does not belong in a modularized version of this class; it must be supplied\r\n * by the consuming top-level products instead.\r\n */\r\n 'unhandlednewline': (element: HTMLInputElement) => void\r\n}\r\n\r\nexport default class Input extends OutputTarget {\r\n root: HTMLInputElement;\r\n\r\n /**\r\n * Tracks the most recently-cached selection start index.\r\n */\r\n private _cachedSelectionStart: number\r\n\r\n /**\r\n * Tracks the most recently processed, extended-string-based selection start index.\r\n * When the element's selectionStart value changes, this should be invalidated.\r\n */\r\n private processedSelectionStart: number;\r\n\r\n /**\r\n * Tracks the most recently processed, extended-string-based selection end index.\r\n * When the element's selectionEnd value changes, this should be invalidated.\r\n */\r\n private processedSelectionEnd: number;\r\n\r\n /**\r\n * Set, then unset within the `forceScroll` method in order to facilitate the\r\n * `isForcingScroll` flag.\r\n */\r\n private _activeForcedScroll: boolean;\r\n\r\n constructor(ele: HTMLInputElement) {\r\n super();\r\n\r\n this.root = ele;\r\n this._cachedSelectionStart = -1;\r\n }\r\n\r\n get isSynthetic(): boolean {\r\n return false;\r\n }\r\n\r\n static isSupportedType(type: string): boolean {\r\n return type == 'email' || type == 'search' || type == 'text' || type == 'url';\r\n }\r\n\r\n getElement(): HTMLInputElement {\r\n return this.root;\r\n }\r\n\r\n clearSelection(): void {\r\n // Processes our codepoint-based variants of selectionStart and selectionEnd.\r\n this.getCaret(); // updates processedSelectionStart if required\r\n this.root.value = this.root.value._kmwSubstring(0, this.processedSelectionStart) + this.root.value._kmwSubstring(this.processedSelectionEnd); //I3319\r\n\r\n this.setCaret(this.processedSelectionStart);\r\n }\r\n\r\n isSelectionEmpty(): boolean {\r\n return this.root.selectionStart == this.root.selectionEnd;\r\n }\r\n\r\n hasSelection(): boolean {\r\n return true;\r\n }\r\n\r\n invalidateSelection() {\r\n // Since .selectionStart will never return this value, we use it to indicate\r\n // the need to refresh our processed indices.\r\n this._cachedSelectionStart = -1;\r\n }\r\n\r\n getCaret(): number {\r\n if(this.root.selectionStart != this._cachedSelectionStart) {\r\n this._cachedSelectionStart = this.root.selectionStart; // KMW-1\r\n this.processedSelectionStart = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionStart); // I3319\r\n this.processedSelectionEnd = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionEnd); // I3319\r\n }\r\n return this.root.selectionDirection == 'forward' ? this.processedSelectionEnd : this.processedSelectionStart;\r\n }\r\n\r\n getDeadkeyCaret(): number {\r\n return this.getCaret();\r\n }\r\n\r\n setCaret(caret: number) {\r\n this.setSelection(caret, caret, \"none\");\r\n }\r\n\r\n setSelection(start: number, end: number, direction: \"forward\" | \"backward\" | \"none\") {\r\n let domStart = this.root.value._kmwCodePointToCodeUnit(start);\r\n let domEnd = this.root.value._kmwCodePointToCodeUnit(end);\r\n this.root.setSelectionRange(domStart, domEnd, direction);\r\n\r\n this.processedSelectionStart = start;\r\n this.processedSelectionEnd = end;\r\n\r\n this.forceScroll();\r\n\r\n this.root.setSelectionRange(domStart, domEnd, direction);\r\n }\r\n\r\n forceScroll() {\r\n // Only executes when com.keyman.DOMEventHandlers is defined.\r\n //\r\n // We bypass this whenever operating in the embedded format.\r\n const element = this.getElement();\r\n\r\n let selectionStart = element.selectionStart;\r\n let selectionEnd = element.selectionEnd;\r\n\r\n this._activeForcedScroll = true;\r\n\r\n try {\r\n //Forces scrolling; the re-focus triggers the scroll, at least.\r\n element.blur();\r\n element.focus();\r\n } finally {\r\n // On Edge, it appears that the blur/focus combination will reset the caret position\r\n // under certain scenarios during unit tests. So, we re-set it afterward.\r\n element.selectionStart = selectionStart;\r\n element.selectionEnd = selectionEnd;\r\n this._activeForcedScroll = false;\r\n }\r\n }\r\n\r\n isForcingScroll(): boolean {\r\n return this._activeForcedScroll;\r\n }\r\n\r\n getSelectionDirection(): \"forward\" | \"backward\" | \"none\" {\r\n return this.root.selectionDirection;\r\n }\r\n\r\n getTextBeforeCaret(): string {\r\n this.getCaret();\r\n return this.getText()._kmwSubstring(0, this.processedSelectionStart);\r\n }\r\n\r\n getSelectedText(): string {\r\n this.getCaret();\r\n return this.getText()._kmwSubstring(this.processedSelectionStart, this.processedSelectionEnd);\r\n }\r\n\r\n setTextBeforeCaret(text: string) {\r\n this.getCaret();\r\n let selectionLength = this.processedSelectionEnd - this.processedSelectionStart;\r\n let direction = this.getSelectionDirection();\r\n let newCaret = text._kmwLength();\r\n this.root.value = text + this.getText()._kmwSubstring(this.processedSelectionStart);\r\n\r\n this.setSelection(newCaret, newCaret + selectionLength, direction);\r\n }\r\n\r\n protected setTextAfterCaret(s: string) {\r\n let direction = this.getSelectionDirection();\r\n\r\n this.root.value = this.getTextBeforeCaret() + s;\r\n this.setSelection(this.processedSelectionStart, this.processedSelectionEnd, direction);\r\n }\r\n\r\n getTextAfterCaret(): string {\r\n this.getCaret();\r\n return this.getText()._kmwSubstring(this.processedSelectionEnd);\r\n }\r\n\r\n getText(): string {\r\n return this.root.value;\r\n }\r\n\r\n deleteCharsBeforeCaret(dn: number) {\r\n if(dn > 0) {\r\n let curText = this.getTextBeforeCaret();\r\n let caret = this.processedSelectionStart;\r\n\r\n if(dn > caret) {\r\n dn = caret;\r\n }\r\n\r\n this.adjustDeadkeys(-dn);\r\n this.setTextBeforeCaret(curText.kmwSubstring(0, caret - dn));\r\n this.setCaret(caret - dn);\r\n }\r\n }\r\n\r\n insertTextBeforeCaret(s: string) {\r\n if(!s) {\r\n return;\r\n }\r\n\r\n let caret = this.getCaret();\r\n let front = this.getTextBeforeCaret();\r\n let back = this.getText()._kmwSubstring(this.processedSelectionStart);\r\n\r\n this.adjustDeadkeys(s._kmwLength());\r\n this.root.value = front + s + back;\r\n this.setCaret(caret + s._kmwLength());\r\n }\r\n\r\n handleNewlineAtCaret(): void {\r\n const inputEle = this.root;\r\n // Can't occur for Mocks - just Input types.\r\n if (inputEle && (inputEle.type == 'search' || inputEle.type == 'submit')) {\r\n inputEle.disabled=false;\r\n inputEle.form.submit();\r\n } else {\r\n this.events.emit('unhandlednewline', inputEle);\r\n }\r\n }\r\n\r\n doInputEvent() {\r\n this.dispatchInputEventOn(this.root);\r\n }\r\n}", + "import OutputTarget from './outputTarget.js';\r\n\r\nexport default class TextArea extends OutputTarget<{}> {\r\n root: HTMLTextAreaElement;\r\n\r\n /**\r\n * Tracks the most recently-cached selection start index.\r\n */\r\n private _cachedSelectionStart: number\r\n\r\n /**\r\n * Tracks the most recently processed, extended-string-based selection start index.\r\n * When the element's selectionStart value changes, this should be invalidated.\r\n */\r\n private processedSelectionStart: number;\r\n\r\n /**\r\n * Tracks the most recently processed, extended-string-based selection end index.\r\n * When the element's selectionEnd value changes, this should be invalidated.\r\n */\r\n private processedSelectionEnd: number;\r\n\r\n /**\r\n * Set, then unset within the `forceScroll` method in order to facilitate the\r\n * `isForcingScroll` flag.\r\n */\r\n private _activeForcedScroll: boolean;\r\n\r\n constructor(ele: HTMLTextAreaElement) {\r\n super();\r\n\r\n this.root = ele;\r\n this._cachedSelectionStart = -1;\r\n }\r\n\r\n get isSynthetic(): boolean {\r\n return false;\r\n }\r\n\r\n getElement(): HTMLTextAreaElement {\r\n return this.root;\r\n }\r\n\r\n clearSelection(): void {\r\n // Processes our codepoint-based variants of selectionStart and selectionEnd.\r\n this.getCaret(); // updates processedSelectionStart if required\r\n this.root.value = this.root.value._kmwSubstring(0, this.processedSelectionStart) + this.root.value._kmwSubstring(this.processedSelectionEnd); //I3319\r\n\r\n this.setCaret(this.processedSelectionStart);\r\n }\r\n\r\n isSelectionEmpty(): boolean {\r\n return this.root.selectionStart == this.root.selectionEnd;\r\n }\r\n\r\n hasSelection(): boolean {\r\n return true;\r\n }\r\n\r\n invalidateSelection() {\r\n // Since .selectionStart will never return this value, we use it to indicate\r\n // the need to refresh our processed indices.\r\n this._cachedSelectionStart = -1;\r\n }\r\n\r\n getCaret(): number {\r\n if(this.root.selectionStart != this._cachedSelectionStart) {\r\n this._cachedSelectionStart = this.root.selectionStart; // KMW-1\r\n this.processedSelectionStart = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionStart); // I3319\r\n this.processedSelectionEnd = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionEnd); // I3319\r\n }\r\n return this.root.selectionDirection == 'forward' ? this.processedSelectionEnd : this.processedSelectionStart;\r\n }\r\n\r\n getDeadkeyCaret(): number {\r\n return this.getCaret();\r\n }\r\n\r\n setCaret(caret: number) {\r\n this.setSelection(caret, caret, \"none\");\r\n }\r\n\r\n setSelection(start: number, end: number, direction: \"forward\" | \"backward\" | \"none\") {\r\n let domStart = this.root.value._kmwCodePointToCodeUnit(start);\r\n let domEnd = this.root.value._kmwCodePointToCodeUnit(end);\r\n this.root.setSelectionRange(domStart, domEnd, direction);\r\n\r\n this.processedSelectionStart = start;\r\n this.processedSelectionEnd = end;\r\n\r\n this.forceScroll();\r\n\r\n this.root.setSelectionRange(domStart, domEnd, direction);\r\n }\r\n\r\n forceScroll() {\r\n // Only executes when com.keyman.DOMEventHandlers is defined.\r\n //\r\n // We bypass this whenever operating in the embedded format.\r\n const element = this.getElement();\r\n\r\n let selectionStart = element.selectionStart;\r\n let selectionEnd = element.selectionEnd;\r\n\r\n this._activeForcedScroll = true;\r\n\r\n try {\r\n //Forces scrolling; the re-focus triggers the scroll, at least.\r\n element.blur();\r\n element.focus();\r\n } finally {\r\n // On Edge, it appears that the blur/focus combination will reset the caret position\r\n // under certain scenarios during unit tests. So, we re-set it afterward.\r\n element.selectionStart = selectionStart;\r\n element.selectionEnd = selectionEnd;\r\n this._activeForcedScroll = false;\r\n }\r\n }\r\n\r\n isForcingScroll(): boolean {\r\n return this._activeForcedScroll;\r\n }\r\n\r\n getSelectionDirection(): \"forward\" | \"backward\" | \"none\" {\r\n return this.root.selectionDirection;\r\n }\r\n\r\n getTextBeforeCaret(): string {\r\n this.getCaret();\r\n return this.getText()._kmwSubstring(0, this.processedSelectionStart);\r\n }\r\n\r\n setTextBeforeCaret(text: string) {\r\n this.getCaret();\r\n let selectionLength = this.processedSelectionEnd - this.processedSelectionStart;\r\n let direction = this.getSelectionDirection();\r\n let newCaret = text._kmwLength();\r\n this.root.value = text + this.getText()._kmwSubstring(this.processedSelectionStart);\r\n\r\n this.setSelection(newCaret, newCaret + selectionLength, direction);\r\n }\r\n\r\n protected setTextAfterCaret(s: string) {\r\n let direction = this.getSelectionDirection();\r\n\r\n this.root.value = this.getTextBeforeCaret() + s;\r\n this.setSelection(this.processedSelectionStart, this.processedSelectionEnd, direction);\r\n }\r\n\r\n getTextAfterCaret(): string {\r\n this.getCaret();\r\n return this.getText()._kmwSubstring(this.processedSelectionEnd);\r\n }\r\n\r\n getSelectedText(): string {\r\n this.getCaret();\r\n return this.getText()._kmwSubstring(this.processedSelectionStart, this.processedSelectionEnd);\r\n }\r\n\r\n getText(): string {\r\n return this.root.value;\r\n }\r\n\r\n deleteCharsBeforeCaret(dn: number) {\r\n if(dn > 0) {\r\n let curText = this.getTextBeforeCaret();\r\n let caret = this.processedSelectionStart;\r\n\r\n if(dn > caret) {\r\n dn = caret;\r\n }\r\n\r\n this.adjustDeadkeys(-dn);\r\n this.setTextBeforeCaret(curText.kmwSubstring(0, caret - dn));\r\n this.setCaret(caret - dn);\r\n }\r\n }\r\n\r\n insertTextBeforeCaret(s: string) {\r\n if(!s) {\r\n return;\r\n }\r\n\r\n let caret = this.getCaret();\r\n let front = this.getTextBeforeCaret();\r\n let back = this.getText()._kmwSubstring(this.processedSelectionStart);\r\n\r\n this.adjustDeadkeys(s._kmwLength());\r\n this.root.value = front + s + back;\r\n this.setCaret(caret + s._kmwLength());\r\n }\r\n\r\n handleNewlineAtCaret(): void {\r\n this.insertTextBeforeCaret('\\n');\r\n }\r\n\r\n doInputEvent() {\r\n this.dispatchInputEventOn(this.root);\r\n }\r\n}", + "import OutputTarget from './outputTarget.js';\r\n\r\nclass SelectionCaret {\r\n node: Node;\r\n offset: number;\r\n\r\n constructor(node: Node, offset: number) {\r\n this.node = node;\r\n this.offset = offset;\r\n }\r\n}\r\n\r\nclass SelectionRange {\r\n start: SelectionCaret;\r\n end: SelectionCaret;\r\n\r\n constructor(start: SelectionCaret, end: SelectionCaret) {\r\n this.start = start;\r\n this.end = end;\r\n }\r\n}\r\n\r\nclass StyleCommand {\r\n cmd: string;\r\n stateType: number;\r\n cache: string|boolean;\r\n\r\n constructor(c: string, s:number) {\r\n this.cmd = c;\r\n this.stateType = s;\r\n }\r\n}\r\n\r\nexport default class DesignIFrame extends OutputTarget<{}> {\r\n root: HTMLIFrameElement;\r\n doc: Document;\r\n docRoot: HTMLElement;\r\n\r\n commandCache: StyleCommand[];\r\n\r\n constructor(ele: HTMLIFrameElement) {\r\n super();\r\n this.root = ele;\r\n\r\n if(ele.contentWindow && ele.contentWindow.document && ele.contentWindow.document.designMode == 'on') {\r\n this.doc = ele.contentWindow.document;\r\n this.docRoot = ele.contentWindow.document.documentElement;\r\n } else {\r\n throw \"Specified IFrame is not in design-mode!\";\r\n }\r\n }\r\n\r\n get isSynthetic(): boolean {\r\n return false;\r\n }\r\n\r\n getElement(): HTMLIFrameElement {\r\n return this.root;\r\n }\r\n\r\n focus(): void {\r\n this.doc.defaultView.focus(); // I3363 (Build 301)\r\n }\r\n\r\n isSelectionEmpty(): boolean {\r\n if(!this.hasSelection()) {\r\n return true;\r\n }\r\n\r\n return this.doc.getSelection().isCollapsed;\r\n }\r\n\r\n hasSelection(): boolean {\r\n let Lsel = this.doc.getSelection();\r\n let outerSel = document.getSelection();\r\n\r\n // If the outer doc's selection matches, we're active.\r\n if(outerSel.anchorNode == Lsel.anchorNode && outerSel.focusNode == Lsel.focusNode) {\r\n return true;\r\n } else {\r\n // Problem: for testing, we can't enforce the ideal (ie: first) condition.\r\n // Technically, the IFrame _will_ always have its own internal selection, though... so... it kinda works?\r\n return true;\r\n }\r\n }\r\n\r\n clearSelection(): void {\r\n if(this.hasSelection()) {\r\n let Lsel = this.doc.getSelection();\r\n\r\n if(!Lsel.isCollapsed) {\r\n Lsel.deleteFromDocument(); // I2134, I2192\r\n }\r\n } else {\r\n console.warn(\"Attempted to clear an unowned Selection!\");\r\n }\r\n }\r\n\r\n invalidateSelection(): void { /* No cache maintenance needed here, partly because\r\n * it's impossible to cache a Selection; it mutates.\r\n */ }\r\n\r\n getCarets(): SelectionRange {\r\n let Lsel = this.doc.getSelection();\r\n let code = Lsel.anchorNode.compareDocumentPosition(Lsel.focusNode);\r\n\r\n if(Lsel.isCollapsed) {\r\n let caret = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);\r\n return new SelectionRange(caret, caret);\r\n } else {\r\n let anchor = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);\r\n let focus = new SelectionCaret(Lsel.focusNode, Lsel.focusOffset);\r\n\r\n if(anchor.node == focus.node) {\r\n code = (focus.offset - anchor.offset > 0) ? 2 : 4;\r\n }\r\n\r\n if(code & 2) {\r\n return new SelectionRange(anchor, focus);\r\n } else { // Default\r\n // can test against code & 4 to ensure Focus is before anchor, though.\r\n return new SelectionRange(focus, anchor);\r\n }\r\n }\r\n }\r\n\r\n getDeadkeyCaret(): number {\r\n return this.getTextBeforeCaret().kmwLength();\r\n }\r\n\r\n getTextBeforeCaret(): string {\r\n if(!this.hasSelection()) {\r\n return this.getText();\r\n }\r\n\r\n let caret = this.getCarets().start;\r\n\r\n if(caret.node.nodeType != 3) {\r\n return ''; // Must be a text node to provide a context.\r\n }\r\n\r\n return caret.node.textContent.substr(0, caret.offset);\r\n }\r\n\r\n getSelectedText(): string {\r\n // TODO: figure out the proper implementation.\r\n // KMW 16 and before behavior may be maintained by just returning the empty string.\r\n return '';\r\n }\r\n\r\n getTextAfterCaret(): string {\r\n if(!this.hasSelection()) {\r\n return '';\r\n }\r\n\r\n let caret = this.getCarets().end;\r\n\r\n if(caret.node.nodeType != 3) {\r\n return ''; // Must be a text node to provide a context.\r\n }\r\n\r\n return caret.node.textContent.substr(caret.offset);\r\n }\r\n\r\n getText(): string {\r\n return this.docRoot.innerText;\r\n }\r\n\r\n deleteCharsBeforeCaret(dn: number) {\r\n if(!this.hasSelection() || dn <= 0) {\r\n return;\r\n }\r\n\r\n let start = this.getCarets().start;\r\n\r\n // Bounds-check on the number of chars to delete.\r\n if(dn > start.offset) {\r\n dn = start.offset;\r\n }\r\n\r\n if(start.node.nodeType != 3) {\r\n console.warn(\"Deletion of characters requested without available context!\");\r\n return; // No context to delete characters from.\r\n }\r\n\r\n let range = this.doc.createRange();\r\n let dnOffset = start.offset - start.node.nodeValue.substr(0, start.offset)._kmwSubstr(-dn).length;\r\n\r\n range.setStart(start.node, dnOffset);\r\n range.setEnd(start.node, start.offset);\r\n\r\n this.adjustDeadkeys(-dn);\r\n range.deleteContents();\r\n // No need to reposition the caret - the DOM will auto-move the selection accordingly, since\r\n // we didn't use the selection to delete anything.\r\n }\r\n\r\n insertTextBeforeCaret(s: string) {\r\n if(!this.hasSelection()) {\r\n return;\r\n }\r\n\r\n let start = this.getCarets().start;\r\n let delta = s._kmwLength();\r\n let Lsel = this.doc.getSelection();\r\n\r\n if(delta == 0) {\r\n return;\r\n }\r\n\r\n this.adjustDeadkeys(delta);\r\n\r\n // While Selection.extend() was really nice for this, IE didn't support it whatsoever.\r\n // However, IE (11, at least) DID support setting selections via ranges, so we were still\r\n // able to manage the caret properly.\r\n //\r\n // TODO: double-check that it was only IE-motivated, re-implement with Selection.extend().\r\n let finalCaret = this.root.ownerDocument.createRange();\r\n\r\n if(start.node.nodeType == 3) {\r\n let textStart = start.node;\r\n textStart.insertData(start.offset, s);\r\n finalCaret.setStart(textStart, start.offset + s.length);\r\n } else {\r\n // Create a new text node - empty control\r\n var n = this.doc.createTextNode(s);\r\n\r\n let range = this.doc.createRange();\r\n range.setStart(start.node, start.offset);\r\n range.collapse(true);\r\n range.insertNode(n);\r\n finalCaret.setStart(n, s.length);\r\n }\r\n\r\n finalCaret.collapse(true);\r\n Lsel.removeAllRanges();\r\n try {\r\n Lsel.addRange(finalCaret);\r\n } catch(e) {\r\n // Chrome (through 4.0 at least) throws an exception because it has not synchronised its content with the selection.\r\n // scrollIntoView synchronises the content for selection\r\n start.node.parentElement.scrollIntoView();\r\n Lsel.addRange(finalCaret);\r\n }\r\n Lsel.collapseToEnd();\r\n }\r\n\r\n handleNewlineAtCaret(): void {\r\n // TODO: Implement.\r\n //\r\n // As it turns out, we never had an implementation for handling newline inputs from the OSK for this element type.\r\n // At least this way, it's more explicit.\r\n //\r\n // Note: consult \"// Create a new text node - empty control\" case in insertTextBeforeCaret -\r\n // this helps to handle the browser-default implementation of newline handling. In particular,\r\n // entry of the first character after a newline.\r\n //\r\n // If raw newlines are entered into the HTML, but as with usual HTML, they're interpreted as excess whitespace and\r\n // have no effect. We need to add DOM elements for a functional newline.\r\n }\r\n\r\n protected setTextAfterCaret(s: string) {\r\n if(!this.hasSelection()) {\r\n return;\r\n }\r\n\r\n let caret = this.getCarets().end;\r\n let delta = s._kmwLength();\r\n\r\n if(delta == 0) {\r\n return;\r\n }\r\n\r\n // This is designed explicitly for use in direct-setting operations; deadkeys\r\n // will be handled after this method.\r\n\r\n if(caret.node.nodeType == 3) {\r\n let textStart = caret.node;\r\n textStart.replaceData(caret.offset, textStart.length, s);\r\n } else {\r\n // Create a new text node - empty control\r\n var n = caret.node.ownerDocument.createTextNode(s);\r\n\r\n let range = this.root.ownerDocument.createRange();\r\n range.setStart(caret.node, caret.offset);\r\n range.collapse(true);\r\n range.insertNode(n);\r\n }\r\n }\r\n\r\n /**\r\n * Function saveProperties\r\n * Scope Private\r\n * Description Build and create list of styles that can be applied in iframes\r\n */\r\n saveProperties() {\r\n // Formerly _CacheCommands.\r\n var _CacheableCommands=[\r\n new StyleCommand('backcolor',1), new StyleCommand('fontname',1), new StyleCommand('fontsize',1),\r\n new StyleCommand('forecolor',1), new StyleCommand('bold',0), new StyleCommand('italic',0),\r\n new StyleCommand('strikethrough',0), new StyleCommand('subscript',0),\r\n new StyleCommand('superscript',0), new StyleCommand('underline',0)\r\n ];\r\n\r\n if(this.doc.defaultView) {\r\n _CacheableCommands.push(new StyleCommand('hilitecolor',1));\r\n }\r\n\r\n for(var n=0; n < _CacheableCommands.length; n++) { // I1511 - array prototype extended\r\n let cmd = _CacheableCommands[n];\r\n //KeymanWeb._Debug('Command:'+_CacheableCommands[n][0]);\r\n if(cmd.stateType == 1) {\r\n cmd.cache = this.doc.queryCommandValue(cmd.cmd);\r\n } else {\r\n cmd.cache = this.doc.queryCommandState(cmd.cmd);\r\n }\r\n }\r\n this.commandCache = _CacheableCommands;\r\n }\r\n\r\n /**\r\n * Function restoreProperties\r\n * Scope Private\r\n * Description Restore styles in IFRAMEs (??)\r\n */\r\n restoreProperties(_func?: () => void): void {\r\n // Formerly _CacheCommandsReset.\r\n if(!this.commandCache) {\r\n console.error(\"No command cache exists to restore!\");\r\n }\r\n\r\n for(var n=0; n < this.commandCache.length; n++) { // I1511 - array prototype extended\r\n let cmd = this.commandCache[n];\r\n\r\n //KeymanWeb._Debug('ResetCacheCommand:'+_CacheableCommands[n][0]+'='+_CacheableCommands[n][2]);\r\n if(cmd.stateType == 1) {\r\n if(this.doc.queryCommandValue(cmd.cmd) != cmd.cache) {\r\n if(_func) {\r\n _func();\r\n }\r\n this.doc.execCommand(cmd.cmd, false, cmd.cache);\r\n }\r\n } else if(this.doc.queryCommandState(cmd.cmd) != cmd.cache) {\r\n if(_func) {\r\n _func();\r\n }\r\n //KeymanWeb._Debug('executing command '+_CacheableCommand[n][0]);\r\n this.doc.execCommand(cmd.cmd, false, null);\r\n }\r\n }\r\n }\r\n\r\n doInputEvent() {\r\n // Root = the iframe, the outermost component and the one we were originally told to attach to.\r\n this.dispatchInputEventOn(this.root);\r\n }\r\n}", + "import OutputTarget from './outputTarget.js';\r\n\r\nclass SelectionCaret {\r\n node: Node;\r\n offset: number;\r\n\r\n constructor(node: Node, offset: number) {\r\n this.node = node;\r\n this.offset = offset;\r\n }\r\n}\r\n\r\nclass SelectionRange {\r\n start: SelectionCaret;\r\n end: SelectionCaret;\r\n\r\n constructor(start: SelectionCaret, end: SelectionCaret) {\r\n this.start = start;\r\n this.end = end;\r\n }\r\n}\r\n\r\nexport default class ContentEditable extends OutputTarget<{}> {\r\n root: HTMLElement;\r\n\r\n constructor(ele: HTMLElement) {\r\n if(ele.isContentEditable) {\r\n super();\r\n this.root = ele;\r\n } else {\r\n throw \"Specified element is not already content-editable!\";\r\n }\r\n }\r\n\r\n get isSynthetic(): boolean {\r\n return false;\r\n }\r\n\r\n getElement(): HTMLElement {\r\n return this.root;\r\n }\r\n\r\n isSelectionEmpty(): boolean {\r\n if(!this.hasSelection()) {\r\n return true;\r\n }\r\n\r\n return this.root.ownerDocument.getSelection().isCollapsed;\r\n }\r\n\r\n hasSelection(): boolean {\r\n let Lsel = this.root.ownerDocument.getSelection();\r\n\r\n if(this.root != Lsel.anchorNode && !this.root.contains(Lsel.anchorNode)) {\r\n return false;\r\n }\r\n\r\n if(this.root != Lsel.focusNode && !this.root.contains(Lsel.focusNode)) {\r\n return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n clearSelection(): void {\r\n if(this.hasSelection()) {\r\n let Lsel = this.root.ownerDocument.getSelection();\r\n\r\n if(!Lsel.isCollapsed) {\r\n Lsel.deleteFromDocument(); // I2134, I2192\r\n }\r\n } else {\r\n console.warn(\"Attempted to clear an unowned Selection!\");\r\n }\r\n }\r\n\r\n invalidateSelection(): void { /* No cache maintenance needed here, partly because\r\n * it's impossible to cache a Selection; it mutates.\r\n */ }\r\n\r\n getCarets(): SelectionRange {\r\n let Lsel = this.root.ownerDocument.getSelection();\r\n let code = Lsel.anchorNode.compareDocumentPosition(Lsel.focusNode);\r\n\r\n if(Lsel.isCollapsed) {\r\n let caret = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);\r\n return new SelectionRange(caret, caret);\r\n } else {\r\n let anchor = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);\r\n let focus = new SelectionCaret(Lsel.focusNode, Lsel.focusOffset);\r\n\r\n if(anchor.node == focus.node) {\r\n code = (focus.offset - anchor.offset > 0) ? 2 : 4;\r\n }\r\n\r\n if(code & 2) {\r\n return new SelectionRange(anchor, focus);\r\n } else { // Default\r\n // can test against code & 4 to ensure Focus is before anchor, though.\r\n return new SelectionRange(focus, anchor);\r\n }\r\n }\r\n }\r\n\r\n getDeadkeyCaret(): number {\r\n return this.getTextBeforeCaret().kmwLength();\r\n }\r\n\r\n getTextBeforeCaret(): string {\r\n if(!this.hasSelection()) {\r\n return this.getText();\r\n }\r\n\r\n let caret = this.getCarets().start;\r\n\r\n if(caret.node.nodeType != 3) {\r\n return ''; // Must be a text node to provide a context.\r\n }\r\n\r\n return caret.node.textContent.substr(0, caret.offset);\r\n }\r\n\r\n getSelectedText(): string {\r\n // TODO: figure out the proper implementation.\r\n // KMW 16 and before behavior may be maintained by just returning the empty string.\r\n return '';\r\n }\r\n\r\n getTextAfterCaret(): string {\r\n if(!this.hasSelection()) {\r\n return '';\r\n }\r\n\r\n let caret = this.getCarets().end;\r\n\r\n if(caret.node.nodeType != 3) {\r\n return ''; // Must be a text node to provide a context.\r\n }\r\n\r\n return caret.node.textContent.substr(caret.offset);\r\n }\r\n\r\n getText(): string {\r\n return this.root.innerText;\r\n }\r\n\r\n deleteCharsBeforeCaret(dn: number) {\r\n if(!this.hasSelection() || dn <= 0) {\r\n return;\r\n }\r\n\r\n let start = this.getCarets().start;\r\n\r\n // Bounds-check on the number of chars to delete.\r\n if(dn > start.offset) {\r\n dn = start.offset;\r\n }\r\n\r\n if(start.node.nodeType != 3) {\r\n console.warn(\"Deletion of characters requested without available context!\");\r\n return; // No context to delete characters from.\r\n }\r\n\r\n let range = this.root.ownerDocument.createRange();\r\n let dnOffset = start.offset - start.node.nodeValue.substr(0, start.offset)._kmwSubstr(-dn).length;\r\n\r\n range.setStart(start.node, dnOffset);\r\n range.setEnd(start.node, start.offset);\r\n\r\n this.adjustDeadkeys(-dn);\r\n range.deleteContents();\r\n // No need to reposition the caret - the DOM will auto-move the selection accordingly, since\r\n // we didn't use the selection to delete anything.\r\n }\r\n\r\n insertTextBeforeCaret(s: string) {\r\n if(!this.hasSelection()) {\r\n return;\r\n }\r\n\r\n let start = this.getCarets().start;\r\n let delta = s._kmwLength();\r\n let Lsel = this.root.ownerDocument.getSelection();\r\n\r\n if(delta == 0) {\r\n return;\r\n }\r\n\r\n this.adjustDeadkeys(delta);\r\n\r\n // While Selection.extend() was really nice for this, IE didn't support it whatsoever.\r\n // However, IE (11, at least) DID support setting selections via ranges, so we were still\r\n // able to manage the caret properly.\r\n //\r\n // TODO: double-check that it was only IE-motivated, re-implement with Selection.extend().\r\n let finalCaret = this.root.ownerDocument.createRange();\r\n\r\n if(start.node.nodeType == 3) {\r\n let textStart = start.node;\r\n textStart.insertData(start.offset, s);\r\n finalCaret.setStart(textStart, start.offset + s.length);\r\n } else {\r\n // Create a new text node - empty control\r\n var n = start.node.ownerDocument.createTextNode(s);\r\n\r\n let range = this.root.ownerDocument.createRange();\r\n range.setStart(start.node, start.offset);\r\n range.collapse(true);\r\n range.insertNode(n);\r\n finalCaret.setStart(n, s.length);\r\n }\r\n\r\n finalCaret.collapse(true);\r\n Lsel.removeAllRanges();\r\n try {\r\n Lsel.addRange(finalCaret);\r\n } catch(e) {\r\n // Chrome (through 4.0 at least) throws an exception because it has not synchronised its content with the selection.\r\n // scrollIntoView synchronises the content for selection\r\n start.node.parentElement.scrollIntoView();\r\n Lsel.addRange(finalCaret);\r\n }\r\n Lsel.collapseToEnd();\r\n }\r\n\r\n handleNewlineAtCaret(): void {\r\n // TODO: Implement.\r\n //\r\n // As it turns out, we never had an implementation for handling newline inputs from the OSK for this element type.\r\n // At least this way, it's more explicit.\r\n //\r\n // Note: consult \"// Create a new text node - empty control\" case in insertTextBeforeCaret -\r\n // this helps to handle the browser-default implementation of newline handling. In particular,\r\n // entry of the first character after a newline.\r\n //\r\n // If raw newlines are entered into the HTML, but as with usual HTML, they're interpreted as excess whitespace and\r\n // have no effect. We need to add DOM elements for a functional newline.\r\n }\r\n\r\n protected setTextAfterCaret(s: string) {\r\n if(!this.hasSelection()) {\r\n return;\r\n }\r\n\r\n let caret = this.getCarets().end;\r\n let delta = s._kmwLength();\r\n\r\n if(delta == 0) {\r\n return;\r\n }\r\n\r\n // This is designed explicitly for use in direct-setting operations; deadkeys\r\n // will be handled after this method.\r\n\r\n if(caret.node.nodeType == 3) {\r\n let textStart = caret.node;\r\n textStart.replaceData(caret.offset, textStart.length, s);\r\n } else {\r\n // Create a new text node - empty control\r\n var n = caret.node.ownerDocument.createTextNode(s);\r\n\r\n let range = this.root.ownerDocument.createRange();\r\n range.setStart(caret.node, caret.offset);\r\n range.collapse(true);\r\n range.insertNode(n);\r\n }\r\n }\r\n\r\n doInputEvent() {\r\n this.dispatchInputEventOn(this.root);\r\n }\r\n}", + "/**\r\n * Checks the type of an input DOM-related object while ensuring that it is checked against the correct prototype,\r\n * as class prototypes are (by specification) scoped upon the owning Window.\r\n *\r\n * See https://stackoverflow.com/questions/43587286/why-does-instanceof-return-false-on-chrome-safari-and-edge-and-true-on-firefox\r\n * for more details.\r\n *\r\n * @param {EventTarget} Pelem An element of the web page or one of its IFrame-based subdocuments.\r\n * @param {string} className The plain-text name of the expected Element type.\r\n * @return {boolean}\r\n */\r\nexport function nestedInstanceOf(Pelem: EventTarget, className: string): boolean {\r\n var scopedClass;\r\n\r\n if(!Pelem) {\r\n // If we're bothering to check something's type, null references don't match\r\n // what we're looking for.\r\n return false;\r\n }\r\n // @ts-ignore\r\n if (Pelem['Window']) { // Window objects contain the class definitions for types held within them. So, we can check for those.\r\n return className == 'Window';\r\n // @ts-ignore\r\n } else if (Pelem['defaultView']) { // Covers Document.\r\n // @ts-ignore\r\n scopedClass = (Pelem as Document)['defaultView'][className];\r\n // @ts-ignore\r\n } else if(Pelem['ownerDocument']) {\r\n // @ts-ignore\r\n scopedClass = (Pelem as Node).ownerDocument.defaultView[className];\r\n }\r\n\r\n if(scopedClass) {\r\n return Pelem instanceof scopedClass;\r\n } else {\r\n return false;\r\n }\r\n}", + "import type OutputTarget from './outputTarget.js';\r\nimport Input from './input.js';\r\nimport TextArea from './textarea.js';\r\nimport DesignIFrame from './designIFrame.js';\r\nimport ContentEditable from './contentEditable.js';\r\nimport { nestedInstanceOf } from './utils.js';\r\n\r\nexport default function wrapElement(e: HTMLElement): OutputTarget {\r\n // Complex type scoping is implemented here so that kmwutils.ts is not a dependency for test compilations.\r\n\r\n if(nestedInstanceOf(e, \"HTMLInputElement\")) {\r\n return new Input( e);\r\n } else if(nestedInstanceOf(e, \"HTMLTextAreaElement\")) {\r\n return new TextArea( e);\r\n } else if(nestedInstanceOf(e, \"HTMLIFrameElement\")) {\r\n let iframe = e;\r\n\r\n if(iframe.contentWindow && iframe.contentWindow.document && iframe.contentWindow.document.designMode == \"on\") {\r\n return new DesignIFrame(iframe);\r\n } else if (e.isContentEditable) {\r\n // Do content-editable