From 89e5ff6a31324aed192ca6106ade5d49a33c2b74 Mon Sep 17 00:00:00 2001 From: Keyman Build Server Date: Mon, 4 Mar 2024 13:21:37 -0500 Subject: [PATCH] auto: KeymanWeb release 18.0.8 --- kmw/engine/18.0.8/keymanweb.es5.js | 2 + kmw/engine/18.0.8/keymanweb.es5.js.map | 4551 +++++++++++++++++ kmw/engine/18.0.8/keymanweb.js | 26 + kmw/engine/18.0.8/keymanweb.js.map | 4282 ++++++++++++++++ kmw/engine/18.0.8/kmwuibutton.js | 2 + kmw/engine/18.0.8/kmwuibutton.js.map | 75 + kmw/engine/18.0.8/kmwuifloat.js | 2 + kmw/engine/18.0.8/kmwuifloat.js.map | 48 + kmw/engine/18.0.8/kmwuitoggle.js | 2 + kmw/engine/18.0.8/kmwuitoggle.js.map | 69 + kmw/engine/18.0.8/kmwuitoolbar.js | 2 + kmw/engine/18.0.8/kmwuitoolbar.js.map | 122 + kmw/engine/18.0.8/osk/ajax-loader.gif | Bin 0 -> 10819 bytes kmw/engine/18.0.8/osk/globe-hint.css | 58 + kmw/engine/18.0.8/osk/keymanweb-osk.ttf | Bin 0 -> 35164 bytes kmw/engine/18.0.8/osk/kmwosk.css | 804 +++ kmw/engine/18.0.8/ui/button/kbdicon.gif | Bin 0 -> 1036 bytes kmw/engine/18.0.8/ui/button/kmw_button.gif | Bin 0 -> 1093 bytes kmw/engine/18.0.8/ui/float/kbdicon.gif | Bin 0 -> 1036 bytes kmw/engine/18.0.8/ui/float/kmicon.gif | Bin 0 -> 976 bytes kmw/engine/18.0.8/ui/ios/kbdicon.png | Bin 0 -> 180 bytes kmw/engine/18.0.8/ui/toggle/kbdicon.gif | Bin 0 -> 1036 bytes kmw/engine/18.0.8/ui/toggle/kmw_logo_16.gif | Bin 0 -> 1077 bytes .../18.0.8/ui/toggle/kmw_logo_16_down.gif | Bin 0 -> 1105 bytes kmw/engine/18.0.8/ui/toggle/kmw_osk_16.gif | Bin 0 -> 932 bytes .../18.0.8/ui/toggle/kmwcontroller2.gif | Bin 0 -> 1053 bytes .../18.0.8/ui/toggle/kmwcontroller2x.gif | Bin 0 -> 1074 bytes kmw/engine/18.0.8/ui/toolbar/blank.gif | Bin 0 -> 807 bytes kmw/engine/18.0.8/ui/toolbar/kmwuitoolbar.css | 521 ++ kmw/engine/18.0.8/ui/toolbar/toolbar.gif | Bin 0 -> 3250 bytes kmw/engine/18.0.8/ui/toolbar/world.png | Bin 0 -> 277714 bytes 31 files changed, 10566 insertions(+) create mode 100644 kmw/engine/18.0.8/keymanweb.es5.js create mode 100644 kmw/engine/18.0.8/keymanweb.es5.js.map create mode 100644 kmw/engine/18.0.8/keymanweb.js create mode 100644 kmw/engine/18.0.8/keymanweb.js.map create mode 100644 kmw/engine/18.0.8/kmwuibutton.js create mode 100644 kmw/engine/18.0.8/kmwuibutton.js.map create mode 100644 kmw/engine/18.0.8/kmwuifloat.js create mode 100644 kmw/engine/18.0.8/kmwuifloat.js.map create mode 100644 kmw/engine/18.0.8/kmwuitoggle.js create mode 100644 kmw/engine/18.0.8/kmwuitoggle.js.map create mode 100644 kmw/engine/18.0.8/kmwuitoolbar.js create mode 100644 kmw/engine/18.0.8/kmwuitoolbar.js.map create mode 100644 kmw/engine/18.0.8/osk/ajax-loader.gif create mode 100644 kmw/engine/18.0.8/osk/globe-hint.css create mode 100644 kmw/engine/18.0.8/osk/keymanweb-osk.ttf create mode 100644 kmw/engine/18.0.8/osk/kmwosk.css create mode 100644 kmw/engine/18.0.8/ui/button/kbdicon.gif create mode 100644 kmw/engine/18.0.8/ui/button/kmw_button.gif create mode 100644 kmw/engine/18.0.8/ui/float/kbdicon.gif create mode 100644 kmw/engine/18.0.8/ui/float/kmicon.gif create mode 100644 kmw/engine/18.0.8/ui/ios/kbdicon.png create mode 100644 kmw/engine/18.0.8/ui/toggle/kbdicon.gif create mode 100644 kmw/engine/18.0.8/ui/toggle/kmw_logo_16.gif create mode 100644 kmw/engine/18.0.8/ui/toggle/kmw_logo_16_down.gif create mode 100644 kmw/engine/18.0.8/ui/toggle/kmw_osk_16.gif create mode 100644 kmw/engine/18.0.8/ui/toggle/kmwcontroller2.gif create mode 100644 kmw/engine/18.0.8/ui/toggle/kmwcontroller2x.gif create mode 100644 kmw/engine/18.0.8/ui/toolbar/blank.gif create mode 100644 kmw/engine/18.0.8/ui/toolbar/kmwuitoolbar.css create mode 100644 kmw/engine/18.0.8/ui/toolbar/toolbar.gif create mode 100644 kmw/engine/18.0.8/ui/toolbar/world.png diff --git a/kmw/engine/18.0.8/keymanweb.es5.js b/kmw/engine/18.0.8/keymanweb.es5.js new file mode 100644 index 00000000..6d209fba --- /dev/null +++ b/kmw/engine/18.0.8/keymanweb.es5.js @@ -0,0 +1,2 @@ +(function(){var jl=Object.create;var sn=Object.defineProperty;var ql=Object.getOwnPropertyDescriptor;var $l=Object.getOwnPropertyNames;var er=Object.getPrototypeOf,tr=Object.prototype.hasOwnProperty;var xc=function(i,t){return function(){return t||i((t={exports:{}}).exports,t),t.exports}},pc=function(i,t){for(var e in t)sn(i,e,{get:t[e],enumerable:!0})},rn=function(i,t,e,n){if(t&&typeof t=="object"||typeof t=="function")for(var c=$l(t),l=0,r=c.length,s;l=0;a--)(o=e[a])&&(s=(r<3?o(s):r>3?o(n,c,s):o(n,c))||s);return r>3&&s&&Object.defineProperty(n,c,s),s},Vc=function(e,n,c,l){function r(s){return s instanceof c?s:new c(function(o){o(s)})}return new(c||(c=Promise))(function(s,o){function a(F){try{g(l.next(F))}catch(u){o(u)}}function B(F){try{g(l.throw(F))}catch(u){o(u)}}function g(F){F.done?s(F.value):r(F.value).then(a,B)}g((l=l.apply(e,n||[])).next())})},Zc=function(e,n){var c={label:0,sent:function(){if(s[0]&1)throw s[1];return s[1]},trys:[],ops:[]},l,r,s,o;return o={next:a(0),throw:a(1),return:a(2)},typeof Symbol=="function"&&(o[Symbol.iterator]=function(){return this}),o;function a(g){return function(F){return B([g,F])}}function B(g){if(l)throw new TypeError("Generator is already executing.");for(;o&&(o=0,g[0]&&(c=0)),c;)try{if(l=1,r&&(s=g[0]&2?r.return:g[0]?r.throw||((s=r.return)&&s.call(r),0):r.next)&&!(s=s.call(r,g[1])).done)return s;switch(r=0,s&&(g=[g[0]&2,s.value]),g[0]){case 0:case 1:s=g;break;case 4:return c.label++,{value:g[1],done:!1};case 5:c.label++,r=g[1],g=[0];continue;case 7:g=c.ops.pop(),c.trys.pop();continue;default:if(s=c.trys,!(s=s.length>0&&s[s.length-1])&&(g[0]===6||g[0]===2)){c=0;continue}if(g[0]===3&&(!s||g[1]>s[0]&&g[1]=e.length&&(e=void 0),{value:e&&e[l++],done:!e}}};throw new TypeError(n?"Object is not iterable.":"Symbol.iterator is not defined.")},fc=function(e,n){var c=typeof Symbol=="function"&&e[Symbol.iterator];if(!c)return e;var l=c.call(e),r,s=[],o;try{for(;(n===void 0||n-- >0)&&!(r=l.next()).done;)s.push(r.value)}catch(a){o={error:a}}finally{try{r&&!r.done&&(c=l.return)&&c.call(l)}finally{if(o)throw o.error}}return s},ft=function(e){return this instanceof ft?(this.v=e,this):new ft(e)},i("__extends",Gc),i("__assign",mc),i("__decorate",Xc),i("__awaiter",Vc),i("__generator",Zc),i("__values",vc),i("__read",fc),i("__await",ft)})});var T=xc(function(Ro,Bn){"use strict";var cr=Object.prototype.hasOwnProperty,j="~";function rt(){}Object.create&&(rt.prototype=Object.create(null),new rt().__proto__||(j=!1));function ir(i,t,e){this.fn=i,this.context=t,this.once=e||!1}function Lc(i,t,e,n,c){if(typeof e!="function")throw new TypeError("The listener must be a function");var l=new ir(e,n||i,c),r=j?j+t:t;return i._events[r]?i._events[r].fn?i._events[r]=[i._events[r],l]:i._events[r].push(l):(i._events[r]=l,i._eventsCount++),i}function Jt(i,t){--i._eventsCount===0?i._events=new rt:delete i._events[t]}function z(){this._events=new rt,this._eventsCount=0}z.prototype.eventNames=function(){var t=[],e,n;if(this._eventsCount===0)return t;for(n in e=this._events)cr.call(e,n)&&t.push(j?n.slice(1):n);return Object.getOwnPropertySymbols?t.concat(Object.getOwnPropertySymbols(e)):t};z.prototype.listeners=function(t){var e=j?j+t:t,n=this._events[e];if(!n)return[];if(n.fn)return[n.fn];for(var c=0,l=n.length,r=new Array(l);c?~",'{|}"']],isKnownOSKModifierKey:function(i){switch(i){case"K_SHIFT":case"K_LOPT":case"K_ROPT":case"K_NUMLOCK":case"K_CAPS":return!0;default:if(ie.keyCodes[i]>=5e4)return!0;var t=ie[i];if(t>5e4&&t<50011)return!0}return!1},getModifierState:function(i){var t=0;i.indexOf("shift")>=0&&(t|=ie.modifierCodes.SHIFT);var e=!1;i.indexOf("leftctrl")>=0&&(t|=ie.modifierCodes.LCTRL,e=!0),i.indexOf("rightctrl")>=0&&(t|=ie.modifierCodes.RCTRL,e=!0),i.indexOf("ctrl")>=0&&!e&&(t|=ie.modifierCodes.CTRL);var n=!1;return i.indexOf("leftalt")>=0&&(t|=ie.modifierCodes.LALT,n=!0),i.indexOf("rightalt")>=0&&(t|=ie.modifierCodes.RALT,n=!0),i.indexOf("alt")>=0&&!n&&(t|=ie.modifierCodes.ALT),t},getStateFromLayer:function(i){var t=0;return i.indexOf("caps")>=0?t|=ie.modifierCodes.CAPS:t|=ie.modifierCodes.NO_CAPS,t}},b=ie;var Je;(function(i){i.Enter="\n",i.Backspace="\b"})(Je||(Je={}));var lr=function(){function i(){}return i.prototype.codeForEvent=function(t){return b.keyCodes[t.kName]||t.Lcode},i.prototype.forAny=function(t,e,n){var c="";if((c=this.forSpecialEmulation(t))!=null)return c;if(!e&&(c=this.forNumpadKeys(t))!=null)return c;if((c=this.forUnicodeKeynames(t,n))!=null)return c;if((c=this.forBaseKeys(t,n))!=null)return c;var l=this.codeForEvent(t);switch(l){default:return null}},i.prototype.isCommand=function(t){var e=this.codeForEvent(t);switch(e){default:return!1}},i.prototype.applyCommand=function(t,e){},i.prototype.forSpecialEmulation=function(t){var e=this.codeForEvent(t);switch(e){case b.keyCodes.K_BKSP:return Je.Backspace;case b.keyCodes.K_ENTER:return Je.Enter;default:return null}},i.prototype.forNumpadKeys=function(t){if(t.Lcode>=b.keyCodes.K_NP0&&t.Lcode<=b.keyCodes.K_NPSLASH){if(t.Lcode<106)var e=t.Lcode-48;else e=t.Lcode-64;var n=String._kmwFromCharCode(e);return n}else return null},i.prototype.forUnicodeKeynames=function(t,e){var n=t.kName;if(!n||n.substr(0,2)!="U_")return null;for(var c="",l=n.substr(2).split("_"),r=0,s=l;r=b.keyCodes.K_0&&n<=b.keyCodes.K_9)return b.codesUS[c][0][n-b.keyCodes.K_0];if(n>=b.keyCodes.K_A&&n<=b.keyCodes.K_Z)return String.fromCharCode(n+(c?0:32));if(n>=b.keyCodes.K_COLON&&n<=b.keyCodes.K_BKQUOTE)return b.codesUS[c][1][n-b.keyCodes.K_COLON];if(n>=b.keyCodes.K_LBRKT&&n<=b.keyCodes.K_QUOTE)return b.codesUS[c][2][n-b.keyCodes.K_LBRKT];if(n==b.keyCodes.K_oE2)return c?"|":"\\"}catch(l){e&&(e.errorLog="Error detected with default mapping for key: code = "+n+", shift state = "+(c==1?"shift":"default"))}return null},i}(),ke=lr;var rr=new ke,sr=function(){function i(t){this.isSynthetic=!0;for(var e in t)t[e]!==void 0&&(this[e]=t[e])}return i.constructNullKeyEvent=function(t){var e=new i({Lcode:0,kName:"",device:t,Lstates:void 0,Lmodifiers:void 0,vkCode:void 0,LisVirtualKey:void 0});return e},Object.defineProperty(i.prototype,"isModifier",{get:function(){switch(this.Lcode){case 16:case 17:case 18:case 20:case 144:case 145:return!0;default:return!1}},enumerable:!1,configurable:!0}),i.prototype.setMnemonicCode=function(t,e){if(this.Lcode!=b.keyCodes.K_SPACE){var n=new i(this);for(var c in this)n[c]=this[c];n.kName="K_xxxx",n.Lmodifiers=t?16:0;var l=rr.forAny(n,!0);this.vkCode=this.Lcode,l?this.Lcode=l.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)},i}(),oe=sr;var Ye=function(){function i(){}return i}(),or=function(){function i(){this.FF=new Ye,this.Safari=new Ye,this.Opera=new Ye,this.FF.k61=187,this.FF.k59=186,this.FF.k173=189}return i}(),ar=function(){function i(){this.se=new Ye,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 Ye,this.uk.k223=192,this.uk.k192=222,this.uk.k222=226,this.uk.k220=220}return i}(),Br=function(){function i(){}return i._usCodeInit=function(){var t=new Ye,e=new Ye;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,i._usCharCodes=[t,e]},i._USKeyCodeToCharCode=function(t){return i.usCharCodes[t.Lmodifiers&16?1:0]["k"+t.Lcode]},Object.defineProperty(i,"usCharCodes",{get:function(){return i._usCharCodes||i._usCodeInit(),i._usCharCodes},enumerable:!1,configurable:!0}),i.browserMap=new or,i.languageMap=new ar,i}(),ae=Br;function te(i,t){var e=t||{};for(var n in i)typeof i[n]=="object"&&i[n]!=null?(e[n]=i[n].constructor===Array?[]:{},te(i[n],e[n])):e[n]=i[n];return e}var st=function(){function i(t,e,n,c){switch(t.toLowerCase()){case i.Browser.Chrome:case i.Browser.Edge:case i.Browser.Firefox:case i.Browser.Native:case i.Browser.Opera:case i.Browser.Safari:this.browser=t.toLowerCase();break;default:this.browser=i.Browser.Other}switch(e.toLowerCase()){case i.FormFactor.Desktop:case i.FormFactor.Phone:case i.FormFactor.Tablet:this.formFactor=e.toLowerCase();break;default:throw"Invalid form factor specified for device: "+e}switch(n.toLowerCase()){case i.OperatingSystem.Windows.toLowerCase():case i.OperatingSystem.macOS.toLowerCase():case i.OperatingSystem.Linux.toLowerCase():case i.OperatingSystem.Android.toLowerCase():case i.OperatingSystem.iOS.toLowerCase():this.OS=n.toLowerCase();break;default:this.OS=i.OperatingSystem.Other}this.touchable=c}return i}();(function(i){var t;(function(c){c.Chrome="chrome",c.Edge="edge",c.Firefox="firefox",c.Native="native",c.Opera="opera",c.Safari="safari",c.Other="other"})(t=i.Browser||(i.Browser={}));var e;(function(c){c.Windows="windows",c.macOS="macosx",c.Linux="linux",c.Android="android",c.iOS="ios",c.Other="other"})(e=i.OperatingSystem||(i.OperatingSystem={}));var n;(function(c){c.Desktop="desktop",c.Phone="phone",c.Tablet="tablet"})(n=i.FormFactor||(i.FormFactor={}))})(st||(st={}));function dn(i){return new st(i.browser,st.FormFactor.Desktop,i.OS,!1)}var V=st;var gn=function(){function i(){}return i.VERSION="18.0.8",i.VERSION_RELEASE="18.0",i.VERSION_MAJOR="18",i.VERSION_MINOR="0",i.VERSION_PATCH="8",i.TIER="alpha",i.VERSION_TAG="-alpha",i.VERSION_WITH_TAG="18.0.8-alpha",i.VERSION_ENVIRONMENT="alpha",i.VERSION_GIT_TAG="release@18.0.8-alpha",i}();var ot=gn;var dr=function(){function i(t){if(t==null){this.components=[].concat(i.DEVELOPER_VERSION_FALLBACK.components);return}if(Array.isArray(t)){var e=t;if(e.length<2)throw new Error("Version string must have at least a major and minor component!");this.components=[].concat(e);return}var n=t.split("."),c=[];if(n.length<2)throw new Error("Version string must have at least a major and minor component!");for(var l=0;l0)return e?-1:1;c++}while(c1114111||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(i){var t=String(this),e=0;if(i<0||i>=t.length)return NaN;for(var n=0;n=55296&&c<=56319&&t.length>e+1){var l=t.charCodeAt(e+1);if(l>=56320&&l<=57343)return(c-55296<<10)+(l-56320)+65536}return c},String.prototype.kmwIndexOf=function(i,t){var e=String(this),n=e.indexOf(i,t);if(n<0)return n;for(var c=0,l=0;l!==null&&lt){var l=i;i=t,t=l}n=e.kmwCodePointToCodeUnit(i),c=e.kmwCodePointToCodeUnit(t)}return(isNaN(n)||n===null)&&(n=0),(isNaN(c)||c===null)&&(c=e.length),e.substring(n,c)},String.prototype.kmwNextChar=function(i){var t=String(this);if(i===null||i<0||i>=t.length-1)return null;var e=t.charCodeAt(i);if(e>=55296&&e<=56319&&t.length>i+1){var n=t.charCodeAt(i+1);if(n>=56320&&n<=57343)return i==t.length-2?null:i+2}return i+1},String.prototype.kmwPrevChar=function(i){var t=String(this);if(i==null||i<=0||i>t.length)return null;var e=t.charCodeAt(i-1);if(e>=56320&&e<=57343&&i>1){var n=t.charCodeAt(i-2);if(n>=55296&&n<=56319)return i-2}return i-1},String.prototype.kmwCodePointToCodeUnit=function(i){if(i===null)return null;var t=String(this),e=0;if(i<0){e=t.length;for(var n=0;n>i;n--)e=t.kmwPrevChar(e);return e}if(i==t.kmwLength())return t.length;for(var n=0;n=0?t.kmwSubstr(i,1):""},String.prototype.kmwBMPNextChar=function(i){var t=String(this);return i<0||i>=t.length-1?null:i+1},String.prototype.kmwBMPPrevChar=function(i){var t=String(this);return i<=0||i>t.length?null:i-1},String.prototype.kmwBMPCodePointToCodeUnit=function(i){return i},String.prototype.kmwBMPCodeUnitToCodePoint=function(i){return i},String.prototype.kmwBMPLength=function(){var i=String(this);return i.length},String.prototype.kmwBMPSubstr=function(i,t){var e=String(this);return i>-1?e.substr(i,t):e.substr(e.length+i,-i)},String.kmwEnableSupplementaryPlane=function(i){var t=String.prototype;String._kmwFromCharCode=i?String.kmwFromCharCode:String.fromCharCode,t._kmwCharAt=i?t.kmwCharAt:t.charAt,t._kmwCharCodeAt=i?t.kmwCharCodeAt:t.charCodeAt,t._kmwIndexOf=i?t.kmwIndexOf:t.indexOf,t._kmwLastIndexOf=i?t.kmwLastIndexOf:t.lastIndexOf,t._kmwSlice=i?t.kmwSlice:t.slice,t._kmwSubstring=i?t.kmwSubstring:t.substring,t._kmwSubstr=i?t.kmwSubstr:t.kmwBMPSubstr,t._kmwLength=i?t.kmwLength:t.kmwBMPLength,t._kmwNextChar=i?t.kmwNextChar:t.kmwBMPNextChar,t._kmwPrevChar=i?t.kmwPrevChar:t.kmwBMPPrevChar,t._kmwCodePointToCodeUnit=i?t.kmwCodePointToCodeUnit:t.kmwBMPCodePointToCodeUnit,t._kmwCodeUnitToCodePoint=i?t.kmwCodeUnitToCodePoint:t.kmwBMPCodeUnitToCodePoint},String._kmwFromCharCode||String.kmwEnableSupplementaryPlane(!1)}at();var gr=function(){function i(t){var e=this;this._isFulfilled=!1,this._isRejected=!1,this._promise=new Promise(function(n,c){e._resolve=function(l){e._isFulfilled=!0,n(l)},e._reject=function(l){e._isRejected=!0,c(l)},t&&t(e._resolve,e._reject)})}return Object.defineProperty(i.prototype,"resolve",{get:function(){return this._resolve},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"reject",{get:function(){return this._reject},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"isFulfilled",{get:function(){return this._isFulfilled},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"isRejected",{get:function(){return this._isRejected},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"isResolved",{get:function(){return this.isFulfilled||this.isRejected},enumerable:!1,configurable:!0}),i.prototype.then=function(t,e){return this._promise.then(t,e)},i.prototype.catch=function(t){return this._promise.catch(t)},i.prototype.finally=function(t){return this._promise.finally(t)},Object.defineProperty(i.prototype,"corePromise",{get:function(){return this._promise},enumerable:!1,configurable:!0}),i}(),J=gr;var Jc=function(i){(0,d.__extends)(t,i);function t(e){var n=this,c=null;n=i.call(this,function(s){var o=setTimeout(function(){n.isResolved||s(!0)},e);c=o})||this,n.timerHandle=c;var l=n._resolve;n._resolve=function(s){clearTimeout(n.timerHandle),l(s)};var r=n._reject;return n._reject=function(s){clearTimeout(n.timerHandle),r(s)},n}return t}(J),Bt=Jc,xe=function(i){var t=new Jc(i);return t.corePromise};var Fr=55296,ur=56319,yr=56320,Ir=57343;function St(i){return i>=Fr&&i<=ur}function Rt(i){return i>=yr&&i<=Ir}var Qr=200,O=function(){function i(){}return i.buildDefaultLayout=function(t,e,n){var c=n;typeof i.dfltLayout[c]!="object"&&(c="desktop");var l=b.modifierBitmasks.NON_CHIRAL,r=ne.CURRENT;e&&(l=e.modifierBitmask,r=e.compilerVersion),t||(t=this.DEFAULT_RAW_SPEC);var s=te(i.dfltLayout[c]),o,a=s.layer,B=t.KLS,g=t.K102,F,u,Q,y,I,h,C,U,x=(l&b.modifierBitmasks.IS_CHIRAL)!=0;if(t.F){var G=/^(?:(?:italic|bold) )* *[0-9.eE-]+(?:[a-z]+) "(.+)"$/.exec(t.F);G&&(s.font=G[1])}var m=!(typeof B=="undefined"||!B);m||(B=t.KLS=i.processLegacyDefinitions(t.BK));var X=/\*\w+\*/,p=Object.getOwnPropertyNames(B),Z=[];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"),B["leftctrl-leftalt"]=B.rightalt),p.indexOf("leftctrl-leftalt-shift")==-1&&p.indexOf("rightalt-shift")!=-1&&(p.push("leftctrl-leftalt-shift"),B["leftctrl-leftalt-shift"]=B["rightalt-shift"])),s.displayUnderlying=e?!!e.scriptObject.KDU:!1,n=="desktop")for(Z=i.generateLayerIds(x),o=0;o0&&(a[o]=te(a[0])),a[o].id=R[o],a[o].nextlayer=R[o],i.formatDefaultLayer(a[o],x,n,!!g);for(o=0;o=0&&W0&&A!=null&&(A.sp=2,A.sk=null,A.text=i.modifierSpecials[a[o].id]?i.modifierSpecials[a[o].id]:"*Shift*")}return s},i.getLayerId=function(t){var e=b.modifierCodes,n="";return t==0?"default":(t&e.LCTRL&&(n=(n.length>0?n+"-":"")+"leftctrl"),t&e.RCTRL&&(n=(n.length>0?n+"-":"")+"rightctrl"),t&e.LALT&&(n=(n.length>0?n+"-":"")+"leftalt"),t&e.RALT&&(n=(n.length>0?n+"-":"")+"rightalt"),t&e.SHIFT&&(n=(n.length>0?n+"-":"")+"shift"),t&e.CTRL&&(n=(n.length>0?n+"-":"")+"ctrl"),t&e.ALT&&(n=(n.length>0?n+"-":"")+"alt"),n)},i.generateLayerIds=function(t){var e,n;t?(e=32,n=1):(e=8,n=16);for(var c=[],l=0;l?~~~~~ ",i.DEFAULT_RAW_SPEC={F:"Tahoma",BK:i.dfltText},i.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*"},i.dfltShiftToCaps={id:"T_*_MT_SHIFT_TO_CAPS",text:"*ShiftLock*",sp:1,nextlayer:"caps"},i.dfltShiftToDefault={id:"T_*_MT_SHIFT_TO_DEFAULT",text:"*Shift*",sp:1,nextlayer:"default"},i.dfltShiftToShift={id:"T_*_MT_SHIFT_TO_SHIFT",text:"*Shift*",sp:1,nextlayer:"shift"},i.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}]}]}]}},i}();function he(i,t,e){e.enumerable=!0}var Sc={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"},Rc=["n","ne","e","se","s","sw","w","nw"],Wc=function(){function i(){this.isMnemonic=!1}return Object.defineProperty(i.prototype,"baseKeyID",{get:function(){if(typeof this.id!="undefined")return this.id},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"isPadding",{get:function(){return this.sp==10},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"coreID",{get:function(){if(typeof this.id!="undefined"){var t=this.id||"";return this.displayLayer!=this.layer&&(t=t+"+"+this.layer),t}},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"elementID",{get:function(){if(typeof this.id!="undefined")return this.displayLayer+"-"+this.coreID},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"baseKeyEvent",{get:function(){return new oe(this._baseKeyEvent)},enumerable:!1,configurable:!0}),i.unicodeIDToText=function(t,e){if(!t||t.substring(0,2)!="U_")return null;for(var n="",c=t.substring(2).split("_"),l=0,r=c;l0){var y=o[o.length-1];if(o.length==1&&y.pad<0){var I=y.width/l,h=1-(u+I+Q);F(y,h,I,u)}else{var h=y.pad/l,I=1-(u+h+Q);F(y,h,I,u)}}var C=new i;for(var B in C)t.hasOwnProperty(B)||(t[B]=C[B]);var U=t;U.proportionalY=r},i.prototype.populateKeyMap=function(t){this.key.forEach(function(e){e.coreID&&(t[e.coreID]=e)})},i.SPECIAL_LABEL=/\*\w+\*/,(0,d.__decorate)([he],i.prototype,"populateKeyMap",null),i}();var Hc=function(){function i(){}return i.sanitize=function(t){for(var e=0,n=t.row;er&&(r=B)}n.formFactor=="desktop"?r+=5:r+=M.DEFAULT_RIGHT_MARGIN;for(var y=t.row.length,I=0;I1){var n=this.keyMap[e[0]];return n.getSubkey(e[1])}else return this.keyMap[t]},(0,d.__decorate)([he],i.prototype,"constructKeyMap",null),(0,d.__decorate)([he],i.prototype,"getKey",null),i}();var At=function(){function i(){this.hasFlicks=!1,this.hasLongpresses=!1,this.hasMultitaps=!1}return i.prototype.getLayer=function(t){return this.layerMap[t]},i.correctLayerEmptyRowBug=function(t){for(var e=0;e=0;l--)(!Array.isArray(c[l].key)||c[l].key.length==0)&&c.splice(l,1)}},i.sanitize=function(t){i.correctLayerEmptyRowBug(t.layer);for(var e=0,n=t.layer;e0){e=this._legacyLayoutSpec;break}}if(!e&&(this.helpText==""||t!=V.FormFactor.Desktop)&&(e={F:"Tahoma",BK:O.dfltText}),this._layouts||(this._layouts={}),e){var l=this._layouts[t]=O.buildDefaultLayout(e,this,t);return l.isDefault=!0,l}else return this._layouts[t]=null,null},i.prototype.layout=function(t){var e=this.findOrConstructLayout(t);return e?(this.layoutStates[t]==we.NOT_LOADED&&(e=At.polyfill(e,this,t),this.layoutStates[t]=we.POLYFILLED),e):null},i.prototype.refreshLayouts=function(){var t=[V.FormFactor.Desktop,V.FormFactor.Phone,V.FormFactor.Tablet],e=this;t.forEach(function(n){e.layoutStates[n]=we.NOT_LOADED})},i.prototype.markLayoutCalibrated=function(t){this.layoutStates[t]!=we.NOT_LOADED&&(this.layoutStates[t]=we.CALIBRATED)},i.prototype.getLayoutState=function(t){return this.layoutStates[t]},i.prototype.constructNullKeyEvent=function(t,e){e=e||{K_CAPS:!1,K_NUMLOCK:!1,K_SCROLL:!1};var n=oe.constructNullKeyEvent(t);return this.setSyntheticEventDefaults(n,e),n},i.prototype.constructKeyEvent=function(t,e,n){var c=t.baseKeyEvent;c.device=e,this.isMnemonic&&c.setMnemonicCode(t.layer.indexOf("shift")!=-1,n.K_CAPS),this.setSyntheticEventDefaults(c,n);var l={K_CAPS:b.stateBitmasks.CAPS,K_NUMLOCK:b.stateBitmasks.NUM_LOCK,K_SCROLL:b.stateBitmasks.SCROLL_LOCK},r=l[c.kName];return r&&(c.Lstates^=r,c.LmodifierChange=!0),c},i.prototype.setSyntheticEventDefaults=function(t,e){t.device.touchable||(t.Lstates=0,t.Lstates|=e.K_CAPS?b.modifierCodes.CAPS:b.modifierCodes.NO_CAPS,t.Lstates|=e.K_NUMLOCK?b.modifierCodes.NUM_LOCK:b.modifierCodes.NO_NUM_LOCK,t.Lstates|=e.K_SCROLL?b.modifierCodes.SCROLL_LOCK:b.modifierCodes.NO_SCROLL_LOCK),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&b.modifierBitmasks.ALT_GR_SIM)==b.modifierBitmasks.ALT_GR_SIM&&this.emulatesAltGr&&(t.Lmodifiers&=~b.modifierBitmasks.ALT_GR_SIM,t.Lmodifiers|=b.modifierCodes.RALT)},i.prototype.getVKDictionaryCode=function(t){if(!this.scriptObject.VKDictionary){var e=[];if(typeof this.scriptObject.KVKD=="string")for(var n=this.scriptObject.KVKD.split(" "),c=0;ct&&(l.p+=e)}},i.prototype.count=function(){return this.dks.length},i}();var Tc=function(){function i(t){this.id=t}return i.prototype.set=function(t){throw new Error("System store with ID "+this.id+" may not be directly set.")},i}();var Ht=function(i){(0,d.__extends)(t,i);function t(e,n){var c=i.call(this,e)||this;return c.handler=null,c._value=n,c}return Object.defineProperty(t.prototype,"value",{get:function(){return this._value},enumerable:!1,configurable:!0}),t.prototype.matches=function(e){return this._value==e},t.prototype.set=function(e){this.handler&&this.handler(this,e)||(this._value=e)},t}(Tc);var Ec=function(i){(0,d.__extends)(t,i);function t(e){var n=i.call(this,w.TSS_PLATFORM)||this;return n.kbdInterface=e,n}return t.prototype.matches=function(e){var n,c,l=e.split(" "),r=this.kbdInterface.activeDevice;for(n=0;n=l.selStart,l}return t.from=function(e,n){var c;if(e instanceof t){var l=e;c=new t(l.text,l.selStart,l.selEnd)}else{var r=e.getText(),s=r._kmwLength(),o=s,a=0;if(e.hasSelection()){var B=e.getTextBeforeCaret(),g=e.getTextAfterCaret();o=B._kmwLength(),a=s-g._kmwLength()}c=new t(r,o,a)}return c.setDeadkeys(e.deadkeys()),c},t.prototype.clearSelection=function(){this.text=this.getTextBeforeCaret()+this.getTextAfterCaret(),this.selEnd=this.selStart,this.selForward=!0},t.prototype.invalidateSelection=function(){},t.prototype.isSelectionEmpty=function(){return this.selStart==this.selEnd},t.prototype.hasSelection=function(){return!0},t.prototype.getDeadkeyCaret=function(){return this.selStart},t.prototype.setSelection=function(e,n){if(this.selStart=e,this.selEnd=typeof n=="number"?n:e,this.selForward=n>=e,!this.selForward){var c=this.selStart;this.selStart=this.selEnd,this.selEnd=c}},t.prototype.getTextBeforeCaret=function(){return this.text.kmwSubstr(0,this.selStart)},t.prototype.getSelectedText=function(){return this.text.kmwSubstr(this.selStart,this.selEnd-this.selStart)},t.prototype.getTextAfterCaret=function(){return this.text.kmwSubstr(this.selEnd)},t.prototype.getText=function(){return this.text},t.prototype.deleteCharsBeforeCaret=function(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)},t.prototype.insertTextBeforeCaret=function(e){this.adjustDeadkeys(e._kmwLength()),this.text=this.getTextBeforeCaret()+e+this.text.kmwSubstr(this.selStart),this.selStart+=e.kmwLength(),this.selEnd+=e.kmwLength()},t.prototype.handleNewlineAtCaret=function(){this.insertTextBeforeCaret("\n")},t.prototype.setTextAfterCaret=function(e){this.text=this.getTextBeforeCaret()+e},t.prototype.doInputEvent=function(){},t}(kc);var Xr=function(){function i(){this.transcription=null,this.setStore={},this.saveStore={},this.variableStores={},this.triggersDefaultCommand=!1}return i.prototype.finalize=function(t,e,n){if(!this.transcription)throw"Cannot finalize a RuleBehavior with no transcription.";t.beepHandler&&this.beep&&t.beepHandler(e);for(var c in this.setStore){var l=t.keyboardInterface.systemStores[c];if(l)try{l.set(this.setStore[c])}catch(s){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: "+c)}if(t.keyboardInterface.applyVariableStores(this.variableStores),t.keyboardInterface.variableStoreSerializer)for(var c in this.saveStore)t.keyboardInterface.variableStoreSerializer.saveStore(t.activeKeyboard.id,c,this.saveStore[c]);if(this.triggersDefaultCommand){var r=this.transcription.keystroke;t.defaultRules.applyCommand(r,e)}t.warningLogger&&this.warningLog?t.warningLogger(this.warningLog):t.errorLogger&&this.errorLog&&t.errorLogger(this.errorLog)},i.prototype.mergeInDefaults=function(t){var 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;var c=D.from(this.transcription.preInput,!1);c.apply(this.transcription.transform),c.apply(t.transcription.transform),this.transcription=c.buildTranscriptionFrom(this.transcription.preInput,e,!1,this.transcription.alternates)},i}(),de=Xr;var Vr=function(){function i(){}return i}();var Zr=function(){function i(){}return i.prototype.reset=function(){this._cache=[]},i.prototype.get=function(t,e){return typeof this._cache[t]=="undefined"||typeof this._cache[t][e]=="undefined"?null:this._cache[t][e]},i.prototype.set=function(t,e,n){typeof this._cache[t]=="undefined"&&(this._cache[t]=[]),this._cache[t][e]=n},i}(),vr=function(){function i(){}return i.prototype.reset=function(){this._cache=[]},i.prototype.get=function(t,e){return typeof this._cache[t]=="undefined"||typeof this._cache[t][e]=="undefined"?null:this._cache[t][e]},i.prototype.set=function(t,e,n){typeof this._cache[t]=="undefined"&&(this._cache[t]=[]),this._cache[t][e]=n},i.prototype.clone=function(){var t=new i;return t._cache=this._cache,t},i}(),w;(function(i){i[i.TSS_LAYER=33]="TSS_LAYER",i[i.TSS_PLATFORM=31]="TSS_PLATFORM",i[i.TSS_NEWLAYER=42]="TSS_NEWLAYER",i[i.TSS_OLDLAYER=43]="TSS_OLDLAYER"})(w||(w={}));var Yc=function(i){(0,d.__extends)(t,i);function t(e,n,c){c===void 0&&(c=null);var l=i.call(this,e,n)||this;return l.cachedContext=new Zr,l.cachedContextEx=new vr,l._AnyIndices=[],l.systemStores={},l.systemStores[w.TSS_PLATFORM]=new Ec(l),l.systemStores[w.TSS_LAYER]=new Ht(w.TSS_LAYER,"default"),l.systemStores[w.TSS_NEWLAYER]=new Ht(w.TSS_NEWLAYER,""),l.systemStores[w.TSS_OLDLAYER]=new Ht(w.TSS_OLDLAYER,""),l.variableStoreSerializer=c,l}return Object.defineProperty(t.prototype,"Codes",{get:function(){return b},enumerable:!1,configurable:!0}),t.prototype.saveFocus=function(){},t.prototype.registerKeyboard=function(e){var n=new q(e);this.loadedKeyboard=n},t.prototype.context=function(e,n,c){var l=this.cachedContext.get(e,n);if(l!==null)return l;var r=this.KC_(e,n,c);return this.cachedContext.set(e,n,r),r},t.prototype.KC_=function(e,n,c){var l="";return l=c.isSelectionEmpty()?c.getTextBeforeCaret():"",l._kmwLength()0&&r[0].p>a){r.splice(0,1);continue}else if(r.length>0&&r[0].p==a)l.deadContext[e-l.valContext.length-1]=r[0],l.valContext=[r[0].d].concat(l.valContext),r.splice(0,1);else{var B=this.context(++s,1,c);l.valContext=[B].concat(l.valContext)}}this.cachedContextEx.set(e,e,l)}var g=l;g.valContext=g.valContext.slice(0,n);for(var F=0;F255&&(r=e.vkCode),e.LisVirtualKey||r>255?((n&16384)==16384||r>255)&&(l=c==r&&(n&o)==B,l=l&&this.stateMatch(e,n&a)):(n&16384)==0&&(l=r==c),l||this.activeTargetOutput.deadkeys().resetMatched(),l},t.prototype.stateMatch=function(e,n){return(n&e.Lstates)==n},t.prototype.keyInformation=function(e){var n=new Vr;return n.vk=e.LisVirtualKey,n.code=e.Lcode,n.modifiers=e.Lmodifiers,n},t.prototype.deadkeyMatch=function(e,n,c){return n.hasDeadkeyMatch(e,c)},t.prototype.beep=function(e){this.resetContextCache(),this.ruleBehavior.beep=!0},t.prototype._ExplodeStore=function(e){if(typeof e=="string"){var n=this.activeKeyboard.explodedStores;if(n[e])return n[e];for(var c=[],l=0;l=0},t.prototype._Index=function(e,n){return e=this._ExplodeStore(e),this._AnyIndices[n-1]0){c=this._BuildExtendedContext(e,e,n);for(var l=0,r=0;ro&&(e=o)}n.deadkeys().resetMatched(),this.output(e,n,"")},t.prototype.output=function(e,n,c){this.resetContextCache(),n.saveProperties(),n.clearSelection(),n.deadkeys().deleteMatched(),e>=0&&n.deleteCharsBeforeCaret(e),n.insertTextBeforeCaret(c),n.restoreProperties()},t.prototype.contextExOutput=function(e,n,c,l){this.resetContextCache(),e>=0&&this.output(e,n,"");var r=this.ruleContextEx.get(c,c),s=r.deadContext[l-1],o=r.valContext[l-1];if(s)n.insertDeadkeyBeforeCaret(s.d);else if(typeof o=="string")this.output(-1,n,o);else throw new Error("contextExOutput: should never be a numeric valContext with no corresponding deadContext")},t.prototype.deadkeyOutput=function(e,n,c){this.resetContextCache(),e>=0&&this.output(e,n,""),n.insertDeadkeyBeforeCaret(c)},t.prototype.ifStore=function(e,n,c){var l=!0,r=this.systemStores[e];return r&&(l=r.matches(n)),l},t.prototype.setStore=function(e,n,c){if(this.resetContextCache(),e==w.TSS_LAYER&&this.activeDevice.touchable)this.ruleBehavior.setStore[e]=n;else return!1},t.prototype.loadStore=function(e,n,c){if(this.resetContextCache(),this.variableStoreSerializer){var l=this.variableStoreSerializer.loadStore(e,n);return l[n]||c}else return c},t.prototype.saveStore=function(e,n){this.resetContextCache();var c=this.activeKeyboard;if(!c||typeof c.id=="undefined"||c.id=="")return!1;var l={};return l[e]=n,this.ruleBehavior?this.ruleBehavior.saveStore[e]=l:this.variableStoreSerializer.saveStore(this.activeKeyboard.id,e,l),!0},t.prototype.resetContextCache=function(){this.cachedContext.reset(),this.cachedContextEx.reset()},t.prototype.defaultBackspace=function(e){e.isSelectionEmpty()?this.output(1,e,""):this.output(0,e,"")},t.prototype.processNewContextEvent=function(e,n){if(!this.activeKeyboard)throw"No active keyboard for keystroke processing!";return this.process(this.activeKeyboard.processNewContextEvent.bind(this.activeKeyboard),e,n,!0)},t.prototype.processPostKeystroke=function(e,n){if(!this.activeKeyboard)throw"No active keyboard for keystroke processing!";return this.process(this.activeKeyboard.processPostKeystroke.bind(this.activeKeyboard),e,n,!0)},t.prototype.processKeystroke=function(e,n){if(!this.activeKeyboard)throw"No active keyboard for keystroke processing!";return this.process(this.activeKeyboard.process.bind(this.activeKeyboard),e,n,!1)},t.prototype.process=function(e,n,c,l){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();var r=D.from(n,!0),s=this.activeKeyboard.variableStores;this.ruleBehavior=new de,this.activeDevice=c.device,this.activeTargetOutput=n;var o=e(n,c);this.activeTargetOutput=null,this.ruleBehavior.transcription=n.buildTranscriptionFrom(r,c,l),this.ruleBehavior.variableStores=this.activeKeyboard.variableStores,this.activeKeyboard.variableStores=s,this.ruleBehavior.triggerKeyDefault=!o;var a=this.ruleBehavior;return this.ruleBehavior=null,a},t.prototype.applyVariableStores=function(e){this.activeKeyboard.variableStores=e},t.__publishShorthandAPI=function(){var e=this.prototype,n=function(c,l){e[l]&&(e[c]=e[l])};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")},t.GLOBAL_NAME="KeymanWeb",t}(Ft),yt=Yc;(function(){Yc.__publishShorthandAPI()})();var wc=N(T(),1);var fr=function(i){(0,d.__extends)(t,i);function t(e,n){var c=i.call(this)||this;return c.stateKeys={K_CAPS:!1,K_NUMLOCK:!1,K_SCROLL:!1},c.modStateFlags=0,n||(n=t.DEFAULT_OPTIONS),c.contextDevice=e,c.baseLayout=n.baseLayout||t.DEFAULT_OPTIONS.baseLayout,c.keyboardInterface=n.keyboardInterface||new yt(ze(),gt),c.defaultRules=n.defaultOutputRules||t.DEFAULT_OPTIONS.defaultOutputRules,c}return Object.defineProperty(t.prototype,"activeKeyboard",{get:function(){return this.keyboardInterface.activeKeyboard},set:function(e){this.keyboardInterface.activeKeyboard=e,this.resetContext()},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"layerStore",{get:function(){return this.keyboardInterface.systemStores[w.TSS_LAYER]},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"newLayerStore",{get:function(){return this.keyboardInterface.systemStores[w.TSS_NEWLAYER]},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"oldLayerStore",{get:function(){return this.keyboardInterface.systemStores[w.TSS_OLDLAYER]},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"layerId",{get:function(){return this.layerStore.value},set:function(e){this.layerStore.set(e)},enumerable:!1,configurable:!0}),t.prototype.defaultRuleBehavior=function(e,n,c){var l=D.from(n,c),r=new de,s=!1,o="",a;if(e.isSynthetic||n.isSynthetic)if(s=!0,this.defaultRules.isCommand(e))r.triggersDefaultCommand=!0;else if((a=this.defaultRules.forSpecialEmulation(e))!=null)switch(a){case Je.Backspace:this.keyboardInterface.defaultBackspace(n);break;case Je.Enter:n.handleNewlineAtCaret();break;default:r.errorLog="Unexpected 'special emulation' character (\\u"+a.kmwCharCodeAt(0).toString(16)+") went unhandled!"}else s=!1;var B=this.activeKeyboard&&this.activeKeyboard.isMnemonic;if(!s)if((o=this.defaultRules.forAny(e,B))!=null)if(a=this.defaultRules.forSpecialEmulation(e),a==Je.Backspace)this.keyboardInterface.defaultBackspace(n);else{if(a||this.defaultRules.isCommand(e))return null;this.keyboardInterface.output(0,n,o)}else return null;if(r.errorLog)return r;var g=n.buildTranscriptionFrom(l,e,c);return r.transcription=g,r},t.prototype.processNewContextEvent=function(e,n){return this.activeKeyboard?this.keyboardInterface.processNewContextEvent(n,this.activeKeyboard.constructNullKeyEvent(e,this.stateKeys)):null},t.prototype.processPostKeystroke=function(e,n){return this.activeKeyboard?this.keyboardInterface.processPostKeystroke(n,this.activeKeyboard.constructNullKeyEvent(e,this.stateKeys)):null},t.prototype.processKeystroke=function(e,n){var c,l=n.getTextBeforeCaret().kmwLength()==0&&n.isSelectionEmpty();if(this.activeKeyboard&&e.Lcode!=0&&(c=this.keyboardInterface.processKeystroke(n,e)),l&&e.Lcode==b.keyCodes.K_BKSP&&c.triggerKeyDefault)c=this.defaultRuleBehavior(e,n,!1),c.triggerKeyDefault=!0,c.transcription.transform.deleteLeft=1;else if(!c||c.triggerKeyDefault){e.Lcode=e.vkCode||e.Lcode,this.keyboardInterface.activeTargetOutput=n;var r=this.defaultRuleBehavior(e,n,!1);r&&(c?c.mergeInDefaults(r):c=r,c.triggerKeyDefault=!1),this.keyboardInterface.activeTargetOutput=null}return c},t.prototype._UpdateVKShift=function(e){var n=0,c=["CAPS","NUM_LOCK","SCROLL_LOCK"],l=["K_CAPS","K_NUMLOCK","K_SCROLL"];if(!this.activeKeyboard)return!0;if(e){n=e.Lmodifiers,this.activeKeyboard.isChiral&&this.activeKeyboard.emulatesAltGr&&(this.modStateFlags&b.modifierBitmasks.ALT_GR_SIM)==b.modifierBitmasks.ALT_GR_SIM&&(n|=b.modifierBitmasks.ALT_GR_SIM,n&=~b.modifierCodes.RALT);for(var r=!1,s=0;s=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(Q){}}else if(e.indexOf("Linux")>=0)this.OS="Linux";else if(e.indexOf("Macintosh")>=0){var c=/Intel Mac OS X (\d+(?:[_\.]\d+)+)/i,l=c.exec(e);if(!l)console.warn("KMW could not properly parse the user agent string.A suboptimal keyboard layout may result."),this.OS="MacOSX";else if(l.length>1&&l[1]){var r=l[1].replace("_","."),s=new ne(r);t=ne.MAC_POSSIBLE_IPAD_ALIAS.compareTo(s)<=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))}var o=Math.min(screen.width,screen.height),a=Math.max(screen.width,screen.height),B=o/a;this.OS!="iOS"&&this.formFactor=="phone"&&(o>=600&&B>.5625||B>=.625)&&(this.formFactor="tablet");var g=navigator.platform=="Win32"||navigator.platform=="MacIntel";this.OS=="iOS"&&!("ongesturestart"in window)&&!g&&(this.OS="Android"),this.browser="web",(this.OS=="iOS"||this.OS.toLowerCase()=="macosx")&&(this.browser="safari");var F=/Firefox|Chrome|OPR|Safari|Edge/;if(F.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;var u=screen.height/screen.width;u<1&&(u=1/u),u>1.6&&(this.formFactor="phone",this.dyPortrait=this.dyLandscape=25)}return this.colorScheme=Mc.prefersDarkMode()?"dark":"light",this.detected=!0,this.coreSpec},Object.defineProperty(i.prototype,"coreSpec",{get:function(){return new V(this.browser,this.formFactor,this.OS,this.touchable)},enumerable:!1,configurable:!0}),i}();var pn=function(i){(0,d.__extends)(t,i);function t(e,n){var c=i.call(this)||this;if(c.applyCacheBusting=!1,!n){var l=new Wt;l.detect(),n=l.coreSpec}return c.sourcePath=e,c.hostDevice=n,c.deferForInitialization=new J,c}return t.prototype.initialize=function(e){var n=this;this._paths?this._paths.updateFromOptions(e):this._paths=new Un(e,this.sourcePath),typeof e.setActiveOnRegister=="boolean"?this.activateFirstKeyboard=e.setActiveOnRegister:this.activateFirstKeyboard=!0,this._spacebarText=e.spacebarText,Ke.spacebarTextMode=function(){return n.spacebarText}},t.prototype.finalizeInit=function(){this.deferForInitialization.resolve()},Object.defineProperty(t.prototype,"paths",{get:function(){return this._paths},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"spacebarText",{get:function(){return this._spacebarText},set:function(e){this._spacebarText!=e&&(this._spacebarText=e,this.emit("spacebartext",e))},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"softDevice",{get:function(){return this.hostDevice},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"hardDevice",{get:function(){return dn(this.hostDevice)},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"stubNamespacer",{get:function(){return this._stubNamespacer},set:function(e){this._stubNamespacer=e},enumerable:!1,configurable:!0}),t.prototype.debugReport=function(){return{hostDevice:this.hostDevice,initialized:this.deferForInitialization.isResolved}},t.prototype.onRuleFinalization=function(e,n){},t}(Dc.default);var Gn=(0,d.__assign)({setActiveOnRegister:!0,spacebarText:Be.LANGUAGE_KEYBOARD},xn);var zc=N(T(),1);var mn=function(i){(0,d.__extends)(t,i);function t(e){var n=i.call(this)||this;return n.pendingActivations=[],n.engineConfig=e,n}return Object.defineProperty(t.prototype,"predictionContext",{get:function(){return this._predictionContext},enumerable:!1,configurable:!0}),t.prototype.configure=function(e){this._resetContext=e.resetContext,this._predictionContext=e.predictionContext,this.keyboardCache=e.keyboardCache},t.prototype.insertText=function(e,n,c){var l=this.activeTarget;return l!=null?(n!=null&&e.output(0,l,n),typeof c!="undefined"&&c!==null&&e.deadkeyOutput(0,l,c),l.invalidateSelection(),!0):!1},t.prototype.resetContext=function(){this._resetContext(this.activeTarget),this.predictionContext.resetContext()},t.prototype.findAndPopActivation=function(e){var n;for(n=0;n9?c=0:c=n-1;else if(typeof n=="string"){for(var l=n.length==2?fn:vn,r=0;r1&&n?"languages":"keyboards"),r="?jsonp=keyman.register&languageidtype=bcp47&version="+ne.CURRENT.toString(),s=l+r+e,o=this.requestEngine.request(s),a=o.promise,B=o.queryId;return this.cloudResolutionPromises[B]=a,a.finally(function(){delete c.cloudResolutionPromises[B]}),a.corePromise},t.prototype._registerCore=function(e){var n=e.options,c=n.fontBaseUri;if(this.pathConfig.fonts!=""?c=this.pathConfig.fonts:this.pathConfig.updateFontPath(c),typeof e.error=="string"){var l="";if(typeof e.options.keyboardid=="string"){var r=e.options.keyboardid;l=r.substring(0,1).toUpperCase()+r.substring(1)}return new Error(Nr(l))}if(typeof n=="undefined"||typeof n.context=="undefined")return new Error(Ln);var s=[];if(n.context=="keyboard"){var o=void 0,a=e.keyboard;if(Array.isArray(a))for(o=0;o0)r.push(B);else if(B.KI||B.KL||B.KLC||B.KFont||B.KOskFont)l.push(new ge(B));else{var g=B;if(typeof g.language!="undefined"&&console.warn("The 'language' property for keyboard stubs has been deprecated. Please use the 'languages' property instead."),g.languages||(g.languages=g.language),typeof g.languages=="undefined"){var F="To use keyboard '"+g.id+"', you must specify languages.";s.push(Sn(g,F))}else if(Array.isArray(g.languages))for(var u=ge.toStubs(g,this.pathConfig.keyboards,this.pathConfig.fonts),Q=0,y=u;Q1&&(L=S[1].split(","));for(var v=0;v0&&L[v]=="")){var Z=ei(S[0],L[v],S[2]);ti(this.cache,G,Z)&&G.push(Z)}}n.forEach(function(E){return e.cache.addStub(E)});var H=this.cloudQueryEngine.fetchCloudStubs(G.map(function(E){return E.toString()}));return H.then(function(E){for(var W=0,Te=E;W0?t=n[0]:t=document.body}this.linkNode=t,this.doCacheBusting=e||!1}return Object.defineProperty(i.prototype,"sheets",{get:function(){return this.linkedSheets},enumerable:!1,configurable:!0}),i.prototype.linkStylesheet=function(t){!(t instanceof HTMLLinkElement)&&!t.innerHTML||(this.linkedSheets.push(t),this.linkNode.appendChild(t))},i.prototype.allLoadedPromise=function(){var t;return(0,d.__awaiter)(this,void 0,void 0,function(){var e,n,c,l,r,s;return(0,d.__generator)(this,function(o){switch(o.label){case 0:for(e=[],n=function(a){if(!((t=a.sheet)===null||t===void 0)&&t.cssRules)e.push(Promise.resolve());else if(a.innerHTML)e.push(Promise.resolve());else{var B=new J;a.addEventListener("load",function(){return B.resolve()}),a.addEventListener("error",function(){return B.reject()}),e.push(B.corePromise)}},c=0,l=this.linkedSheets;c0&&(o=B[s]),B[s].toLowerCase().indexOf(".ttf")>0&&(o=B[s]),B[s].toLowerCase().indexOf(".woff")>0&&(a=B[s]);o!=""&&o.indexOf("/")<0&&(o=e+o),a!=""&&a.indexOf("/")<0&&(a=e+a);var u="@font-face {\nfont-family:"+t.family+";\nfont-style:normal;\nfont-weight:normal;\n";if(n==V.OperatingSystem.iOS?o!=""&&(this.doCacheBusting&&(o=this.cacheBust(o)),r="url('"+encodeURI(o)+"') format('truetype')"):(a!=""&&(r="url('"+encodeURI(a)+"') format('woff')"),o!=""&&(r="url('"+encodeURI(o)+"') format('truetype')")),!r)return null;u+="src:"+r+";",u=u+"\n}\n";var Q=Se(u);g[l]=Q;var y=new FontFace(t.family,r),I=function(){return c.fontPromises=c.fontPromises.filter(function(C){return C!=h})},h=y.load().then(I).catch(I);return this.fontPromises.push(h),this.linkStylesheet(Q),Q}},i.prototype.cacheBust=function(t){return t+"?v="+new Date().getTime()},i.prototype.linkExternalSheet=function(t,e){try{if(!e&&document.querySelector("link[href="+JSON.stringify(t)+"]")!=null)return null}catch(c){return null}var n=document.createElement("link");return n.type="text/css",n.rel="stylesheet",n.href=t,this.linkStylesheet(n),n},i.prototype.unlink=function(t){var e=this.linkedSheets.indexOf(t);return e>-1?(this.linkedSheets.splice(e,1),t.parentNode.removeChild(t),!0):!1},i.prototype.unlinkAll=function(){for(var t=0,e=this.linkedSheets;t1){var a=o[0],B=o[1];n[a]=e(B,a)}else n[o[0]]=""}return n},i.prototype.saveCookie=function(t,e,n){var c="";for(var l in e)c+=l+"="+n(e[l],l)+";";var r=new Date(new Date().valueOf()+1e3*60*60*24*30).toUTCString(),s=" path=/; expires="+r;document.cookie="".concat(t,"=").concat(encodeURIComponent(c),"; ").concat(s)},i}(),Fe=wr;function K(i){var t;if(!i)return 0;var e=i.offsetLeft?i.offsetLeft:0;if(t=i,t.offsetParent){for(;t.offsetParent;)t=t.offsetParent,e+=t.offsetLeft;var n=t.ownerDocument;t.style.position=="fixed"&&n&&n.scrollingElement&&(e+=n.scrollingElement.scrollLeft)}if(t&&t.ownerDocument&&i.ownerDocument!=window.document){var c=t.ownerDocument;if(c&&c.defaultView&&c.defaultView.frameElement)return e+K(c.defaultView.frameElement)-c.documentElement.scrollLeft}return e}function _(i){var t;if(!i)return 0;var e=i.offsetTop?i.offsetTop:0;if(t=i,t.ownerDocument&&t instanceof t.ownerDocument.defaultView.HTMLElement){for(;t.offsetParent;)t=t.offsetParent,e+=t.offsetTop;var n=t.ownerDocument;t.style.position=="fixed"&&n&&n.scrollingElement&&(e+=n.scrollingElement.scrollTop)}if(t&&t.ownerDocument&&i.ownerDocument!=window.document){var c=t.ownerDocument;if(c&&c.defaultView&&c.defaultView.frameElement)return e+_(c.defaultView.frameElement)}return e}var ci=function(i){(0,d.__extends)(t,i);function t(e,n){return i.call(this,"KeymanWeb_".concat(e,"_Option_").concat(n))||this}return t.prototype.load=function(){return i.prototype.load.call(this,decodeURIComponent)},t.prototype.save=function(e){i.prototype.save.call(this,e,encodeURIComponent)},t}(Fe),Hn=function(){function i(){}return i.prototype.loadStore=function(t,e){var n=new ci(t,e);return n.load()},i.prototype.saveStore=function(t,e,n){var c=new ci(t,e);c.save(n)},i}();var ii=function(i){(0,d.__extends)(t,i);function t(e,n,c){var l=i.call(this,e,n,new Hn)||this;return l.insertText=function(r,s){l.resetContextCache(),l.engine.contextManager.insertText(l,r,s)},l.KT=l.insertText,l.engine=n,l.stubNamespacer=c,l}return t.prototype.preserveID=function(e){var n;if(document.currentScript)n=document.currentScript.id;else{var c=document.getElementsByTagName("script"),l=c[c.length-1];n=l.id}if(n)n.indexOf(pe(e.KI))!=-1?e.KI=n:console.error("Error when registering keyboard: current SCRIPT tag's ID does not match!");else return},t.prototype.registerKeyboard=function(e){var n=this;i.prototype.registerKeyboard.call(this,e);var c=this.loadedKeyboard;this.preserveID(e),this.engine.config.deferForInitialization.then(function(){n.engine.keyboardRequisitioner.cache.isFetchingKeyboard(c.id)||(n.engine.keyboardRequisitioner.cache.addKeyboard(c),n.loadedKeyboard=null)})},t.prototype.registerStub=function(e){var n=this,c;this.stubNamespacer&&this.stubNamespacer(e);var l=function(){var s=n.engine.config.paths;return new ge(e,s.keyboards,s.fonts)};if(!this.engine.config.deferForInitialization.isResolved)this.engine.config.deferForInitialization.then(function(){return n.engine.keyboardRequisitioner.cache.addStub(l())});else{var r=l();if(!((c=this.engine.keyboardRequisitioner)===null||c===void 0)&&c.cache.findMatchingStub(r))return 1;this.engine.keyboardRequisitioner.cache.addStub(r)}return null},t}(yt),Wn=ii;(function(){ii.__publishShorthandAPI()})();var li=function(i){(0,d.__extends)(t,i);function t(e,n){var c=this;return e&&e._jsGlobal!=window&&(e._jsGlobal.String=window.String),e?c=i.call(this,e)||this:c=i.call(this,new Ft(window,gt))||this,c.performCacheBusting=n||!1,c}return t.prototype.loadKeyboardInternal=function(e,n,c){var l=this,r=new J;this.performCacheBusting&&(e=this.cacheBust(e));try{var s=this.harness._jsGlobal.document,o=s.createElement("script");c&&(o.id=c),s.head.appendChild(o),o.onerror=function(a){r.reject(n.missingError(a))},o.onload=function(){if(l.harness.loadedKeyboard){var a=l.harness.loadedKeyboard;l.harness.loadedKeyboard=null,r.resolve(a)}else r.reject(n.scriptError())},r.then(function(){o.remove()}).catch(function(){o.remove()}),o.src=e}catch(a){return Promise.reject(a)}return r.corePromise},t.prototype.cacheBust=function(e){return e+"?v="+new Date().getTime()},t}(un);function Et(i,t){var e=new Map;return t.keys.forEach(function(n){var c=Math.abs(i.x-n.centerX),l=Math.abs(i.y-n.centerY),r,s;c>.5*n.width?(r=c-.5*n.width,c=.5):(r=0,c/=n.width),l>.5*n.height?(s=l-.5*n.height,l=.5):(s=0,l/=n.height),r*=t.kbdScaleRatio,r+=c*n.height,s+=l*n.height;var o=r*r+s*s;e.set(n.keySpec,o)}),e}function Re(i){var t,e,n,c,l,r,s,o=new Map,a=0;Array.isArray(i)||(i=[i]);try{for(var B=(0,d.__values)(i),g=B.next();!g.done;g=B.next()){var F=g.value;try{for(var u=(n=void 0,(0,d.__values)(F.keys())),Q=u.next();!Q.done;Q=u.next()){var y=Q.value,I=1/(Math.pow(F.get(y),2)+1e-6);a+=I,o.set(y,(s=o.get(y))!==null&&s!==void 0?s:0+I)}}catch(x){n={error:x}}finally{try{Q&&!Q.done&&(c=u.return)&&c.call(u)}finally{if(n)throw n.error}}}}catch(x){t={error:x}}finally{try{g&&!g.done&&(e=B.return)&&e.call(B)}finally{if(t)throw t.error}}var h=[];try{for(var C=(0,d.__values)(o.keys()),U=C.next();!U.done;U=C.next()){var y=U.value;h.push({keySpec:y,p:o.get(y)/a})}}catch(x){l={error:x}}finally{try{U&&!U.done&&(r=C.return)&&r.call(C)}finally{if(l)throw l.error}}return h.sort(function(x,G){return G.p-x.p})}var Or=function(){function i(t,e,n){this.keySpec=n,this.centerX=n.proportionalX,this.centerY=e.proportionalY,this.width=n.proportionalWidth,this.height=t.rowProportionalHeight}return i}();function Mr(i){return i.baseKeyID?b.isKnownOSKModifierKey(i.baseKeyID)?!1:!i.isPadding:!1}function ri(i,t){return{keys:i.row.map(function(e){return e.key.map(function(n){return new Or(i,e,n)})}).reduce(function(e,n){return e.concat(n)},[]).filter(function(e){return Mr(e.keySpec)}),kbdScaleRatio:t}}var Dr=function(){function i(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}return i.prototype.toMock=function(){var t=this.left._kmwLength();return new D(this.left+(this.right||""),t)},i.ENGINE_RULE_WINDOW={leftContextCodePoints:64,rightContextCodePoints:32},i}(),Ge=Dr;var Bi=N(T(),1);var zr=function(){function i(){this._promises=new Map}return Object.defineProperty(i.prototype,"length",{get:function(){return this._promises.size},enumerable:!1,configurable:!0}),i.prototype.make=function(t,e,n){if(this._promises.has(t))return n("Existing request with token ".concat(t));this._promises.set(t,{reject:n,resolve:e})},i.prototype.keep=function(t,e){var n=this._promises.get(t);if(!n)throw new Error("No promise associated with token: ".concat(t));var c=n.resolve;return this._promises.delete(t),c(e)},i.prototype.break=function(t,e){var n=this._promises.get(t);if(!n)throw new Error("No promise associated with token: ".concat(t));this._promises.delete(t),n.reject(e)},i}(),Qt=zr;var Kr=function(){function i(t,e,n){this._worker=e,this._worker.onmessage=this.onMessage.bind(this),this._declareLMLayerReady=null,this._predictPromises=new Qt,this._wordbreakPromises=new Qt,this._acceptPromises=new Qt,this._revertPromises=new Qt,this._nextToken=Number.MIN_SAFE_INTEGER,this.sendConfig(t,!!n)}return i.prototype.sendConfig=function(t,e){this._worker.postMessage({message:"config",capabilities:t,testMode:e})},i.prototype.loadModel=function(t,e){var n=this;return e===void 0&&(e="file"),new Promise(function(c,l){n._declareLMLayerReady=c;var r={type:e};e=="file"?r.file=t:r.code=t,n._worker.postMessage({message:"load",source:r})})},i.prototype.unloadModel=function(){this._worker.postMessage({message:"unload"})},i.prototype.predict=function(t,e){var n=this,c=this._nextToken++;return new Promise(function(l,r){n._predictPromises.make(c,l,r),n._worker.postMessage({message:"predict",token:c,transform:t,context:e})})},i.prototype.wordbreak=function(t){var e=this,n=this._nextToken++;return new Promise(function(c,l){e._wordbreakPromises.make(n,c,l),e._worker.postMessage({message:"wordbreak",token:n,context:t})})},i.prototype.acceptSuggestion=function(t,e,n){var c=this,l=this._nextToken++;return new Promise(function(r,s){c._acceptPromises.make(l,r,s),c._worker.postMessage({message:"accept",token:l,suggestion:t,context:e,postTransform:n})})},i.prototype.revertSuggestion=function(t,e){var n=this,c=this._nextToken++;return new Promise(function(l,r){n._revertPromises.make(c,l,r),n._worker.postMessage({message:"revert",token:c,reversion:t,context:e})})},i.prototype.resetContext=function(t){this._worker.postMessage({message:"reset-context",context:t})},i.prototype.onMessage=function(t){var 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: ".concat(e.message))},i.prototype.shutdown=function(){this._worker.terminate()},i}(),Nn=Kr;function kt(i){return i}var si='"use strict";/*! https://mths.be/codepointat v0.2.0 by @mathias */String.prototype.codePointAt||function(){"use strict";var L=function(){try{var P={},q=Object.defineProperty,A=q(P,P,P)&&q}catch(N){}return A}(),k=function(P){if(this==null)throw TypeError();var q=String(this),A=q.length,N=P?Number(P):0;if(N!=N&&(N=0),!(N<0||N>=A)){var F=q.charCodeAt(N),R;return F>=55296&&F<=56319&&A>N+1&&(R=q.charCodeAt(N+1),R>=56320&&R<=57343)?(F-55296)*1024+R-56320+65536:F}};L?L(String.prototype,"codePointAt",{value:k,configurable:!0,writable:!0}):String.prototype.codePointAt=k}(),Array.prototype.fill||Object.defineProperty(Array.prototype,"fill",{value:function(L){if(this==null)throw new TypeError("this is null or not defined");for(var k=Object(this),P=k.length>>>0,q=arguments[1],A=q>>0,N=A<0?Math.max(P+A,0):Math.min(A,P),F=arguments[2],R=F===void 0?P:F>>0,J=R<0?Math.max(P+R,0):Math.min(R,P);N>>0;if(typeof L!="function")throw new TypeError("predicate must be a function");for(var q=arguments[1],A=0;A=0&&typeof g.length=="number"&&g.length>=0){var E=Math.floor(h.length),b=Math.floor(g.length),D=0;for(h.length=E+b;D=0&&this._nextIndex=0))return(U=new b(0)).length=0,U;for(D=Math.floor(h.length),(U=new b(D)).length=D;z0&&d[d.length-1])&&(y[0]===6||y[0]===2)){f=0;continue}if(y[0]===3&&(!d||y[1]>d[0]&&y[1]=u.length&&(u=void 0),{value:u&&u[v++],done:!u}}};throw new TypeError(c?"Object is not iterable.":"Symbol.iterator is not defined.")},l=function(u,c){var f=typeof Symbol=="function"&&u[Symbol.iterator];if(!f)return u;var v=f.call(u),p,d=[],m;try{for(;(c===void 0||c-- >0)&&!(p=v.next()).done;)d.push(p.value)}catch(C){m={error:C}}finally{try{p&&!p.done&&(f=v.return)&&f.call(v)}finally{if(m)throw m.error}}return d},o("__extends",t),o("__assign",n),o("__generator",i),o("__values",a),o("__read",l)})}}),T={};R(T,{tslib:function(){return he}}),Oe(T,me(we(),1));var he=me(we(),1);function B(){String.kmwFromCharCode=function(r){var e=[],t;for(t=0;t1114111||Math.floor(n)!==n)throw new RangeError("Invalid code point "+n);n<65536?e.push(n):(n-=65536,e.push((n>>10)+55296),e.push(n%1024+56320))}return String.fromCharCode.apply(void 0,e)},String.prototype.kmwCharCodeAt=function(r){var e=String(this),t=0;if(r<0||r>=e.length)return NaN;for(var n=0;n=55296&&i<=56319&&e.length>t+1){var a=e.charCodeAt(t+1);if(a>=56320&&a<=57343)return(i-55296<<10)+(a-56320)+65536}return i},String.prototype.kmwIndexOf=function(r,e){var t=String(this),n=t.indexOf(r,e);if(n<0)return n;for(var i=0,a=0;a!==null&&ae){var a=r;r=e,e=a}n=t.kmwCodePointToCodeUnit(r),i=t.kmwCodePointToCodeUnit(e)}return(isNaN(n)||n===null)&&(n=0),(isNaN(i)||i===null)&&(i=t.length),t.substring(n,i)},String.prototype.kmwNextChar=function(r){var e=String(this);if(r===null||r<0||r>=e.length-1)return null;var t=e.charCodeAt(r);if(t>=55296&&t<=56319&&e.length>r+1){var n=e.charCodeAt(r+1);if(n>=56320&&n<=57343)return r==e.length-2?null:r+2}return r+1},String.prototype.kmwPrevChar=function(r){var e=String(this);if(r==null||r<=0||r>e.length)return null;var t=e.charCodeAt(r-1);if(t>=56320&&t<=57343&&r>1){var n=e.charCodeAt(r-2);if(n>=55296&&n<=56319)return r-2}return r-1},String.prototype.kmwCodePointToCodeUnit=function(r){if(r===null)return null;var e=String(this),t=0;if(r<0){t=e.length;for(var n=0;n>r;n--)t=e.kmwPrevChar(t);return t}if(r==e.kmwLength())return e.length;for(var n=0;n=0?e.kmwSubstr(r,1):""},String.prototype.kmwBMPNextChar=function(r){var e=String(this);return r<0||r>=e.length-1?null:r+1},String.prototype.kmwBMPPrevChar=function(r){var e=String(this);return r<=0||r>e.length?null:r-1},String.prototype.kmwBMPCodePointToCodeUnit=function(r){return r},String.prototype.kmwBMPCodeUnitToCodePoint=function(r){return r},String.prototype.kmwBMPLength=function(){var r=String(this);return r.length},String.prototype.kmwBMPSubstr=function(r,e){var t=String(this);return r>-1?t.substr(r,e):t.substr(t.length+r,-r)},String.kmwEnableSupplementaryPlane=function(r){var e=String.prototype;String._kmwFromCharCode=r?String.kmwFromCharCode:String.fromCharCode,e._kmwCharAt=r?e.kmwCharAt:e.charAt,e._kmwCharCodeAt=r?e.kmwCharCodeAt:e.charCodeAt,e._kmwIndexOf=r?e.kmwIndexOf:e.indexOf,e._kmwLastIndexOf=r?e.kmwLastIndexOf:e.lastIndexOf,e._kmwSlice=r?e.kmwSlice:e.slice,e._kmwSubstring=r?e.kmwSubstring:e.substring,e._kmwSubstr=r?e.kmwSubstr:e.kmwBMPSubstr,e._kmwLength=r?e.kmwLength:e.kmwBMPLength,e._kmwNextChar=r?e.kmwNextChar:e.kmwBMPNextChar,e._kmwPrevChar=r?e.kmwPrevChar:e.kmwBMPPrevChar,e._kmwCodePointToCodeUnit=r?e.kmwCodePointToCodeUnit:e.kmwBMPCodePointToCodeUnit,e._kmwCodeUnitToCodePoint=r?e.kmwCodeUnitToCodePoint:e.kmwBMPCodeUnitToCodePoint},String._kmwFromCharCode||String.kmwEnableSupplementaryPlane(!1)}B();var G={};R(G,{DummyModel:function(){return vt},PriorityQueue:function(){return b},QuoteBehavior:function(){return U},SENTINEL_CODE_UNIT:function(){return Q},TrieModel:function(){return lt},applyTransform:function(){return H},buildMergedTransform:function(){return le},defaultApplyCasing:function(){return g},getLastPreCaretToken:function(){return pe},isHighSurrogate:function(){return $},isLowSurrogate:function(){return M},isSentinel:function(){return X},tokenize:function(){return z},transformToSuggestion:function(){return h},wordbreak:function(){return Ae}}),B();var Q="\\uFDD0";function H(r,e){var t,n,i=e.left||"",a=i.kmwLength(),l=a=55296&&r<=56319}function M(r){return typeof r=="string"&&(r=r.charCodeAt(0)),r>=56320&&r<=57343}function X(r){return r==Q}function h(r,e){var t={transform:r,transformId:r.id,displayAs:r.insert};return(e===0||e)&&(t.p=e),t}function g(r,e){switch(r){case"lower":return e.toLowerCase();case"upper":return e.toUpperCase();case"initial":var t=1;return e.length>1&&$(e.charAt(0))&&M(e.charCodeAt(1))&&(t=2),e.substring(0,t).toUpperCase().concat(e.substring(t))}}var E=function(){function r(e,t){if(typeof e!="function"){this.comparator=e.comparator,this.heap=[].concat(e.heap);return}var n=e;this.comparator=n,this.heap=(t!=null?t:[]).slice(0),this.heapify()}return r.leftChildIndex=function(e){return e*2+1},r.rightChildIndex=function(e){return e*2+2},r.parentIndex=function(e){return Math.floor((e-1)/2)},r.prototype.heapify=function(e,t){if(e==null||t==null){this.heapify(0,this.count-1);return}for(var n=[],i=-1,a=t;a>=e;a--){var l=r.parentIndex(a);this.siftDown(a)&&l0;){var o=n.shift(),l=r.parentIndex(o);this.siftDown(o)&&l>=0&&i!=l&&(n.push(l),i=l)}},Object.defineProperty(r.prototype,"count",{get:function(){return this.heap.length},enumerable:!1,configurable:!0}),r.prototype.peek=function(){return this.heap[0]},r.prototype.enqueue=function(e){var t=this.heap.length;this.heap.push(e);for(var n=r.parentIndex,i=n(t);t!==0&&this.comparator(this.heap[t],this.heap[i])<0;){var a=this.heap[t];this.heap[t]=this.heap[i],this.heap[i]=a,t=i,i=n(t)}},r.prototype.enqueueAll=function(e){if(e.length!=0){var t=this.count;this.heap=this.heap.concat(e);var n=r.parentIndex(t);this.heapify(n>=0?n:0,r.parentIndex(this.count-1))}},r.prototype.dequeue=function(){if(this.count!=0){var e=this.heap[0],t=this.heap.pop();return this.heap.length>0&&(this.heap[0]=t,this.siftDown(0)),e}},r.prototype.siftDown=function(e){var t=r.leftChildIndex(e),n=r.rightChildIndex(e),i=e;if(t0&&(i=t[t.length-1]),t.length>1){var a=t[t.length-2];if(a.end==i.start&&i.text=="\'"){var l={text:a.text+i.text,start:a.start,end:i.end,length:a.length+i.length};t.pop(),t.pop(),t.push(l),i=l}}var o={left:t.map(function(f){return f.text}),right:n.map(function(f){return f.text}),caretSplitsToken:!1};if(t.length>0&&n.length>0){var s=n[0],u=i.end!=e.left.length,c=s.start!=0;if(u||c)return o;r(i.text+s.text).length==1&&(o.caretSplitsToken=!0)}return o}function pe(r,e){var t=z(r,e);return t.left.length>0?t.left.pop():""}function Ae(r,e){return pe(r,e)}var Pe={};R(Pe,{ascii:function(){return tt},default:function(){return ce},defaultWordbreaker:function(){return ce},placeholder:function(){return et}});function et(r){var e=0;return r.split(/\\s+/).map(function(t){var n={start:e,end:e+t.length,text:t,length:t.length};return e=n.end,n})}function tt(r){for(var e=/[A-Za-z0-9\']+/g,t=[],n;(n=e.exec(r))!==null;)t.push(new rt(n[0],n.index));return t}var rt=function(){function r(e,t){this.text=e,this.start=t}return Object.defineProperty(r.prototype,"length",{get:function(){return this.text.length},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"end",{get:function(){return this.start+this.text.length},enumerable:!1,configurable:!0}),r}(),nt=["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"],Me=[[0,0],[10,1],[11,2],[13,3],[14,0],[32,4],[33,0],[34,5],[35,0],[39,6],[40,0],[44,7],[45,0],[46,8],[47,0],[48,9],[58,10],[59,7],[60,0],[65,11],[91,0],[95,12],[96,0],[97,11],[123,0],[133,2],[134,0],[170,11],[171,0],[173,13],[174,0],[181,11],[182,0],[183,10],[184,0],[186,11],[187,0],[192,11],[215,0],[216,11],[247,0],[248,11],[728,0],[734,11],[768,14],[880,11],[885,0],[886,11],[888,0],[890,11],[894,7],[895,11],[896,0],[902,11],[903,10],[904,11],[907,0],[908,11],[909,0],[910,11],[930,0],[931,11],[1014,0],[1015,11],[1154,0],[1155,14],[1162,11],[1328,0],[1329,11],[1367,0],[1369,11],[1373,0],[1374,11],[1375,10],[1376,11],[1417,7],[1418,11],[1419,0],[1425,14],[1470,0],[1471,14],[1472,0],[1473,14],[1475,0],[1476,14],[1478,0],[1479,14],[1480,0],[1488,15],[1515,0],[1519,15],[1523,11],[1524,10],[1525,0],[1536,13],[1542,0],[1548,7],[1550,0],[1552,14],[1563,0],[1564,13],[1565,0],[1568,11],[1611,14],[1632,9],[1642,0],[1643,9],[1644,7],[1645,0],[1646,11],[1648,14],[1649,11],[1748,0],[1749,11],[1750,14],[1757,13],[1758,0],[1759,14],[1765,11],[1767,14],[1769,0],[1770,14],[1774,11],[1776,9],[1786,11],[1789,0],[1791,11],[1792,0],[1807,13],[1808,11],[1809,14],[1810,11],[1840,14],[1867,0],[1869,11],[1958,14],[1969,11],[1970,0],[1984,9],[1994,11],[2027,14],[2036,11],[2038,0],[2040,7],[2041,0],[2042,11],[2043,0],[2045,14],[2046,0],[2048,11],[2070,14],[2074,11],[2075,14],[2084,11],[2085,14],[2088,11],[2089,14],[2094,0],[2112,11],[2137,14],[2140,0],[2144,11],[2155,0],[2208,11],[2229,0],[2230,11],[2248,0],[2259,14],[2274,13],[2275,14],[2308,11],[2362,14],[2365,11],[2366,14],[2384,11],[2385,14],[2392,11],[2402,14],[2404,0],[2406,9],[2416,0],[2417,11],[2433,14],[2436,0],[2437,11],[2445,0],[2447,11],[2449,0],[2451,11],[2473,0],[2474,11],[2481,0],[2482,11],[2483,0],[2486,11],[2490,0],[2492,14],[2493,11],[2494,14],[2501,0],[2503,14],[2505,0],[2507,14],[2510,11],[2511,0],[2519,14],[2520,0],[2524,11],[2526,0],[2527,11],[2530,14],[2532,0],[2534,9],[2544,11],[2546,0],[2556,11],[2557,0],[2558,14],[2559,0],[2561,14],[2564,0],[2565,11],[2571,0],[2575,11],[2577,0],[2579,11],[2601,0],[2602,11],[2609,0],[2610,11],[2612,0],[2613,11],[2615,0],[2616,11],[2618,0],[2620,14],[2621,0],[2622,14],[2627,0],[2631,14],[2633,0],[2635,14],[2638,0],[2641,14],[2642,0],[2649,11],[2653,0],[2654,11],[2655,0],[2662,9],[2672,14],[2674,11],[2677,14],[2678,0],[2689,14],[2692,0],[2693,11],[2702,0],[2703,11],[2706,0],[2707,11],[2729,0],[2730,11],[2737,0],[2738,11],[2740,0],[2741,11],[2746,0],[2748,14],[2749,11],[2750,14],[2758,0],[2759,14],[2762,0],[2763,14],[2766,0],[2768,11],[2769,0],[2784,11],[2786,14],[2788,0],[2790,9],[2800,0],[2809,11],[2810,14],[2816,0],[2817,14],[2820,0],[2821,11],[2829,0],[2831,11],[2833,0],[2835,11],[2857,0],[2858,11],[2865,0],[2866,11],[2868,0],[2869,11],[2874,0],[2876,14],[2877,11],[2878,14],[2885,0],[2887,14],[2889,0],[2891,14],[2894,0],[2901,14],[2904,0],[2908,11],[2910,0],[2911,11],[2914,14],[2916,0],[2918,9],[2928,0],[2929,11],[2930,0],[2946,14],[2947,11],[2948,0],[2949,11],[2955,0],[2958,11],[2961,0],[2962,11],[2966,0],[2969,11],[2971,0],[2972,11],[2973,0],[2974,11],[2976,0],[2979,11],[2981,0],[2984,11],[2987,0],[2990,11],[3002,0],[3006,14],[3011,0],[3014,14],[3017,0],[3018,14],[3022,0],[3024,11],[3025,0],[3031,14],[3032,0],[3046,9],[3056,0],[3072,14],[3077,11],[3085,0],[3086,11],[3089,0],[3090,11],[3113,0],[3114,11],[3130,0],[3133,11],[3134,14],[3141,0],[3142,14],[3145,0],[3146,14],[3150,0],[3157,14],[3159,0],[3160,11],[3163,0],[3168,11],[3170,14],[3172,0],[3174,9],[3184,0],[3200,11],[3201,14],[3204,0],[3205,11],[3213,0],[3214,11],[3217,0],[3218,11],[3241,0],[3242,11],[3252,0],[3253,11],[3258,0],[3260,14],[3261,11],[3262,14],[3269,0],[3270,14],[3273,0],[3274,14],[3278,0],[3285,14],[3287,0],[3294,11],[3295,0],[3296,11],[3298,14],[3300,0],[3302,9],[3312,0],[3313,11],[3315,0],[3328,14],[3332,11],[3341,0],[3342,11],[3345,0],[3346,11],[3387,14],[3389,11],[3390,14],[3397,0],[3398,14],[3401,0],[3402,14],[3406,11],[3407,0],[3412,11],[3415,14],[3416,0],[3423,11],[3426,14],[3428,0],[3430,9],[3440,0],[3450,11],[3456,0],[3457,14],[3460,0],[3461,11],[3479,0],[3482,11],[3506,0],[3507,11],[3516,0],[3517,11],[3518,0],[3520,11],[3527,0],[3530,14],[3531,0],[3535,14],[3541,0],[3542,14],[3543,0],[3544,14],[3552,0],[3558,9],[3568,0],[3570,14],[3572,0],[3633,14],[3634,0],[3636,14],[3643,0],[3655,14],[3663,0],[3664,9],[3674,0],[3761,14],[3762,0],[3764,14],[3773,0],[3784,14],[3790,0],[3792,9],[3802,0],[3840,11],[3841,0],[3864,14],[3866,0],[3872,9],[3882,0],[3893,14],[3894,0],[3895,14],[3896,0],[3897,14],[3898,0],[3902,14],[3904,11],[3912,0],[3913,11],[3949,0],[3953,14],[3973,0],[3974,14],[3976,11],[3981,14],[3992,0],[3993,14],[4029,0],[4038,14],[4039,0],[4139,14],[4159,0],[4160,9],[4170,0],[4182,14],[4186,0],[4190,14],[4193,0],[4194,14],[4197,0],[4199,14],[4206,0],[4209,14],[4213,0],[4226,14],[4238,0],[4239,14],[4240,9],[4250,14],[4254,0],[4256,11],[4294,0],[4295,11],[4296,0],[4301,11],[4302,0],[4304,11],[4347,0],[4348,11],[4681,0],[4682,11],[4686,0],[4688,11],[4695,0],[4696,11],[4697,0],[4698,11],[4702,0],[4704,11],[4745,0],[4746,11],[4750,0],[4752,11],[4785,0],[4786,11],[4790,0],[4792,11],[4799,0],[4800,11],[4801,0],[4802,11],[4806,0],[4808,11],[4823,0],[4824,11],[4881,0],[4882,11],[4886,0],[4888,11],[4955,0],[4957,14],[4960,0],[4992,11],[5008,0],[5024,11],[5110,0],[5112,11],[5118,0],[5121,11],[5741,0],[5743,11],[5760,4],[5761,11],[5787,0],[5792,11],[5867,0],[5870,11],[5881,0],[5888,11],[5901,0],[5902,11],[5906,14],[5909,0],[5920,11],[5938,14],[5941,0],[5952,11],[5970,14],[5972,0],[5984,11],[5997,0],[5998,11],[6001,0],[6002,14],[6004,0],[6068,14],[6100,0],[6109,14],[6110,0],[6112,9],[6122,0],[6155,14],[6158,13],[6159,0],[6160,9],[6170,0],[6176,11],[6265,0],[6272,11],[6277,14],[6279,11],[6313,14],[6314,11],[6315,0],[6320,11],[6390,0],[6400,11],[6431,0],[6432,14],[6444,0],[6448,14],[6460,0],[6470,9],[6480,0],[6608,9],[6618,0],[6656,11],[6679,14],[6684,0],[6741,14],[6751,0],[6752,14],[6781,0],[6783,14],[6784,9],[6794,0],[6800,9],[6810,0],[6832,14],[6849,0],[6912,14],[6917,11],[6964,14],[6981,11],[6988,0],[6992,9],[7002,0],[7019,14],[7028,0],[7040,14],[7043,11],[7073,14],[7086,11],[7088,9],[7098,11],[7142,14],[7156,0],[7168,11],[7204,14],[7224,0],[7232,9],[7242,0],[7245,11],[7248,9],[7258,11],[7294,0],[7296,11],[7305,0],[7312,11],[7355,0],[7357,11],[7360,0],[7376,14],[7379,0],[7380,14],[7401,11],[7405,14],[7406,11],[7412,14],[7413,11],[7415,14],[7418,11],[7419,0],[7424,11],[7616,14],[7674,0],[7675,14],[7680,11],[7958,0],[7960,11],[7966,0],[7968,11],[8006,0],[8008,11],[8014,0],[8016,11],[8024,0],[8025,11],[8026,0],[8027,11],[8028,0],[8029,11],[8030,0],[8031,11],[8062,0],[8064,11],[8117,0],[8118,11],[8125,0],[8126,11],[8127,0],[8130,11],[8133,0],[8134,11],[8141,0],[8144,11],[8148,0],[8150,11],[8156,0],[8160,11],[8173,0],[8178,11],[8181,0],[8182,11],[8189,0],[8192,4],[8199,0],[8200,4],[8203,0],[8204,14],[8205,16],[8206,13],[8208,0],[8216,8],[8218,0],[8228,8],[8229,0],[8231,10],[8232,2],[8234,13],[8239,12],[8240,0],[8255,12],[8257,0],[8260,7],[8261,0],[8276,12],[8277,0],[8287,4],[8288,13],[8293,0],[8294,13],[8304,0],[8305,11],[8306,0],[8319,11],[8320,0],[8336,11],[8349,0],[8400,14],[8433,0],[8450,11],[8451,0],[8455,11],[8456,0],[8458,11],[8468,0],[8469,11],[8470,0],[8473,11],[8478,0],[8484,11],[8485,0],[8486,11],[8487,0],[8488,11],[8489,0],[8490,11],[8494,0],[8495,11],[8506,0],[8508,11],[8512,0],[8517,11],[8522,0],[8526,11],[8527,0],[8544,11],[8585,0],[9398,11],[9450,0],[11264,11],[11311,0],[11312,11],[11359,0],[11360,11],[11493,0],[11499,11],[11503,14],[11506,11],[11508,0],[11520,11],[11558,0],[11559,11],[11560,0],[11565,11],[11566,0],[11568,11],[11624,0],[11631,11],[11632,0],[11647,14],[11648,11],[11671,0],[11680,11],[11687,0],[11688,11],[11695,0],[11696,11],[11703,0],[11704,11],[11711,0],[11712,11],[11719,0],[11720,11],[11727,0],[11728,11],[11735,0],[11736,11],[11743,0],[11744,14],[11776,0],[11823,11],[11824,0],[12288,4],[12289,0],[12293,11],[12294,0],[12330,14],[12336,0],[12337,17],[12342,0],[12347,11],[12349,0],[12441,14],[12443,17],[12445,0],[12448,17],[12539,0],[12540,17],[12544,0],[12549,11],[12592,0],[12593,11],[12687,0],[12704,11],[12736,0],[12784,17],[12800,0],[13008,17],[13055,0],[13056,17],[13144,0],[40960,11],[42125,0],[42192,11],[42238,0],[42240,11],[42509,0],[42512,11],[42528,9],[42538,11],[42540,0],[42560,11],[42607,14],[42611,0],[42612,14],[42622,0],[42623,11],[42654,14],[42656,11],[42736,14],[42738,0],[42760,11],[42944,0],[42946,11],[42955,0],[42997,11],[43010,14],[43011,11],[43014,14],[43015,11],[43019,14],[43020,11],[43043,14],[43048,0],[43052,14],[43053,0],[43072,11],[43124,0],[43136,14],[43138,11],[43188,14],[43206,0],[43216,9],[43226,0],[43232,14],[43250,11],[43256,0],[43259,11],[43260,0],[43261,11],[43263,14],[43264,9],[43274,11],[43302,14],[43310,0],[43312,11],[43335,14],[43348,0],[43360,11],[43389,0],[43392,14],[43396,11],[43443,14],[43457,0],[43471,11],[43472,9],[43482,0],[43493,14],[43494,0],[43504,9],[43514,0],[43520,11],[43561,14],[43575,0],[43584,11],[43587,14],[43588,11],[43596,14],[43598,0],[43600,9],[43610,0],[43643,14],[43646,0],[43696,14],[43697,0],[43698,14],[43701,0],[43703,14],[43705,0],[43710,14],[43712,0],[43713,14],[43714,0],[43744,11],[43755,14],[43760,0],[43762,11],[43765,14],[43767,0],[43777,11],[43783,0],[43785,11],[43791,0],[43793,11],[43799,0],[43808,11],[43815,0],[43816,11],[43823,0],[43824,11],[43882,0],[43888,11],[44003,14],[44011,0],[44012,14],[44014,0],[44016,9],[44026,0],[44032,11],[55204,0],[55216,11],[55239,0],[55243,11],[55292,0],[64256,11],[64263,0],[64275,11],[64280,0],[64285,15],[64286,14],[64287,15],[64297,0],[64298,15],[64311,0],[64312,15],[64317,0],[64318,15],[64319,0],[64320,15],[64322,0],[64323,15],[64325,0],[64326,15],[64336,11],[64434,0],[64467,11],[64830,0],[64848,11],[64912,0],[64914,11],[64968,0],[65008,11],[65020,0],[65024,14],[65040,7],[65041,0],[65043,10],[65044,7],[65045,0],[65056,14],[65072,0],[65075,12],[65077,0],[65101,12],[65104,7],[65105,0],[65106,8],[65107,0],[65108,7],[65109,10],[65110,0],[65136,11],[65141,0],[65142,11],[65277,0],[65279,13],[65280,0],[65287,8],[65288,0],[65292,7],[65293,0],[65294,8],[65295,0],[65296,9],[65306,10],[65307,7],[65308,0],[65313,11],[65339,0],[65343,12],[65344,0],[65345,11],[65371,0],[65382,17],[65438,14],[65440,11],[65471,0],[65474,11],[65480,0],[65482,11],[65488,0],[65490,11],[65496,0],[65498,11],[65501,0],[65529,13],[65532,0],[65536,11],[65548,0],[65549,11],[65575,0],[65576,11],[65595,0],[65596,11],[65598,0],[65599,11],[65614,0],[65616,11],[65630,0],[65664,11],[65787,0],[65856,11],[65909,0],[66045,14],[66046,0],[66176,11],[66205,0],[66208,11],[66257,0],[66272,14],[66273,0],[66304,11],[66336,0],[66349,11],[66379,0],[66384,11],[66422,14],[66427,0],[66432,11],[66462,0],[66464,11],[66500,0],[66504,11],[66512,0],[66513,11],[66518,0],[66560,11],[66718,0],[66720,9],[66730,0],[66736,11],[66772,0],[66776,11],[66812,0],[66816,11],[66856,0],[66864,11],[66916,0],[67072,11],[67383,0],[67392,11],[67414,0],[67424,11],[67432,0],[67584,11],[67590,0],[67592,11],[67593,0],[67594,11],[67638,0],[67639,11],[67641,0],[67644,11],[67645,0],[67647,11],[67670,0],[67680,11],[67703,0],[67712,11],[67743,0],[67808,11],[67827,0],[67828,11],[67830,0],[67840,11],[67862,0],[67872,11],[67898,0],[67968,11],[68024,0],[68030,11],[68032,0],[68096,11],[68097,14],[68100,0],[68101,14],[68103,0],[68108,14],[68112,11],[68116,0],[68117,11],[68120,0],[68121,11],[68150,0],[68152,14],[68155,0],[68159,14],[68160,0],[68192,11],[68221,0],[68224,11],[68253,0],[68288,11],[68296,0],[68297,11],[68325,14],[68327,0],[68352,11],[68406,0],[68416,11],[68438,0],[68448,11],[68467,0],[68480,11],[68498,0],[68608,11],[68681,0],[68736,11],[68787,0],[68800,11],[68851,0],[68864,11],[68900,14],[68904,0],[68912,9],[68922,0],[69248,11],[69290,0],[69291,14],[69293,0],[69296,11],[69298,0],[69376,11],[69405,0],[69415,11],[69416,0],[69424,11],[69446,14],[69457,0],[69552,11],[69573,0],[69600,11],[69623,0],[69632,14],[69635,11],[69688,14],[69703,0],[69734,9],[69744,0],[69759,14],[69763,11],[69808,14],[69819,0],[69821,13],[69822,0],[69837,13],[69838,0],[69840,11],[69865,0],[69872,9],[69882,0],[69888,14],[69891,11],[69927,14],[69941,0],[69942,9],[69952,0],[69956,11],[69957,14],[69959,11],[69960,0],[69968,11],[70003,14],[70004,0],[70006,11],[70007,0],[70016,14],[70019,11],[70067,14],[70081,11],[70085,0],[70089,14],[70093,0],[70094,14],[70096,9],[70106,11],[70107,0],[70108,11],[70109,0],[70144,11],[70162,0],[70163,11],[70188,14],[70200,0],[70206,14],[70207,0],[70272,11],[70279,0],[70280,11],[70281,0],[70282,11],[70286,0],[70287,11],[70302,0],[70303,11],[70313,0],[70320,11],[70367,14],[70379,0],[70384,9],[70394,0],[70400,14],[70404,0],[70405,11],[70413,0],[70415,11],[70417,0],[70419,11],[70441,0],[70442,11],[70449,0],[70450,11],[70452,0],[70453,11],[70458,0],[70459,14],[70461,11],[70462,14],[70469,0],[70471,14],[70473,0],[70475,14],[70478,0],[70480,11],[70481,0],[70487,14],[70488,0],[70493,11],[70498,14],[70500,0],[70502,14],[70509,0],[70512,14],[70517,0],[70656,11],[70709,14],[70727,11],[70731,0],[70736,9],[70746,0],[70750,14],[70751,11],[70754,0],[70784,11],[70832,14],[70852,11],[70854,0],[70855,11],[70856,0],[70864,9],[70874,0],[71040,11],[71087,14],[71094,0],[71096,14],[71105,0],[71128,11],[71132,14],[71134,0],[71168,11],[71216,14],[71233,0],[71236,11],[71237,0],[71248,9],[71258,0],[71296,11],[71339,14],[71352,11],[71353,0],[71360,9],[71370,0],[71453,14],[71468,0],[71472,9],[71482,0],[71680,11],[71724,14],[71739,0],[71840,11],[71904,9],[71914,0],[71935,11],[71943,0],[71945,11],[71946,0],[71948,11],[71956,0],[71957,11],[71959,0],[71960,11],[71984,14],[71990,0],[71991,14],[71993,0],[71995,14],[71999,11],[72e3,14],[72001,11],[72002,14],[72004,0],[72016,9],[72026,0],[72096,11],[72104,0],[72106,11],[72145,14],[72152,0],[72154,14],[72161,11],[72162,0],[72163,11],[72164,14],[72165,0],[72192,11],[72193,14],[72203,11],[72243,14],[72250,11],[72251,14],[72255,0],[72263,14],[72264,0],[72272,11],[72273,14],[72284,11],[72330,14],[72346,0],[72349,11],[72350,0],[72384,11],[72441,0],[72704,11],[72713,0],[72714,11],[72751,14],[72759,0],[72760,14],[72768,11],[72769,0],[72784,9],[72794,0],[72818,11],[72848,0],[72850,14],[72872,0],[72873,14],[72887,0],[72960,11],[72967,0],[72968,11],[72970,0],[72971,11],[73009,14],[73015,0],[73018,14],[73019,0],[73020,14],[73022,0],[73023,14],[73030,11],[73031,14],[73032,0],[73040,9],[73050,0],[73056,11],[73062,0],[73063,11],[73065,0],[73066,11],[73098,14],[73103,0],[73104,14],[73106,0],[73107,14],[73112,11],[73113,0],[73120,9],[73130,0],[73440,11],[73459,14],[73463,0],[73648,11],[73649,0],[73728,11],[74650,0],[74752,11],[74863,0],[74880,11],[75076,0],[77824,11],[78895,0],[78896,13],[78905,0],[82944,11],[83527,0],[92160,11],[92729,0],[92736,11],[92767,0],[92768,9],[92778,0],[92880,11],[92910,0],[92912,14],[92917,0],[92928,11],[92976,14],[92983,0],[92992,11],[92996,0],[93008,9],[93018,0],[93027,11],[93048,0],[93053,11],[93072,0],[93760,11],[93824,0],[93952,11],[94027,0],[94031,14],[94032,11],[94033,14],[94088,0],[94095,14],[94099,11],[94112,0],[94176,11],[94178,0],[94179,11],[94180,14],[94181,0],[94192,14],[94194,0],[110592,17],[110593,0],[110948,17],[110952,0],[113664,11],[113771,0],[113776,11],[113789,0],[113792,11],[113801,0],[113808,11],[113818,0],[113821,14],[113823,0],[113824,13],[113828,0],[119141,14],[119146,0],[119149,14],[119155,13],[119163,14],[119171,0],[119173,14],[119180,0],[119210,14],[119214,0],[119362,14],[119365,0],[119808,11],[119893,0],[119894,11],[119965,0],[119966,11],[119968,0],[119970,11],[119971,0],[119973,11],[119975,0],[119977,11],[119981,0],[119982,11],[119994,0],[119995,11],[119996,0],[119997,11],[120004,0],[120005,11],[120070,0],[120071,11],[120075,0],[120077,11],[120085,0],[120086,11],[120093,0],[120094,11],[120122,0],[120123,11],[120127,0],[120128,11],[120133,0],[120134,11],[120135,0],[120138,11],[120145,0],[120146,11],[120486,0],[120488,11],[120513,0],[120514,11],[120539,0],[120540,11],[120571,0],[120572,11],[120597,0],[120598,11],[120629,0],[120630,11],[120655,0],[120656,11],[120687,0],[120688,11],[120713,0],[120714,11],[120745,0],[120746,11],[120771,0],[120772,11],[120780,0],[120782,9],[120832,0],[121344,14],[121399,0],[121403,14],[121453,0],[121461,14],[121462,0],[121476,14],[121477,0],[121499,14],[121504,0],[121505,14],[121520,0],[122880,14],[122887,0],[122888,14],[122905,0],[122907,14],[122914,0],[122915,14],[122917,0],[122918,14],[122923,0],[123136,11],[123181,0],[123184,14],[123191,11],[123198,0],[123200,9],[123210,0],[123214,11],[123215,0],[123584,11],[123628,14],[123632,9],[123642,0],[124928,11],[125125,0],[125136,14],[125143,0],[125184,11],[125252,14],[125259,11],[125260,0],[125264,9],[125274,0],[126464,11],[126468,0],[126469,11],[126496,0],[126497,11],[126499,0],[126500,11],[126501,0],[126503,11],[126504,0],[126505,11],[126515,0],[126516,11],[126520,0],[126521,11],[126522,0],[126523,11],[126524,0],[126530,11],[126531,0],[126535,11],[126536,0],[126537,11],[126538,0],[126539,11],[126540,0],[126541,11],[126544,0],[126545,11],[126547,0],[126548,11],[126549,0],[126551,11],[126552,0],[126553,11],[126554,0],[126555,11],[126556,0],[126557,11],[126558,0],[126559,11],[126560,0],[126561,11],[126563,0],[126564,11],[126565,0],[126567,11],[126571,0],[126572,11],[126579,0],[126580,11],[126584,0],[126585,11],[126589,0],[126590,11],[126591,0],[126592,11],[126602,0],[126603,11],[126620,0],[126625,11],[126628,0],[126629,11],[126634,0],[126635,11],[126652,0],[127280,11],[127306,0],[127312,11],[127338,0],[127344,11],[127370,0],[127462,18],[127488,0],[127995,14],[128e3,0],[130032,9],[130042,0],[917505,13],[917506,0],[917536,14],[917632,0],[917760,14],[918e3,0]];function ce(r,e){var t=ot(r,e);if(t.length==0)return[];for(var n=[],i=0;i=this.text.length?20:We(this.text[e])?Ee(this.text[e]+this.text[e+1]):Ee(this.text[e],this.options)},r.prototype.match=function(e,t,n,i){var a,l,o,s,u=(a=e==null?void 0:e.includes(this.lookbehind))!==null&&a!==void 0?a:!0;return u=u&&((l=t==null?void 0:t.includes(this.left))!==null&&l!==void 0?l:!0),u=u&&((o=n==null?void 0:n.includes(this.right))!==null&&o!==void 0?o:!0),u&&((s=i==null?void 0:i.includes(this.lookahead))!==null&&s!==void 0?s:!0)},r.prototype.propertyMatch=function(e,t,n,i){var a=this,l=function(o){return Be(o,a.options)};return this.match(e==null?void 0:e.map(l),t==null?void 0:t.map(l),n==null?void 0:n.map(l),i==null?void 0:i.map(l))},r}();function at(r,e){return!Array.from(r).map(function(t){return Ee(t,e)}).every(function(t){return t===3||t===1||t===2||t===4})}function ot(r,e){var t,n,i;if(r.length===0)return[];e&&!e.rules&&(e.rules=[]);var a=[],l,o=0,s=new it(r,e,o),u=0;do{if(l=o,o=_(o),s=s.next(o),s.match(null,[19],null,null)){a.push(l);continue}if(s.match(null,null,[20],null)){a.push(l);break}if(!s.match(null,[3],[1],null)){var c=[2,3,1];if(s.match(null,c,null,null)){a.push(l);continue}if(s.match(null,null,c,null)){a.push(l);continue}if(!s.match(null,[4],[4],null)){for(var f=[13,14,16];s.match(null,null,f,null);)t=(0,T.__read)([o,_(o)],2),l=t[0],o=t[1],s=s.ignoringRight(o);if(s.right===20){a.push(l);break}for(;s.match(null,null,null,f);)o=_(o),s=s.ignoringLookahead(o);var v=[11,15],p=[8,6];if(e!=null&&e.rules){var d=!1;try{for(var m=(n=void 0,(0,T.__values)(e.rules)),C=m.next();!C.done;C=m.next()){var x=C.value;if(d=x.match(s),d){x.breakIfMatch&&a.push(l);break}}}catch(I){n={error:I}}finally{try{C&&!C.done&&(i=m.return)&&i.call(m)}finally{if(n)throw n.error}}if(d)continue}if(!s.match(null,v,v,null)){var y=[10].concat(p);if(!s.match(null,v,y,v)&&!s.match(v,y,v,null)&&!s.match(null,[15],[6],null)&&!s.match(null,[15],[5],[15])&&!s.match([15],[5],[15],null)&&!s.match(null,[9],[9],null)&&!s.match(null,v,[9],null)&&!s.match(null,[9],v,null)){var S=[7].concat(p);if(!s.match([9],S,[9],null)&&!s.match(null,[9],S,[9])&&!s.match(null,[17],[17],null)){var w=[17,9].concat(v);if(!s.match(null,w,[12],null)&&!s.match(null,[12],[12],null)&&!s.match(null,[12],w,null)){if(s.right===18){if(u+=1,u%2==1)continue}else u=0;a.push(l)}}}}}}}while(l=r.length?r.length:We(r[I])?I+2:I+1}}function We(r){var e=r.charCodeAt(0);return e>=55296&&e<=56319}function Ee(r,e){if(e!=null&&e.propertyMapping){var t=e.propertyMapping(r);if(t)return Be(t,e)}var n=r.codePointAt(0);return Ie(n,0,Me.length-1)}function Be(r,e){var t,n,i=function(l){return l.toLowerCase()==r.toLowerCase()},a=(n=(t=e==null?void 0:e.customProperties)===null||t===void 0?void 0:t.findIndex(i))!==null&&n!==void 0?n:-1;return a!=-1?-a-1:nt.findIndex(i)}function Ie(r,e,t){if(t=l?Ie(r,n+1,t):i[1]}B();var Qe=12,ut=function(){function r(e,t){this.root=e,this.prefix=t}return r.prototype.children=function(){var e,t,n,i,a,l,o,s,u,c,f,v,p,d,m,C,x,y;return(0,T.__generator)(this,function(S){switch(S.label){case 0:if(e=this.root,e.type!="internal")return[3,9];t=function(w){var _,I,Y,ne,Z,be,ke,ie,_e,ae,K,ve;return(0,T.__generator)(this,function(W){switch(W.label){case 0:if(_=e.children[w],!$(w))return[3,12];if(_.type!="internal")return[3,9];I=_,Y=function(oe){var ue;return(0,T.__generator)(this,function(se){switch(se.label){case 0:return ue=n.prefix+w+oe,[4,{char:w+oe,traversal:function(){return new r(I.children[oe],ue)}}];case 1:return se.sent(),[2]}})},W.label=1;case 1:W.trys.push([1,6,7,8]),ne=(K=void 0,(0,T.__values)(I.values)),Z=ne.next(),W.label=2;case 2:return Z.done?[3,5]:(be=Z.value,[5,Y(be)]);case 3:W.sent(),W.label=4;case 4:return Z=ne.next(),[3,2];case 5:return[3,8];case 6:return ke=W.sent(),K={error:ke},[3,8];case 7:try{Z&&!Z.done&&(ve=ne.return)&&ve.call(ne)}finally{if(K)throw K.error}return[7];case 8:return[3,11];case 9:return ie=_.entries[0].key,w=w+ie[n.prefix.length+1],_e=n.prefix+w,[4,{char:w,traversal:function(){return new r(_,_e)}}];case 10:W.sent(),W.label=11;case 11:return[3,16];case 12:return X(w)?[2,"continue"]:[3,13];case 13:return w?[3,14]:[2,"continue"];case 14:return ae=n.prefix+w,[4,{char:w,traversal:function(){return new r(_,ae)}}];case 15:W.sent(),W.label=16;case 16:return[2]}})},n=this,S.label=1;case 1:S.trys.push([1,6,7,8]),i=(0,T.__values)(e.values),a=i.next(),S.label=2;case 2:return a.done?[3,5]:(l=a.value,[5,t(l)]);case 3:S.sent(),S.label=4;case 4:return a=i.next(),[3,2];case 5:return[3,8];case 6:return o=S.sent(),m={error:o},[3,8];case 7:try{a&&!a.done&&(C=i.return)&&C.call(i)}finally{if(m)throw m.error}return[7];case 8:return[2];case 9:s=this.prefix,u=e.entries.filter(function(w){return w.key!=s&&s.length=n)return o}}}catch(C){i={error:C}}finally{try{u&&!u.done&&(a=s.return)&&a.call(s)}finally{if(i)throw i.error}}else{l.enqueue(r);for(var p=void 0,d=function(){if(ft(p))if(p.type==="leaf")l.enqueueAll(p.entries);else{var C=p;l.enqueueAll(p.values.map(function(x){return C.children[x]}))}else if(o.push({text:p.content,p:p.weight/t}),o.length>=n)return{value:o}};p=l.dequeue();){var m=d();if(typeof m=="object")return m.value}}return o}function ft(r){return"type"in r}function ht(r){return r.normalize("NFD").replace(/[\\u0300-\\u036f]/g,"").toLowerCase()}var pt=function(){function r(e){e=e||{},this._futureSuggestions=e.futureSuggestions?e.futureSuggestions.slice():[],e.punctuation&&(this.punctuation=e.punctuation)}return r.prototype.configure=function(e){return this.configuration={leftContextCodePoints:e.maxLeftContextCodePoints,rightContextCodePoints:e.maxRightContextCodePoints},this.configuration},r.prototype.predict=function(e,t,n){var i=function(l){var o,s,u=[],c=l.length;try{for(var f=(0,T.__values)(l),v=f.next();!v.done;v=f.next()){var p=v.value;u.push({sample:p,p:1})}}catch(d){o={error:d}}finally{try{v&&!v.done&&(s=f.return)&&s.call(f)}finally{if(o)throw o.error}}return u};if(n)return i(n);var a=this._futureSuggestions.shift();return a?i(a):[]},r}(),vt=pt,qe={};R(qe,{ClassicalDistanceCalculation:function(){return Ne},ContextTracker:function(){return ze},QUEUE_NODE_COMPARATOR:function(){return Se},SearchNode:function(){return Ge},SearchResult:function(){return He},SearchSpace:function(){return ee},TrackedContextState:function(){return Ce},TrackedContextSuggestion:function(){return gt},TrackedContextToken:function(){return re}});var Ne=function(){function r(e){if(this.diagonalWidth=2,this.inputSequence=[],this.matchSequence=[],e){var t=e.resolvedDistances.length;this.resolvedDistances=Array(t);for(var n=0;n2*n)&&(i.sparse=!0),i},r.prototype.getCostAt=function(e,t,n){if(n===void 0&&(n=this.diagonalWidth),e<0||t<0)return e==-1&&t>=-1?t+1:t==-1&&e>=-1?e+1:Number.MAX_VALUE;var i=this.getTrueIndex(e,t,n);return i.sparse?Number.MAX_VALUE:this.resolvedDistances[i.row][i.col]},r.prototype.getFinalCost=function(){for(var e=this,t=e.getHeuristicFinalCost();t>e.diagonalWidth;)e=e.increaseMaxDistance(),t=e.getHeuristicFinalCost();return t},r.prototype.getHeuristicFinalCost=function(){return this.getCostAt(this.inputSequence.length-1,this.matchSequence.length-1)},r.prototype.hasFinalCostWithin=function(e){var t=this,n=t.getHeuristicFinalCost(),i=this.diagonalWidth;do{if(n<=e)return!0;if(i=0&&f>=0){var v=1;if(i=["transpose-start"],c!=e-1){var p=e-c-1;i=i.concat(Array(p).fill("transpose-delete")),v+=p}else{var p=t-f-1;i=i.concat(Array(p).fill("transpose-insert")),v+=p}i.push("transpose-end"),this.getCostAt(c-1,f-1)!=n-v&&(i=null),a=[c-1,f-1]}return i||(s==n-1?(i=["substitute"],a=[e-1,t-1]):l==n-1?(i=["insert"],a=[e,t-1]):o==n-1?(i=["delete"],a=[e-1,t]):(i=["match"],a=[e-1,t-1])),a[0]>=0&&a[1]>=0?this.editPath(a[0],a[1]).concat(i):a[0]>-1?Array(a[0]+1).fill("delete").concat(i):a[1]>-1?Array(a[1]+1).fill("insert").concat(i):i},r.getTransposeParent=function(e,t,n){if(t<0||n<0||e.inputSequence[t].key==e.matchSequence[n].key)return[-1,-1];for(var i=-1,a=t-1;a>=0;a--)if(e.inputSequence[a].key==e.matchSequence[n].key){i=a;break}for(var l=-1,a=n-1;a>=0;a--)if(e.matchSequence[a].key==e.inputSequence[t].key){l=a;break}return[i,l]},r.initialCostAt=function(e,t,n,i,a){var l=e.inputSequence[t].key==e.matchSequence[n].key?0:1,o=e.getCostAt(t-1,n-1)+l,s=i||e.getCostAt(t,n-1)+1,u=a||e.getCostAt(t-1,n)+1,c=Number.MAX_VALUE;if(t>0&&n>0){var f=(0,T.__read)(r.getTransposeParent(e,t,n),2),v=f[0],p=f[1];c=e.getCostAt(v-1,p-1)+(t-v-1)+1+(n-p-1)}return Math.min(o,u,s,c)},r.prototype.getSubset=function(e,t){var n=new r(this);if(e>this.inputSequence.length||t>this.matchSequence.length)throw"Invalid dimensions specified for trim operation";n.inputSequence.splice(e),n.matchSequence.splice(t),n.resolvedDistances.splice(e);for(var i=this.getTrueIndex(e-1,t-1,this.diagonalWidth),a=i.col;a<=2*this.diagonalWidth;a++){var l=i.row-(a-i.col);if(l<0)break;if(a<0)n.resolvedDistances[l]=Array(2*n.diagonalWidth+1).fill(Number.MAX_VALUE);else{var o=2*this.diagonalWidth-a,s=n.resolvedDistances[l].splice(0,a+1),u=Array(o).fill(Number.MAX_VALUE);n.resolvedDistances[l]=s.concat(u)}}return n},r.forDiagonalOfAxis=function(e,t,n,i){for(var a=n-t=0){var c=u==0?o+2:Number.MAX_VALUE;s=r.initialCostAt(e,o,u,c,void 0);var f=s;if(u0&&l){var s=i+1;this.propagateUpdateFrom(e,t+1,n,s,a-1)}if(l&&o){var s=i+(e.inputSequence[t+1].key==e.matchSequence[n+1].key?0:1);this.propagateUpdateFrom(e,t+1,n+1,s,a);for(var u=-1,c=t+2;c0&&f>0){var v=i+(u-t-2)+1+(f-n-2);this.propagateUpdateFrom(e,u,f,v,e.diagonalWidth-1+f-u)}}},Object.defineProperty(r.prototype,"mapKey",{get:function(){var e=this.inputSequence.map(function(n){return n.key}).join(""),t=this.matchSequence.map(function(n){return n.key}).join("");return e+Q+t+Q+this.diagonalWidth},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"lastInputEntry",{get:function(){return this.inputSequence[this.inputSequence.length-1]},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"lastMatchEntry",{get:function(){return this.matchSequence[this.matchSequence.length-1]},enumerable:!1,configurable:!0}),r.computeDistance=function(e,t,n){n===void 0&&(n=1);var i=new r;n=n||1,i.diagonalWidth=n;for(var a=0;ae?t.p:e}).reduce(function(t,n){return t-Math.log(n)},0),this._inputCost},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"currentCost",{get:function(){return ee.EDIT_DISTANCE_COST_SCALE*this.knownCost+this.inputSamplingCost},enumerable:!1,configurable:!0}),r.prototype.buildInsertionEdges=function(){var e,t,n=[];try{for(var i=(0,T.__values)(this.currentTraversal.children()),a=i.next();!a.done;a=i.next()){var l=a.value,o=l.traversal(),s={key:l.char,traversal:o},u=this.calculation.addMatchChar(s),c=new r(this);c.calculation=u,c.priorInput=this.priorInput,c.currentTraversal=o,n.push(c)}}catch(f){e={error:f}}finally{try{a&&!a.done&&(t=i.return)&&t.call(i)}finally{if(e)throw e.error}}return n},r.prototype.buildDeletionEdges=function(e){var t,n,i=[];try{for(var a=(0,T.__values)(e),l=a.next();!l.done;l=a.next()){var o=l.value;if(o.p0:!1},r.prototype.handleNextNode=function(){if(!this.hasNextMatchEntry())return{type:"none"};var e=this.selectionQueue.dequeue(),t=e.correctionQueue.dequeue(),n={type:"intermediate",cost:t.currentCost};if(this.processedEdgeSet[t.mapKey])return this.selectionQueue.enqueue(e),n;this.processedEdgeSet[t.mapKey]=!0;var i=!1;if(t.knownCost>2)return n;t.knownCost==2&&(i=!0);for(var a=0,l=0;l<=e.index;l++)a+=this.minInputCost[l];if(t.currentCost>a+2.5*r.EDIT_DISTANCE_COST_SCALE)return n;if(!i){var o=t.buildInsertionEdges();e.correctionQueue.enqueueAll(o)}if(e.index==this.tierOrdering.length-1)return this.completedPaths.push(t),this.selectionQueue.enqueue(e),{type:"complete",cost:t.currentCost,finalNode:t};var s=this.tierOrdering[e.index+1],u=s.index,c=[];i||(c=t.buildDeletionEdges(this.inputSequence[u-1]));var f=t.buildSubstitutionEdges(this.inputSequence[u-1]);return s.correctionQueue.enqueueAll(c.concat(f)),this.selectionQueue=new b(this.QUEUE_SPACE_COMPARATOR,this.tierOrdering),n},r.prototype.getBestMatches=function(e){var t,n,i,a,l,o,s,u,c,f,v,p,d,m,C,x;return(0,T.__generator)(this,function(y){switch(y.label){case 0:if(t=this,n={},e==0?i=1/0:e==null||Number.isNaN(e)?i=r.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL:i=e,a=function(){function S(w,_){this.largestIntervals=[0],this.loopStart=this.start=Date.now(),this.maxExecutionTime=w,this.maxTrueTime=_}return S.prototype.startLoop=function(){this.loopStart=Date.now()},S.prototype.markIteration=function(){var w=Date.now(),_=w-this.loopStart;this.executionTime+=_,_&&_>this.largestIntervals[0]&&(this.largestIntervals.length>2?this.largestIntervals[0]=_:this.largestIntervals.push(_),this.largestIntervals.sort(),this.updateOutliers())},S.prototype.updateOutliers=function(){this.largestIntervals.length>2&&this.largestIntervals[2]>=2*(this.largestIntervals[0]+this.largestIntervals[1])&&(this.executionTime-=this.largestIntervals[2],this.largestIntervals.pop())},S.prototype.shouldTimeout=function(){var w=Date.now();return w-this.start>this.maxTrueTime?!0:this.executionTime>this.maxExecutionTime},S.prototype.resetOutlierCheck=function(){this.largestIntervals=[]},S}(),l=function(){function S(){this.currentCost=Number.MIN_SAFE_INTEGER,this.entries=[]}return S.prototype.checkAndAdd=function(w){var _=null;w.currentCost>this.currentCost&&(_=this.tryFinalize(),this.currentCost=w.currentCost);var I=w.calculation.matchSequence.map(function(Y){return Y.key}).join("");return t.returnedValues[I]||(t.returnedValues[I]=w),n[I]||(this.entries.push(new He(w)),n[I]=w),_},S.prototype.tryFinalize=function(){var w=null;return this.entries.length>0&&(w=this.entries,this.entries=[]),w},S}(),o=new l,s=new a(i*1.5,i),u=Object.values(this.returnedValues),!(u.length>0))return[3,6];c=new b(Se,u),s.startLoop(),y.label=1;case 1:return c.count>0?(f=c.dequeue(),f.isFullReplacement?[3,1]:(v=o.checkAndAdd(f),s.markIteration(),v?[4,v]:[3,3])):[3,4];case 2:y.sent(),y.label=3;case 3:return[3,1];case 4:return p=o.tryFinalize(),p?[4,p]:[3,6];case 5:y.sent(),y.label=6;case 6:s.resetOutlierCheck(),s.startLoop(),d=!1,y.label=7;case 7:m=void 0;do m=this.handleNextNode(),s.markIteration(),s.shouldTimeout()&&(d=!0);while(!d&&m.type=="intermediate");if(C=void 0,m.type=="none")return[3,10];if(m.type=="complete"){if(m.finalNode.isFullReplacement)return[3,10];C=o.checkAndAdd(m.finalNode)}return C?[4,C]:[3,9];case 8:y.sent(),y.label=9;case 9:if(!d&&this.hasNextMatchEntry())return[3,7];y.label=10;case 10:return x=o.tryFinalize(),x?[4,x]:[3,12];case 11:y.sent(),y.label=12;case 12:return[2,null]}})},r.EDIT_DISTANCE_COST_SCALE=5,r.MIN_KEYSTROKE_PROBABILITY=1e-4,r.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL=33,r}(),dt=function(){function r(){}return r.isWhitespace=function(e){var 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},r.isBackspace=function(e){return e.insert==""&&e.deleteLeft>0&&!e.deleteRight},r.isEmpty=function(e){return e.insert==""&&e.deleteLeft==0&&!e.deleteRight},r}(),te=dt;function Xe(r,e){for(var t=[],n=0;n0&&e.transformDistributions.forEach(function(n){return t.searchSpace[0].addInput(n)})},r.prototype.pushWhitespaceToTail=function(e){e===void 0&&(e=null);var t=new re;t.transformDistributions=e?[e]:[],t.raw=null,this.tokens.push(t)},r.prototype.replaceTailForBackspace=function(e,t){this.tokens.pop();var n=Xe(e,t).map(function(a){return[{sample:a,p:1}]}),i=new re;i.raw=e,i.transformDistributions=n,this.pushTail(i)},r.prototype.updateTail=function(e,t){var n=this.tail;t=t||(t===""?"":n.raw),e&&e.length>0&&(n.transformDistributions.push(e),this.searchSpace&&this.searchSpace.forEach(function(i){return i.addInput(e)})),n.raw=t},r.prototype.toRawTokenization=function(){var e,t,n=[];try{for(var i=(0,T.__values)(this.tokens),a=i.next();!a.done;a=i.next()){var l=a.value;l.currentText!==null&&n.push(l.currentText)}}catch(o){e={error:o}}finally{try{a&&!a.done&&(t=i.return)&&t.call(i)}finally{if(e)throw e.error}}return n},r}(),yt=function(){function r(e){e===void 0&&(e=r.DEFAULT_ARRAY_SIZE),this.currentHead=0,this.currentTail=0,this.circle=Array(e)}return Object.defineProperty(r.prototype,"count",{get:function(){var e=this.currentHead-this.currentTail;return e<0&&(e=e+this.circle.length),e},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"maxCount",{get:function(){return this.circle.length},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"oldest",{get:function(){if(this.count!=0)return this.item(0)},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,"newest",{get:function(){if(this.count!=0)return this.item(this.count-1)},enumerable:!1,configurable:!0}),r.prototype.enqueue=function(e){var t=null,n=(this.currentHead+1)%this.maxCount;return n==this.currentTail&&(t=this.circle[this.currentTail],this.currentTail=(this.currentTail+1)%this.maxCount),this.circle[this.currentHead]=e,this.currentHead=n,t},r.prototype.dequeue=function(){if(this.currentTail==this.currentHead)return null;var e=this.circle[this.currentTail];return this.currentTail=(this.currentTail+1)%this.maxCount,e},r.prototype.popNewest=function(){if(this.currentTail==this.currentHead)return null;var e=this.circle[this.currentHead];return this.currentHead=(this.currentHead-1+this.maxCount)%this.maxCount,e},r.prototype.item=function(e){if(e>=this.count)throw"Invalid array index";var t=(this.currentTail+e)%this.maxCount;return this.circle[t]},r.DEFAULT_ARRAY_SIZE=5,r}(),ze=function(r){(0,T.__extends)(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.attemptMatchContext=function(t,n,i){var a=n.toRawTokenization(),l=Ne.computeDistance(a.map(function(I){return{key:I}}),t.map(function(I){return{key:I}}),1),o=l.editPath(),s=!1,u=!1;if(o.length>1){if(o[0]=="insert"&&!(o[1]=="substitute"&&o.length==2)||o[0].indexOf("transpose")>=0)return null;o[0]=="delete"&&(s=!0)}var c=o.length-1,f=!1;if(o[c]=="delete"||o[0].indexOf("transpose")>=0||(o[c]=="insert"?u=!0:c>0&&o[c-1]=="insert"&&o[c]=="substitute"&&(u=!0,f=!0),c>0&&o[c-1]=="delete"&&o[c]=="substitute"))return null;for(var v=1;v1)if(s&&p.popHead(),u){var S=t[t.length-1],w=new re;w.raw=S,C||!m?(p.pushWhitespaceToTail(i!=null?i:[]),w.transformDistributions=[]):(p.pushWhitespaceToTail(),w.transformDistributions=i?[i]:[]),p.pushTail(w)}else x?p.replaceTailForBackspace(y,m.id):p.updateTail(m?i:null,y);else if(o[c]=="insert"){var _=new re;_.raw=t[0],_.transformDistributions=[i],p.pushTail(_)}else x?p.replaceTailForBackspace(y,m.id):p.updateTail(m?i:null,y);return p},e.modelContextState=function(t,n,i){var a=t.map(function(s){var u=new re;return u.raw=s,u.raw?u.transformDistributions=Xe(u.raw).map(function(c){return[{sample:c,p:1}]}):u.transformDistributions=[],u}),l=new Ce(i);for(a.length>0&&l.pushTail(a.splice(0,1)[0]);a.length>0;)l.pushWhitespaceToTail(),l.pushTail(a.splice(0,1)[0]);if(l.tokens.length==0){var o=new re;o.raw="",l.pushTail(o)}return l},e.prototype.analyzeState=function(t,n,i){if(!t.traverseFromRoot)throw"This lexical model does not provide adequate data for correction algorithms and context reuse";var a=z(t.wordbreaker||ce,n);if(a.left.length>0)for(var l=this.count-1;l>=0;l--){var o=this.item(l),s=o.taggedContext;if(s&&i&&i.length>0){var u=H(i[0].sample,s);if(u.left!=n.left)continue}else if((s==null?void 0:s.left)!=n.left)continue;var c=e.attemptMatchContext(a.left,this.item(l),i);if(c)return this.newest!=c&&this.newest!=o&&this.enqueue(o),c.taggedContext=n,c!=this.item(l)&&this.enqueue(c),c}var f=e.modelContextState(a.left,i,t);return f.taggedContext=n,this.enqueue(f),f},e.prototype.clearCache=function(){for(;this.count>0;)this.dequeue()},e}(yt),mt=function(){function r(e,t){this.SUGGESTION_ID_SEED=0,this.testMode=!1,this.lexicalModel=e,e.traverseFromRoot&&(this.contextTracker=new ze),this.punctuation=r.determinePunctuationFromModel(e),this.testMode=!!t}return r.prototype.predictFromCorrections=function(e,t){var n,i,a=[],l=function(f){var v=o.lexicalModel.predict(f.sample,t),p=v.map(function(d){var m=f.sample,C=f.p;m.id!==void 0&&(d.sample.transformId=m.id);var x={sample:d.sample,p:d.p*C};return x},o);a=a.concat(p)},o=this;try{for(var s=(0,T.__values)(e),u=s.next();!u.done;u=s.next()){var c=u.value;l(c)}}catch(f){n={error:f}}finally{try{u&&!u.done&&(i=s.return)&&i.call(s)}finally{if(n)throw n.error}}return a},r.prototype.predict=function(e,t){var n,i,a,l,o=[],s=this.lexicalModel,u=this.punctuation;e instanceof Array?e.length==0&&e.push({sample:{insert:"",deleteLeft:0},p:1}):e=[{sample:e,p:1}];var c=e.sort(function(O,j){return j.p-O.p})[0].sample,f=te.isWhitespace(c),v=te.isBackspace(c),p=H(c,t),d=this.wordbreak(p),m=null,C=[],x,y=null;if(this.contextTracker){var w=this.contextTracker.analyzeState(this.lexicalModel,t,null);y=this.contextTracker.analyzeState(this.lexicalModel,p,te.isEmpty(c)?null:e);var _=y.searchSpace[0],I=0,Y=y.tokens,ne=Y.length,Z=Y.length-w.tokens.length;ne==0||Z>0?(I=0,te.isWhitespace(c)&&(x=c,t=p,w=y)):Z<0?I=this.wordbreak(p).kmwLength()+c.deleteLeft:I=this.wordbreak(t).kmwLength();var be=Y[Y.length-1],ke=be.transformDistributions.length<=1,ie=void 0,_e=this.testMode?0:ee.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL;try{for(var ae=(0,T.__values)(_.getBestMatches(_e)),K=ae.next();!K.done;K=ae.next()){var ve=K.value,S=ve.map(function(j){var fe=j.matchString,Te;j.inputSequence.length>0?Te=j.inputSequence[j.inputSequence.length-1].sample:Te=c;var Tt={insert:fe,deleteLeft:I,id:c.id},$e=j.totalCost;return ke&&($e*=r.SINGLE_CHAR_KEY_PROB_EXPONENT),{sample:Tt,p:Math.exp(-$e)}},this),W=this.predictFromCorrections(S,t);W.length>0&&ie===void 0&&(ie=-Math.log(S[0].p)),C=C.concat(W);var oe=ve[0].totalCost;if(oe>=ie+8)break;if(C.length>=r.MAX_SUGGESTIONS){if(oe>=ie+4)break;if(C.sort(function(j,fe){return fe.p-j.p}),C[r.MAX_SUGGESTIONS-1].p>Math.exp(-oe))break}}}catch(O){n={error:O}}finally{try{K&&!K.done&&(i=ae.return)&&i.call(ae)}finally{if(n)throw n.error}}}else{var S=void 0;f?(S=[{sample:c,p:1}],x=c):S=e.map(function(O){var j=O.sample;return te.isWhitespace(j)&&!f||te.isBackspace(j)&&!v?null:O}),S=S.filter(function(O){return!!O}),C=this.predictFromCorrections(S,t)}var ue={},se=null;s.languageUsesCasing&&(se=this.detectCurrentCasing(p));var bt=this.wordbreak(t);try{for(var xe=(0,T.__values)(C),de=xe.next();!de.done;de=xe.next()){var V=de.value,ge=V.sample.displayAs,Re=ge==d;if(this.lexicalModel.languageUsesCasing&&(Re=Re||ge==this.lexicalModel.applyCasing("lower",d)),Re)if(m)m.p&&V.p&&(m.p+=V.p);else{var Fe=V.sample.transform,De={insert:d,deleteLeft:Fe.deleteLeft,deleteRight:Fe.deleteRight,id:Fe.id},kt=h(De,V.p);m=this.toAnnotatedSuggestion(kt,"keep",U.noQuotes),m.matchesModel=!0,m.transformId=V.sample.transformId}else{se&&se!="lower"&&(this.applySuggestionCasing(V.sample,bt,se),ge=V.sample.displayAs);var Ze=ue[ge];Ze?Ze.p+=V.p:ue[ge]=V}}}catch(O){a={error:O}}finally{try{de&&!de.done&&(l=xe.return)&&l.call(xe)}finally{if(a)throw a.error}}if(!m&&d!=""){var De=(0,T.__assign)({},c),Je=h(De,1);Je.displayAs=d,m=this.toAnnotatedSuggestion(Je,"keep"),m.matchesModel=!1}for(var _t in ue){var xt=ue[_t];o.push(xt)}o=o.sort(function(O,j){return j.p-O.p});var ye=o.splice(0,r.MAX_SUGGESTIONS).map(function(O){return O.sample.p&&(O.sample["lexical-p"]=O.sample.p,O.sample["correction-p"]=O.p/O.sample.p,O.sample.p=O.p),O.sample});m&&(ye=[m].concat(ye));var je=this;return ye.forEach(function(O){if(!t.right)O.transform.insert+=u.insertAfterWord;else{var j=je.tokenize(t);j&&j.caretSplitsToken&&(O.transform.insert+=u.insertAfterWord)}if(x){var fe=le(x,O.transform);fe.id=O.transformId;var Te=O;Te.transform=fe}O.id=je.SUGGESTION_ID_SEED,je.SUGGESTION_ID_SEED++}),y&&(y.tail.replacements=ye.map(function(O){return{suggestion:O,tokenWidth:1}})),ye},r.prototype.applySuggestionCasing=function(e,t,n){var i=t.kmwLength()-e.transform.deleteLeft;i>0&&(e.transform.deleteLeft+=i,e.transform.insert=t.kmwSubstr(0,i)+e.transform.insert),e.transform.insert=this.lexicalModel.applyCasing(n,e.transform.insert),e.displayAs=this.lexicalModel.applyCasing(n,e.displayAs)},r.prototype.toAnnotatedSuggestion=function(e,t,n){n===void 0&&(n=U.default);var i=U,a=i.noQuotes;return(t=="keep"||t=="revert")&&(a=i.useQuotes),{transform:e.transform,transformId:e.transformId,displayAs:i.apply(n,e.displayAs,this.punctuation,a),tag:t,p:e.p}},r.determinePunctuationFromModel=function(e){var t=St;if(!e.punctuation)return t;var n=e.punctuation,i=n.insertAfterWord;i!==""&&!i&&(i=t.insertAfterWord);var a=n.quotesForKeepSuggestion;a||(a=t.quotesForKeepSuggestion);var l=n.isRTL;return{insertAfterWord:i,quotesForKeepSuggestion:a,isRTL:l}},r.prototype.acceptSuggestion=function(e,t,n){var i=e.transform,a=t.left.kmwSubstr(-i.deleteLeft,i.deleteLeft),l=i.insert.kmwLength(),o={insert:a,deleteLeft:l},s=t;n&&(o=le(o,n),s=H(n,s));var u,c=this.tokenize(s);c?(c.left.length>0?u=c.left[c.left.length-1]:u="",u+=c.caretSplitsToken?c.right[0]:""):u=this.wordbreak(s);var f=h(o);f.displayAs=u;var v=this.toAnnotatedSuggestion(f,"revert");if(e.transformId!=null&&(v.transformId=-e.transformId),e.id!=null?v.id=-e.id:(v.id=-this.SUGGESTION_ID_SEED,this.SUGGESTION_ID_SEED++),this.contextTracker){var p=this.contextTracker.newest;p||(p=this.contextTracker.analyzeState(this.lexicalModel,t)),p.tail.activeReplacementId=e.id;var d=H(e.transform,t);this.contextTracker.analyzeState(this.lexicalModel,d)}return v},r.prototype.applyReversion=function(e,t){var n=this,i=function(){var u=H(e.transform,t),c=n.predict({insert:"",deleteLeft:0},u);return c.forEach(function(f){f.transformId=-e.transformId}),c};if(!this.contextTracker)return i();for(var a=!1,l=this.contextTracker.count-1;l>=0;l--){var o=this.contextTracker.item(l);if(o.tail.activeReplacementId==-e.id){a=!0;break}}if(!a)return i();for(;this.contextTracker.newest.tail.activeReplacementId!=-e.id;)this.contextTracker.popNewest();this.contextTracker.newest.tail.revert();var s=this.contextTracker.newest.tail.replacements.map(function(u){return u.suggestion});return s.forEach(function(u){u.transformId=-e.transformId}),s},r.prototype.wordbreak=function(e){var t=this.lexicalModel;if(t.wordbreaker||!t.wordbreak){var n=t.wordbreaker||ce;return Ae(n,e)}else return t.wordbreak(e)},r.prototype.tokenize=function(e){var t=this.lexicalModel;return t.wordbreaker?z(t.wordbreaker,e):null},r.prototype.resetContext=function(e){this.contextTracker&&(this.contextTracker.clearCache(),this.contextTracker.analyzeState(this.lexicalModel,e,null))},r.prototype.detectCurrentCasing=function(e){var t,n=this.lexicalModel,i=this.wordbreak(e);if(!n.languageUsesCasing)throw"Invalid attempt to detect casing: languageUsesCasing is set to false";if(!n.applyCasing)throw"Invalid LMLayer state: languageUsesCasing is set to true, but no applyCasing function exists";return e.casingForm=="upper"||e.casingForm=="initial"?e.casingForm:n.applyCasing("lower",i)==i?"lower":n.applyCasing("upper",i)==i?i.kmwLength()>1?"upper":"initial":n.applyCasing("initial",i)==i?"initial":(t=e.casingForm)!==null&&t!==void 0?t:null},r.MAX_SUGGESTIONS=12,r.SINGLE_CHAR_KEY_PROB_EXPONENT=16,r}(),wt=mt,St={quotesForKeepSuggestion:{open:"\\u201C",close:"\\u201D"},insertAfterWord:" "};B();var Ct=function(){function r(e){e===void 0&&(e={importScripts:null,postMessage:null}),this._testMode=!1,this._postMessage=e.postMessage||postMessage,this._importScripts=e.importScripts||importScripts,this.setupConfigState()}return r.prototype.error=function(e,t){this.cast("error",{log:e,error:t&&t.stack?t.stack:void 0})},r.prototype.onMessage=function(e){var t=e.data.message;if(!t)throw new Error("Missing required \'message\' property: ".concat(e.data));var n=e.data;if(n.message=="load"){var i=n,a=!1;if(this._currentModelSource&&i.source.type==this._currentModelSource.type&&(i.source.type=="file"&&i.source.file==this._currentModelSource.file||i.source.type=="raw"&&i.source.code==this._currentModelSource.code)&&(a=!0),a){typeof console!="undefined"&&console.warn("Duplicate model load message detected - squashing!");return}else this._currentModelSource=i.source}else n.message=="unload"&&(this._currentModelSource=null);this.state.handleMessage(n)},r.prototype.cast=function(e,t){var n=this._postMessage;n((0,T.__assign)({message:e},t))},r.prototype.loadModel=function(e){try{var 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=g);var n=this.transitionToReadyState(e);t.wordbreaksAfterSuggestions===void 0&&(t.wordbreaksAfterSuggestions=n.punctuation.insertAfterWord!=""),this.cast("ready",{configuration:t})}catch(i){this.error("loadModel failed!",i)}},r.prototype.loadModelFile=function(e){try{this._importScripts(e)}catch(t){this.error("Error occurred when attempting to load dictionary",t)}},r.prototype.unloadModel=function(){this.transitionToLoadingState()},r.prototype.setupConfigState=function(){var e=this;this.state={name:"unconfigured",handleMessage:function(t){if(t.message!=="config")throw new Error("invalid message; expected \'config\' but got ".concat(t.message));e._platformCapabilities=t.capabilities,e._testMode=!!t.testMode,e.transitionToLoadingState()}}},r.prototype.transitionToLoadingState=function(){var e=this;this.state={name:"modelless",handleMessage:function(t){if(t.message!=="load")throw new Error("invalid message; expected \'load\' but got ".concat(t.message));if(t.source.type=="file")e.loadModelFile(t.source.file);else{var n=t.source.code,i=new Function("LMLayerWorker","models","correction","wordBreakers",n);i(e,G,qe,Pe)}}}},r.prototype.transitionToReadyState=function(e){var t=this,n=new wt(e,this._testMode);return this.state={name:"ready",handleMessage:function(i){switch(i.message){case"predict":var a=i.transform,f=i.context,c=n.predict(a,f);t.cast("suggestions",{token:i.token,suggestions:c});break;case"wordbreak":var l=Ae(e.wordbreaker||ce,i.context);t.cast("currentword",{token:i.token,word:l});break;case"unload":t.unloadModel();break;case"accept":var o=i.suggestion,f=i.context,s=i.postTransform,u=n.acceptSuggestion(o,f,s);t.cast("postaccept",{token:i.token,reversion:u});break;case"revert":var u=i.reversion,f=i.context,c=n.applyReversion(u,f);t.cast("postrevert",{token:i.token,suggestions:c});break;case"reset-context":var f=i.context;n.resetContext(f);break;default:throw new Error("invalid message; expected one of {\'predict\', \'wordbreak\', \'accept\', \'revert\', \'reset-context\', \'unload\'} but got ".concat(i.message))}},compositor:n},n},r.install=function(e){var t=new r({postMessage:e.postMessage,importScripts:e.importScripts.bind(e)});return e.onmessage=t.onMessage.bind(t),t.self=e,e.LMLayerWorker=t,e.models=G,e.correction=qe,e.wordBreakers=Pe,t},r}(),Ye=Ct;typeof self!="undefined"&&"postMessage"in self&&"importScripts"in self?Ye.install(self):window.LMLayerWorker=Ye}();\n',oi="";var _r=function(){function i(){}return i.constructInstance=function(){return new Worker(this.asBlobURI(si))},i.asBlobURI=function(t){var e=kt(t);e+="\n"+oi;var n=new Blob([e],{type:"text/javascript"});return URL.createObjectURL(n)},i}(),Tn=_r;var ai=function(){function i(t,e){this.suggestions=t,this.transcriptionID=e}return i}();var Pr=function(i){(0,d.__extends)(t,i);function t(e,n,c){c===void 0&&(c=!1);var l=i.call(this)||this;l._mayPredict=!0,l._mayCorrect=!0,l._state="inactive",l.recentTranscriptions=n;var r={maxLeftContextCodePoints:64,maxRightContextCodePoints:c?0:64};return e&&(l.lmEngine=new Nn(r,e)),l}return Object.defineProperty(t.prototype,"activeModel",{get:function(){return this.currentModel},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"isConfigured",{get:function(){return!!this.configuration},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"state",{get:function(){return this._state},enumerable:!1,configurable:!0}),t.prototype.unloadModel=function(){this.lmEngine.unloadModel(),delete this.currentModel,delete this.configuration,this._state="inactive",this.emit("statechange","inactive")},t.prototype.loadModel=function(e){var n=this;if(!e)throw new Error("Null reference not allowed.");var c=e.path?"file":"raw",l=c=="file"?e.path:e.code;return this.currentModel=e,this.mayPredict&&(this._state="active",this.emit("statechange","active")),this.lmEngine.loadModel(l,c).then(function(r){n.configuration=r,n.mayPredict&&(n._state="configured",n.emit("statechange","configured"))}).catch(function(r){var s;r instanceof Error?s=r.message:s=String(r),console.error("Could not load model '"+e.id+"': "+s),n.currentModel=null,n._state="inactive",n.emit("statechange","inactive")})},t.prototype.invalidateContext=function(e,n){if(this.emit("invalidatesuggestions","context"),!this.currentModel||!this.configuration)return Promise.resolve([]);if(this.isActive){if(e){var c=e.buildTranscriptionFrom(e,null,!1);return this.predict_internal(c,!0,n)}}else return Promise.resolve([])},t.prototype.wordbreak=function(e,n){if(!this.isActive)return null;var c=new Ge(D.from(e,!1),this.configuration,n);return this.lmEngine.wordbreak(c)},t.prototype.predict=function(e,n){return!this.isActive||!this.currentModel||!this.configuration?null:(this.emit("invalidatesuggestions","new"),this.predict_internal(e,!1,n))},t.prototype.applySuggestion=function(e,n,c){var l=this;if(!n)throw"Accepting suggestions requires a destination OutputTarget instance.";var r=this.getPredictionState(e.transformId);if(r){var s=D.from(r.preInput,!1);s.apply(e.transform);var o=s.buildTransformFrom(n);n.apply(o),this.emit("suggestionapplied",n);var a=D.from(r.preInput,!1);a.apply(r.transform);var B=new Ge(r.preInput,this.configuration,c()),g=this.lmEngine.acceptSuggestion(e,B,r.transform);return g=g.then(function(F){var u={transform:r.transform,transformId:-r.token,displayAs:F.displayAs,id:F.id,tag:F.tag};return l.predictFromTarget(n,c()),u}),g}else return console.warn("Could not apply the Suggestion!"),null},t.prototype.applyReversion=function(e,n){var c=this;if(!n)throw"Accepting suggestions requires a destination OutputTarget instance.";var l=this.getPredictionState(-e.transformId);if(!l){console.warn("Could not apply the Suggestion!");return}var r=D.from(l.preInput,!1);r.apply(e.transform);var s=r.buildTransformFrom(n);n.apply(s);var o=this.lmEngine.revertSuggestion(e,new Ge(l.preInput,this.configuration,null));return o.then(function(a){var B=new ai(a,s.id);return c.emit("suggestionsready",B),c.currentPromise=null,a})},t.prototype.predictFromTarget=function(e,n){if(!e)return null;var c=e.buildTranscriptionFrom(e,null,!1);return this.predict(c,n)},t.prototype.predict_internal=function(e,n,c){var l=this;if(!e)return null;var r=new Ge(e.preInput,this.configuration,c);this.recordTranscription(e),n&&this.lmEngine.resetContext(r);var s=e.alternates;(!s||s.length==0)&&(s=[{sample:e.transform,p:1}]);var o=e.transform,a=this.currentPromise=this.lmEngine.predict(s,r);return a.then(function(B){if(a==l.currentPromise){var g=new ai(B,o.id);l.emit("suggestionsready",g),l.currentPromise=null}return B})},t.prototype.recordTranscription=function(e){this.recentTranscriptions.save(e)},t.prototype.getPredictionState=function(e){return this.recentTranscriptions.get(e)},t.prototype.shutdown=function(){this.lmEngine.shutdown()},Object.defineProperty(t.prototype,"isActive",{get:function(){return this.canEnable()?(this.activeModel||!1)&&this._mayPredict:(this._mayPredict=!1,!1)},enumerable:!1,configurable:!0}),t.prototype.canEnable=function(){return!!this.lmEngine},Object.defineProperty(t.prototype,"mayPredict",{get:function(){return this._mayPredict},set:function(e){if(!!this.canEnable()){var n=this._mayPredict;if(this._mayPredict=e,n!=e&&this.activeModel){var c=e?"active":"inactive";this._state=c,this.emit("statechange",c),e&&this.isConfigured&&(this._state="configured",this.emit("statechange","configured"))}}},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"mayCorrect",{get:function(){return this._mayCorrect},set:function(e){this._mayCorrect=e},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"wordbreaksAfterSuggestions",{get:function(){return this.configuration.wordbreaksAfterSuggestions},enumerable:!1,configurable:!0}),t.prototype.tryAcceptSuggestion=function(e){return this.emit("tryaccept",e),!1},t.prototype.tryRevertSuggestion=function(){return this.emit("tryrevert"),!1},t}(Bi.default),En=Pr;var jr=10,kn=function(){function i(){this.map=new Map}return i.prototype.get=function(t){var e=this.map.get(t);return e&&this.save(e),e},i.prototype.save=function(t){var e=t.token>=0?t.token:-t.token;this.map.delete(e),this.map.set(e,t),this.map.size>jr&&this.map.delete(this.map.keys().next().value)},i}();var qr=function(){function i(t,e,n){if(this.contextCache=new kn,!t)throw new Error("device must be defined");n||(n=i.DEFAULT_OPTIONS),this.contextDevice=t,this.kbdProcessor=new bn(t,n),this.lngProcessor=new En(e,this.contextCache)}return Object.defineProperty(i.prototype,"languageProcessor",{get:function(){return this.lngProcessor},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"keyboardProcessor",{get:function(){return this.kbdProcessor},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"keyboardInterface",{get:function(){return this.keyboardProcessor.keyboardInterface},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"activeKeyboard",{get:function(){return this.keyboardInterface.activeKeyboard},set:function(t){this.keyboardInterface.activeKeyboard=t,this.resetContext()},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"activeModel",{get:function(){return this.languageProcessor.activeModel},enumerable:!1,configurable:!0}),i.prototype.processKeyEvent=function(t,e){var n=t.srcKeyboard&&this.activeKeyboard!=t.srcKeyboard,c=this.activeKeyboard;try{if(n&&(this.keyboardInterface.activeKeyboard=t.srcKeyboard),t.baseTranscriptionToken){var l=this.contextCache.get(t.baseTranscriptionToken);l?e.restoreTo(l.preInput):console.warn("The base context for the multitap could not be found")}return this._processKeyEvent(t,e)}finally{n&&(this.keyboardInterface.activeKeyboard=c)}},i.prototype._processKeyEvent=function(t,e){var n,c=t.device.formFactor,l=t.isSynthetic;if((c==V.FormFactor.Desktop||!this.activeKeyboard||this.activeKeyboard.usesDesktopLayoutOnDevice(t.device))&&l&&this.keyboardProcessor.selectLayer(t))return new de;if(this.keyboardProcessor.doModifierPress(t,e,!l)&&!l)return new de;if(this.languageProcessor.isActive){if((t.kName=="K_BKSP"||t.Lcode==b.keyCodes.K_BKSP)&&this.languageProcessor.tryRevertSuggestion())return new de;if((t.kName=="K_SPACE"||t.Lcode==b.keyCodes.K_SPACE)&&this.languageProcessor.tryAcceptSuggestion("space"))return new de}var r=D.from(e,!0),s=this.keyboardProcessor.layerId,o=this.keyboardProcessor.processKeystroke(t,e);t.kNextLayer&&this.keyboardProcessor.selectLayer(t);var a=b.isKnownOSKModifierKey(t.kName);_e((n=o==null?void 0:o.transcription)===null||n===void 0?void 0:n.transform)&&t.kNextLayer&&(a=!0);var B=o!=null;if(B){var g=a?null:this.buildAlternates(o,t,r);o.finalize(this.keyboardProcessor,e,!1),g&&g.length>0&&(o.transcription.alternates=g)}else o=new de,o.transcription=e.buildTranscriptionFrom(e,null,!1),o.triggersDefaultCommand=!0;this.contextCache.save(o.transcription);var F=o.setStore[w.TSS_LAYER]||t.kNextLayer;this.keyboardProcessor.newLayerStore.set(F?this.keyboardProcessor.layerId:""),this.keyboardProcessor.oldLayerStore.set(F?s:"");var u=this.keyboardProcessor.processPostKeystroke(this.contextDevice,e);return u&&u.finalize(this.keyboardProcessor,e,!0),o.predictionPromise=this.languageProcessor.predict(o.transcription,this.keyboardProcessor.layerId),o.triggersDefaultCommand||e.doInputEvent(),B?o:null},i.prototype.buildAlternates=function(t,e,n){var c,l,r;if(this.languageProcessor.isActive&&!t.triggersDefaultCommand){var s=e.keyDistribution,o=new Ge(n,Ge.ENGINE_RULE_WINDOW,this.keyboardProcessor.layerId),a=o.toMock();if(this.languageProcessor.isActive&&s&&e.kbdLayer){var B=Number.MAX_VALUE,g=ze(),F=void 0;g.performance&&g.performance.now&&(F=function(){return g.performance.now()},B=F()+16);var u=Math.exp(-5);s.sort(function(X,p){return p.p-X.p}),r=[];var Q=0;try{for(var y=(0,d.__values)(s),I=y.next();!I.done;I=y.next()){var h=I.value;if(h.p=B)break;var C=D.from(a,!1),U=h.keySpec;if(!U){console.warn("Internal error: failed to properly filter set of keys for corrections");continue}var x=this.keyboardProcessor.activeKeyboard.constructKeyEvent(U,e.device,this.keyboardProcessor.stateKeys),G=this.keyboardProcessor.processKeystroke(x,C);if(G&&!G.beep&&h.p>0){var m=G.transcription.transform;m.id=t.transcription.token,r.push({sample:m,p:h.p}),Q+=h.p}}}catch(X){c={error:X}}finally{try{I&&!I.done&&(l=y.return)&&l.call(y)}finally{if(c)throw c.error}}r.forEach(function(X){X.p/=Q})}}return r},i.prototype.resetContext=function(t){this.keyboardProcessor.resetContext(t),this.languageProcessor.invalidateContext(t,this.keyboardProcessor.layerId)},i.DEFAULT_OPTIONS={baseLayout:"us"},i}(),Yn=qr;var di=N(T(),1),$r=function(i){(0,d.__extends)(t,i);function t(e,n){var c=i.call(this)||this;c.initNewContext=!0,c._currentSuggestions=[],c.recentAccept=!1,c.swallowPrediction=!1,c.doRevert=!1,c.recentRevert=!1,c.doTryAccept=function(r){!c.recentAccept&&c.selected?c.accept(c.selected):c.recentAccept&&r=="space"&&(c.recentAccept=!1)},c.doTryRevert=function(){c.doRevert?(c.doRevert=!1,c.recentAccept=!1):c.recentAccept&&(c.showRevert(),c.swallowPrediction=!0)},c.invalidateSuggestions=function(r){c.initNewContext=!1,(!c.swallowPrediction||r=="context")&&(c.recentAccept=!1,c.doRevert=!1,c.recentRevert=!1,r=="context"&&(c.swallowPrediction=!1,c.initNewContext=!0)),r!="new"&&c.clearSuggestions()},c.updateSuggestions=function(r){var s,o,a=r.suggestions;c._currentSuggestions=a,c.keepSuggestion=null;try{for(var B=(0,d.__values)(a),g=B.next();!g.done;g=B.next()){var F=g.value;F.tag=="keep"&&(c.keepSuggestion=F)}}catch(u){s={error:u}}finally{try{g&&!g.done&&(o=B.return)&&o.call(B)}finally{if(s)throw s.error}}c.keepSuggestion&&c._currentSuggestions.splice(c._currentSuggestions.indexOf(c.keepSuggestion),1),c.swallowPrediction?c.swallowPrediction=!1:(c.recentAccept=!1,c.doRevert=!1,c.recentRevert=!1),c.sendUpdateEvent()},c.onModelStateChange=function(r){(r=="configured"||r=="inactive")&&c.resetContext()},c.langProcessor=e,c.kbdProcessor=n;var l=function(){return c.currentTarget&&e.state=="configured"};return c.suggestionApplier=function(r){if(l())return e.applySuggestion(r,c.currentTarget,function(){return n.layerId})},c.suggestionReverter=function(r){l()&&e.applyReversion(r,c.currentTarget)},c.postApplicationHandler=function(){var r;n.newLayerStore.set(""),n.oldLayerStore.set(""),(r=n.processPostKeystroke(n.contextDevice,c.currentTarget))===null||r===void 0||r.finalize(n,c.currentTarget,!0)},c.connect(),c}return Object.defineProperty(t.prototype,"currentTarget",{get:function(){return this._currentTarget},enumerable:!1,configurable:!0}),t.prototype.setCurrentTarget=function(e){var n=this._currentTarget;return this._currentTarget=e,n!=e?this.resetContext():Promise.resolve([])},t.prototype.connect=function(){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),this.langProcessor.addListener("suggestionapplied",this.postApplicationHandler)},t.prototype.disconnect=function(){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.langProcessor.removeListener("suggestionapplied",this.postApplicationHandler),this.clearSuggestions()},Object.defineProperty(t.prototype,"currentSuggestions",{get:function(){var e=[];return this.activateKeep()&&this.keepSuggestion&&this.keepSuggestion.matchesModel?e.push(this.keepSuggestion):this.doRevert&&e.push(this.revertSuggestion),e.concat(this._currentSuggestions)},enumerable:!1,configurable:!0}),t.prototype.acceptInternal=function(e){return e?e.tag=="revert"?(this.suggestionReverter(e),null):this.suggestionApplier(e):null},t.prototype.accept=function(e){var n=this;return this.selected=null,this.doRevert=!1,this.revertAcceptancePromise=this.acceptInternal(e),this.revertAcceptancePromise?(this.revertAcceptancePromise.then(function(c){c&&(n.revertSuggestion=c)}),this.recentAccept=!0,this.recentRevert=!1,this.swallowPrediction=!0,this.revertAcceptancePromise):(e&&e.tag=="revert"&&(this.recentAccept=!1,this.recentRevert=!0),Promise.resolve(null))},t.prototype.showRevert=function(){this.doRevert=!0,this.sendUpdateEvent()},t.prototype.clearSuggestions=function(){this.updateSuggestions({suggestions:[],transcriptionID:0})},t.prototype.activateKeep=function(){return!this.recentAccept&&!this.recentRevert&&!this.initNewContext},t.prototype.sendUpdateEvent=function(){this.emit("update",this.currentSuggestions)},t.prototype.resetContext=function(){var e=this.currentTarget;return e?this.langProcessor.invalidateContext(e,this.kbdProcessor.layerId):Promise.resolve([])},t}(di.default),wn=$r;var gi=function(){function i(t,e,n,c){this.Pelem=t,this.Peventname=e.toLowerCase(),this.Phandler=n,this.PuseCapture=c}return i.prototype.equals=function(t){return this.Pelem==t.Pelem&&this.Peventname==t.Peventname&&this.Phandler==t.Phandler&&this.PuseCapture==t.PuseCapture},i}(),me=function(){function i(){this.domEvents=[]}return i.prototype.attachDOMEvent=function(t,e,n,c){this.detachDOMEvent(t,e,n,c),t.addEventListener(e,n,!!c);var l=new gi(t,e,n,c);this.domEvents.push(l)},i.prototype.detachDOMEvent=function(t,e,n,c){t.removeEventListener(e,n,c);for(var l=new gi(t,e,n,c),r=0;r0?t:0,this.update()},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"width",{get:function(){return this._width},set:function(t){this._width=t,this.update()},enumerable:!1,configurable:!0}),i.prototype.update=function(){var 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},i.prototype.appendStyleSheet=function(){},i.prototype.getDiv=function(){return this.div},i.prototype.configureForKeyboard=function(t,e){},i.DEFAULT_HEIGHT=37,i.BANNER_CLASS="kmw-banner-bar",i.BANNER_ID="kmw-banner-bar",i}();var f=function(){function i(t){var e=typeof t=="string"?i.parseLengthStyle(t):t;this.val=e.val,this.absolute=e.absolute,e.special&&(this.special=e.special)}return Object.defineProperty(i.prototype,"styleString",{get:function(){return this.absolute?this.val+"px":this.special?this.val+this.special:this.val*100+"%"},enumerable:!1,configurable:!0}),i.prototype.scaledBy=function(t){return new i({val:t*this.val,absolute:this.absolute})},i.inPixels=function(t){return new i({val:t,absolute:!0})},i.inPercent=function(t){return new i({val:t/100,absolute:!1})},i.forScalar=function(t){return new i({val:t,absolute:!1})},i.special=function(t,e){return new i({val:t,absolute:!1,special:e})},i.parseLengthStyle=function(t){var e=parseFloat(t);return isNaN(e)?(console.error("Could not properly parse specified length style info: '"+t+"'."),null):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}},i}();var Pe=function(i){(0,d.__extends)(t,i);function t(){var e=i.call(this,0)||this;return e.type="blank",e}return t}(ue);var yi=function(){function i(){this._activeBannerHeight=ue.DEFAULT_HEIGHT,this.events=new ui.default,this.constructContainer()}return i.prototype.constructContainer=function(){var t=k("div");return t.id="keymanweb_banner_container",t.className="kmw-banner-container",this.bannerContainer=t},Object.defineProperty(i.prototype,"element",{get:function(){return this.bannerContainer},enumerable:!1,configurable:!0}),i.prototype.appendStyles=function(){this.currentBanner&&this.currentBanner.appendStyleSheet()},Object.defineProperty(i.prototype,"banner",{get:function(){return this.currentBanner},set:function(t){if(this.currentBanner){if(t==this.currentBanner)return;var e=this.currentBanner;this.currentBanner=t,this.bannerContainer.replaceChild(t.getDiv(),e.getDiv())}else this.currentBanner=t,t&&this.bannerContainer.appendChild(t.getDiv());t instanceof Pe||(t.height=this.activeBannerHeight),this.events.emit("bannerchange")},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"height",{get:function(){return this.currentBanner?this.currentBanner.height:0},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"activeBannerHeight",{get:function(){return this._activeBannerHeight},set:function(t){this._activeBannerHeight=t,this.currentBanner&&!(this.currentBanner instanceof Pe)&&(this.currentBanner.height=t)},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"layoutHeight",{get:function(){return f.inPixels(this.height)},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"width",{get:function(){var t;return(t=this.currentBanner)===null||t===void 0?void 0:t.width},set:function(t){this.currentBanner&&(this.currentBanner.width=t)},enumerable:!1,configurable:!0}),i.prototype.refreshLayout=function(){var t,e;(e=(t=this.currentBanner).refreshLayout)===null||e===void 0||e.call(t)},i}();var Ii=function(i){(0,d.__extends)(t,i);function t(e,n){var c=this;e.length>0?(c=i.call(this)||this,n&&(c.height=n)):c=i.call(this,0)||this,c.type="image",e.indexOf("base64")>=0?console.log("Loading img from base64 data"):console.log("Loading img with src '"+e+"'"),c.img=document.createElement("img"),c.img.setAttribute("src",e);var l=c.img.style;return l.width="100%",l.height="100%",c.getDiv().appendChild(c.img),console.log("Image loaded."),c}return t.prototype.setImagePath=function(e){this.img&&this.img.setAttribute("src",e)},t}(ue);function Qi(i){return"targetX"in i&&"targetY"in i&&"t"in i}var Ae=function(){function i(t){if(this.rawLinearSums={x:0,y:0,t:0},this.coordArcSum=0,this._sampleCount=0,!!t)if(t instanceof i)Object.assign(this,t),this.rawLinearSums=(0,d.__assign)({},t.rawLinearSums);else if(Qi(t))Object.assign(this,this.extend(t));else throw new Error("A constructor for this input pattern has not yet been implemented")}return i.prototype.extend=function(t){return this._extend(new i(this),t)},i.prototype._extend=function(t,e){t._initialSample||(t._initialSample=e,t.baseSample=e);var n=t.baseSample;this.followingSample=e;var c=e.targetX-n.targetX,l=e.targetY-n.targetY,r=e.t-n.t;if(t.rawLinearSums.x+=c,t.rawLinearSums.y+=l,t.rawLinearSums.t+=r,this.lastSample){var s=e.targetX-this.lastSample.targetX,o=e.targetY-this.lastSample.targetY,a=e.t-this.lastSample.t,B=s*s+o*o,g=Math.sqrt(B);t.coordArcSum+=g}return t._lastSample=e,t.sampleCount=this.sampleCount+1,t},i.prototype.deaccumulate=function(t){var e=new i(this);return this._deaccumulate(e,t)},i.prototype._deaccumulate=function(t,e){if(!e)return t;if(!e.followingSample||!e.lastSample)throw"Invalid argument: stats missing necessary tracking variable.";for(var n in t.rawLinearSums){var c=n;t.rawLinearSums[c]-=e.rawLinearSums[c]}if(e.followingSample&&e.lastSample){var l=e.followingSample.targetX-e.lastSample.targetX,r=e.followingSample.targetY-e.lastSample.targetY,s=e.followingSample.t-e.lastSample.t,o=l*l+r*r,a=Math.sqrt(o);t.coordArcSum-=a,t.coordArcSum-=e.coordArcSum}return t.sampleCount-=e.sampleCount,t._initialSample=e.followingSample,t},i.prototype.translateCoordSystem=function(t){var e=new i(this);return this._translateCoordSystem(e,t)},i.prototype._translateCoordSystem=function(t,e){if(this.sampleCount==0)return t;var 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},i.prototype.replaceInitialSample=function(t){var e=new i(this);return this._replaceInitialSample(e,t)},i.prototype._replaceInitialSample=function(t,e){if(this.sampleCount==0)throw new Error("no sample available to replace");var n=t.initialSample;if(t._initialSample=e,this.sampleCount>1){var c=e.targetX-n.targetX,l=e.targetY-n.targetY,r=e.t-n.t;t.rawLinearSums.x+=c,t.rawLinearSums.y+=l,t.rawLinearSums.t+=r;var s=c*c+l*l,o=Math.sqrt(s);t.coordArcSum+=o}else t._lastSample=e;return t},Object.defineProperty(i.prototype,"lastSample",{get:function(){return this._lastSample},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"lastTimestamp",{get:function(){var t;return(t=this.lastSample)===null||t===void 0?void 0:t.t},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"sampleCount",{get:function(){return this._sampleCount},set:function(t){this._sampleCount=t},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"initialSample",{get:function(){return this._initialSample},enumerable:!1,configurable:!0}),i.prototype.mappingConstant=function(t){if(!!this.baseSample)return t=="t"?this.baseSample.t:t=="x"?this.baseSample.targetX:t=="y"?this.baseSample.targetY:0},i.prototype.mean=function(t){return this.rawLinearSums[t]/this.sampleCount+this.mappingConstant(t)},Object.defineProperty(i.prototype,"netDistance",{get:function(){if(!this.lastSample||!this.initialSample)return 0;var t=this.lastSample.targetX-this.initialSample.targetX,e=this.lastSample.targetY-this.initialSample.targetY;return Math.sqrt(t*t+e*e)},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"duration",{get:function(){return!this.lastSample||!this.initialSample?0:this.lastSample.t-this.initialSample.t},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"angle",{get:function(){if(!(this.sampleCount==1||!this.lastSample||!this.initialSample)&&!(this.netDistance<1)){var 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}},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"angleInDegrees",{get:function(){return this.angle*180/Math.PI},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"cardinalDirection",{get:function(){if(!(this.sampleCount==1||!this.lastSample||!this.initialSample)&&!(isNaN(this.angle)||this.angle===null||this.angle===void 0)){var t=["n","ne","e","se","s","sw","w","nw","n"],e=Math.ceil((this.angleInDegrees-22.5)/45);return t[e]}},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"speed",{get:function(){return this.duration?this.netDistance/this.duration:0},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"rawDistance",{get:function(){return this.coordArcSum},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"summaryObject",{get:function(){return{angle:this.angle,cardinal:this.cardinalDirection,netDistance:this.netDistance,duration:this.duration,sampleCount:this.sampleCount,rawDistance:this.rawDistance}},enumerable:!1,configurable:!0}),i}();var hi=function(){function i(){}return i.prototype.getBoundingClientRect=function(){return new DOMRect(0,0,Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),Math.max(document.documentElement.clientHeight||0,window.innerHeight||0))},i}();var ye=function(){function i(t,e){Array.isArray(t)&&(e=t,t=new hi),this.root=t,e=e||[0,0,0,0],this.updatePadding(e)}return Object.defineProperty(i.prototype,"edgePadding",{get:function(){return this._edgePadding},enumerable:!1,configurable:!0}),i.prototype.updatePadding=function(t){switch(t.length){case 1:var 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.")}},i.prototype.getBoundingClientRect=function(){var 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)},i}();function Ot(i){var t,e,n,c,l,r,s=(0,d.__assign)({},i);if(s.mouseEventRoot=(t=s.mouseEventRoot)!==null&&t!==void 0?t:s.targetRoot,s.touchEventRoot=(e=s.touchEventRoot)!==null&&e!==void 0?e:s.targetRoot,s.inputStartBounds=(n=s.inputStartBounds)!==null&&n!==void 0?n:s.targetRoot,s.maxRoamingBounds=(c=s.maxRoamingBounds)!==null&&c!==void 0?c:s.targetRoot,s.safeBounds=(l=s.safeBounds)!==null&&l!==void 0?l:new ye([2]),s.itemIdentifier=(r=s.itemIdentifier)!==null&&r!==void 0?r:function(){return null},s.recordingMode=!!s.recordingMode,i.paddedSafeBounds)delete s.safeBoundPadding;else{var o=i.safeBoundPadding;typeof o=="number"&&(o=[o]),o=o!=null?o:[3],s.paddedSafeBounds=new ye(s.safeBounds,o)}return s}var Ui=N(T(),1);var Ci=N(T(),1);var Mt=function(i){(0,d.__extends)(t,i);function t(){var e=i.call(this)||this;return e._isComplete=!1,e._stats=new Ae,e}return Object.defineProperty(t.prototype,"stats",{get:function(){return this._stats},enumerable:!1,configurable:!0}),t.prototype.clone=function(){var e=new t;return e._isComplete=this._isComplete,e._wasCancelled=this._wasCancelled,e._stats=new Ae(this._stats),e},Object.defineProperty(t.prototype,"isComplete",{get:function(){return this._isComplete},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"wasCancelled",{get:function(){return this._wasCancelled},enumerable:!1,configurable:!0}),t.prototype.translateCoordSystem=function(e){this._stats=this._stats.translateCoordSystem(e)},t.prototype.replaceInitialSample=function(e){this._stats=this._stats.replaceInitialSample(e)},t.prototype.extend=function(e){if(this._isComplete)throw new Error("Invalid state: this GesturePath has already terminated.");this._stats=this._stats.extend(e),this.emit("step",e)},t.prototype.terminate=function(e){e===void 0&&(e=!1),!this._isComplete&&(this._wasCancelled=e,this._isComplete=!0,e?this.emit("invalidated"):this.emit("complete"),this.removeAllListeners())},t}(Ci.default);var je=function(i){(0,d.__extends)(t,i);function t(){var e=i!==null&&i.apply(this,arguments)||this;return e.samples=[],e}return t.prototype.clone=function(){var e=new t;return e.samples=[].concat(this.samples),e._isComplete=this._isComplete,e._wasCancelled=this._wasCancelled,e._stats=new Ae(this._stats),e},t.deserialize=function(e){var n=new t;n.samples=[].concat(e.coords.map(function(l){return(0,d.__assign)({},l)})),n._isComplete=!0,n._wasCancelled=e.wasCancelled;var c=n.samples.reduce(function(l,r){return l.extend(r)},new Ae);return n._stats=c,n},t.prototype.extend=function(e){if(this.isComplete)throw new Error("Invalid state: this GesturePath has already terminated.");this.samples.push(e),i.prototype.extend.call(this,e)},t.prototype.translateCoordSystem=function(e){i.prototype.translateCoordSystem.call(this,e);for(var n=0;n0?1:0):s._baseStartIndex=o,c?e.path.stats.sampleCount&&s._path.extend(e.path.stats.lastSample):s._path=e.path.clone(),s._path.translateCoordSystem(g),l?s._baseItem=e.baseItem:s._baseItem=F==null?void 0:F.item;var u=function(){return s.path.terminate(!1)},Q=function(){return s.path.terminate(!0)},y=function(I){i.prototype.update.call(s,g(I))};return B.path.on("complete",u),B.path.on("invalidated",Q),B.path.on("step",y),s.subviewDisconnector=function(){B.path.off("complete",u),B.path.off("invalidated",Q),B.path.off("step",y)},B.isPathComplete&&(s.path.terminate(B.path.wasCancelled),s.disconnect()),s}return Object.defineProperty(t.prototype,"recognizerTranslation",{get:function(){if(this.recognizerConfigStack.length==1||!this.currentRecognizerConfig)return{x:0,y:0};var e=this.currentRecognizerConfig,n=e.targetRoot.getBoundingClientRect(),c=this.recognizerConfigStack[0].targetRoot.getBoundingClientRect();return{x:n.left-c.left,y:n.top-c.top}},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"baseSource",{get:function(){return this._baseSource},enumerable:!1,configurable:!0}),t.prototype.disconnect=function(){this.subviewDisconnector&&(this.subviewDisconnector(),this.subviewDisconnector=null)},t.prototype.pushRecognizerConfig=function(e){throw new Error("Pushing and popping of recognizer configurations should only be called on the base GestureSource")},t.prototype.popRecognizerConfig=function(){throw new Error("Pushing and popping of recognizer configurations should only be called on the base GestureSource")},t.prototype.update=function(e){throw new Error("Updates should be provided through the base GestureSource.")},t.prototype.terminate=function(e){this.baseSource.terminate(e)},t}(be);var bi=function(i){(0,d.__extends)(t,i);function t(e,n,c){var l=i.call(this,e,n,c,je)||this;return l.stateToken=null,l}return t.prototype.initPath=function(){return new je},t.deserialize=function(e,n){var c=n!==void 0?n:this._jsonIdSeed++,l=e.isFromTouch,r=je.deserialize(e.path),s=new t(c,null,l);return s._path=r,s},t.prototype.toJSON=function(){var e=this.path,n={isFromTouch:this.isFromTouch,path:e.toJSON()};return n},t}(be);var xi=function(i){(0,d.__extends)(t,i);function t(e){var n=this,c;return n=i.call(this)||this,n._activeTouchpoints=[],n.identifierMap={},n.config=e,n.sourceConstructor=!((c=e==null?void 0:e.recordingMode)!==null&&c!==void 0)||c?bi:be,n}return t.prototype.createTouchpoint=function(e,n){var c=t.IDENTIFIER_SEED++;this.identifierMap[e]=c;var l=new this.sourceConstructor(c,this.config,n);return l.stateToken=this.stateToken,l},t.prototype.maintainTouchpointsWithIds=function(e){var n=this,c=e.map(function(l){return n.identifierMap[l]});this._activeTouchpoints.filter(function(l){return!c.includes(l.rawIdentifier)}).forEach(function(l){return l.terminate(!0)})},t.prototype.hasActiveTouchpoint=function(e){return this.identifierMap[e]!==void 0},t.prototype.getTouchpointWithId=function(e){var n=this.identifierMap[e];return this._activeTouchpoints.find(function(c){return c.rawIdentifier==n})},t.prototype.getConfigForId=function(e){return this.getTouchpointWithId(e).currentRecognizerConfig},t.prototype.getStateTokenForId=function(e){var n;return(n=this.getTouchpointWithId(e).stateToken)!==null&&n!==void 0?n:null},t.prototype.dropTouchpoint=function(e){var n=e.rawIdentifier;this._activeTouchpoints=this._activeTouchpoints.filter(function(s){return e!=s});for(var c=0,l=Object.keys(this.identifierMap);cn.right?i.FAR_RIGHT:0,c|=t.clientYn.bottom?i.FAR_BOTTOM:0,c},i.inputStartOutOfBoundsCheck=function(t,e){return!!this.getCoordZoneBitmask(t,e.inputStartBounds)},i.inputStartSafeBoundProximityCheck=function(t,e){return this.getCoordZoneBitmask(t,e.paddedSafeBounds)},i.inputMoveCancellationCheck=function(t,e,n){if(n=n||0,this.getCoordZoneBitmask(t,e.maxRoamingBounds))return!0;var c=this.getCoordZoneBitmask(t,e.safeBounds);return!!(c&~n)},i.FAR_TOP=8,i.FAR_LEFT=4,i.FAR_BOTTOM=2,i.FAR_RIGHT=1,i}();var pi=function(i){(0,d.__extends)(t,i);function t(e){var n=i.call(this,e)||this;return n.hasActiveClick=!1,n.disabledSafeBounds=0,n._mouseStart=function(c){return n.onMouseStart(c)},n._mouseMove=function(c){return n.onMouseMove(c)},n._mouseEnd=function(c){return n.onMouseEnd(c)},n}return Object.defineProperty(t.prototype,"eventRoot",{get:function(){return this.config.mouseEventRoot},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"activeIdentifier",{get:function(){return 0},enumerable:!1,configurable:!0}),t.prototype.registerEventHandlers=function(){this.eventRoot.addEventListener("mousedown",this._mouseStart,!0),this.eventRoot.addEventListener("mousemove",this._mouseMove,!1),this.eventRoot.addEventListener("mouseup",this._mouseEnd,!0)},t.prototype.unregisterEventHandlers=function(){this.eventRoot.removeEventListener("mousedown",this._mouseStart,!0),this.eventRoot.removeEventListener("mousemove",this._mouseMove,!1),this.eventRoot.removeEventListener("mouseup",this._mouseEnd,!0)},t.prototype.preventPropagation=function(e){e.preventDefault(),e.cancelBubble=!0,e.returnValue=!1,typeof e.stopImmediatePropagation=="function"?e.stopImmediatePropagation():typeof e.stopPropagation=="function"&&e.stopPropagation()},t.prototype.buildSampleFromEvent=function(e,n){var c=this.getTouchpointWithId(n);return this.buildSampleFor(e.clientX,e.clientY,e.target,performance.now(),c)},t.prototype.onMouseStart=function(e){if(!!this.config.targetRoot.contains(e.target)){this.preventPropagation(e);var n=this.activeIdentifier,c=this.buildSampleFromEvent(e,n);He.inputStartOutOfBoundsCheck(c,this.config)||(this.disabledSafeBounds=He.inputStartSafeBoundProximityCheck(c,this.config)),this.onInputStart(n,c,e.target,!1)}},t.prototype.onMouseMove=function(e){if(!!this.hasActiveTouchpoint(this.activeIdentifier)){var n=this.buildSampleFromEvent(e,this.activeIdentifier);if(!e.buttons){this.hasActiveClick&&(this.hasActiveClick=!1,this.onInputMoveCancel(this.activeIdentifier,n,e.target));return}this.preventPropagation(e);var c=this.getConfigForId(this.activeIdentifier);He.inputMoveCancellationCheck(n,c,this.disabledSafeBounds)?this.onInputMoveCancel(this.activeIdentifier,n,e.target):this.onInputMove(this.activeIdentifier,n,e.target)}},t.prototype.onMouseEnd=function(e){!this.hasActiveTouchpoint(this.activeIdentifier)||(e.buttons||(this.hasActiveClick=!1),this.onInputEnd(this.activeIdentifier,e.target))},t}(Dt);function zn(i){for(var t=[],e=0;e0&&e.currentSample.item!=e.baseItem){var n=t.itemChangeAction=="resolve";return this.finalize(n,"item")}else{var 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"}}},i}();var Kn=function(){function i(t,e){var n=this,c;if(this._isCancelled=!1,!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.");var l=e instanceof be?null:e,r=l?null:e;this.predecessor=l,this.publishedPromise=new J,this.model=t,t.sustainTimer&&(this.sustainTimerPromise=new Bt(t.sustainTimer.duration),this.sustainTimerPromise.then(function(I){var h=t.sustainTimer.expectedResult==I;n.finalize(h,"timer")})),this.pathMatchers=[];var s=r?[r]:l.sources,o=s.map(function(I){return r&&I==r?r:I.isPathComplete?null:I}).reduce(function(I,h){return h?I.concat(h):I},[]);if(t.sustainTimer&&o.length>0){this.finalize(!1,"path");return}else!t.sustainTimer&&o.length==0&&this.finalize(!1,"path");for(var a=0;ae&&(e=l.model.itemPriority,t=l)}return!t&&this.predecessor?this.predecessor.primaryPath:t==null?void 0:t.source},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"baseItem",{get:function(){return this.primaryPath.baseItem},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"currentItem",{get:function(){return this.primaryPath.currentSample.item},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"allSourceIds",{get:function(){var t=this.sources.map(function(n){return n.identifier}),e=this.predecessor?this.predecessor.allSourceIds:[];return t=t.filter(function(n){return e.indexOf(n)==-1}),t.concat(e)},enumerable:!1,configurable:!0}),i.prototype.mayAddContact=function(){return this.pathMatchers.length-1&&e._sourceSelector.splice(a,1)}),c.forEach(function(o){return o.cancel()}),this.sustainMode=!0,this._sourceSelector.map(function(o){return o.source})},t.prototype.matchGesture=function(e,n){return(0,d.__awaiter)(this,void 0,void 0,function(){var c,l,r,s,o,a,B,g,F,u,Q,y,I,h,C,U,x,G,m=this;return(0,d.__generator)(this,function(X){switch(X.label){case 0:return c=e instanceof be,l=function(p){var Z=p.sources.map(function(R){return R.baseSource});return Z&&Z.length>0?Z:p.predecessor?l(p.predecessor):[]},r=c?[e instanceof le?e.baseSource:e]:l(e),s=c?e:null,o=c?null:e,this.pendingMatchSetup?[4,this.pendingMatchSetup]:[3,2];case 1:X.sent(),X.label=2;case 2:return c&&s.path.on("invalidated",function(){m.dropSourcesWithIds([s.identifier])}),a=new J,B=r.map(function(p){var Z={source:p,matchPromise:a};return m._sourceSelector.push(Z),Z}),g=B.map(function(p){return p.matchPromise}),c?(F=this.potentialMatchers.filter(function(p){return p.mayAddContact()}),F.forEach(function(p){p.addContact(s),p.promise.then(m.matcherSelectionFilter(p,g))}),F.length>0?(u=this.stateToken,Q=new J,this.pendingMatchSetup=Q.corePromise,[4,xe(0)]):[3,5]):[3,5];case 3:return X.sent(),[4,xe(0)];case 4:if(X.sent(),this.pendingMatchSetup=null,Q.resolve(),y=this.stateToken,u!=y&&(I=s.currentSample,s.stateToken=y,I.stateToken=y,I.item=e.currentRecognizerConfig.itemIdentifier(I,null),s.baseItem=I.item),h=F.find(function(p){return p.result}),h&&h.allSourceIds.includes(e.identifier))return a.resolve({matcher:null,result:{matched:!1,action:{type:"complete",item:null}}}),[2,{selectionPromise:a.corePromise}];X.label=5;case 5:if(this.sustainMode&&s)return a.resolve({matcher:null,result:{matched:!1,action:{type:"complete",item:null}}}),[2,{selectionPromise:a.corePromise,sustainModeWithoutMatch:!0}];for(C=n.map(function(p){return new Kn(p,s||o)}),C=C.filter(function(p){return!p.result||p.result.matched!==!1}),U=0,x=C;U0?this.potentialMatchers=this.potentialMatchers.concat(C):a.resolve({matcher:null,result:{matched:!1,action:{type:"complete",item:null}}}),this.potentialMatchers.sort(function(p,Z){return Z.model.resolutionPriority-p.model.resolutionPriority}),this.resetSourceHooks(),[2,{selectionPromise:a.corePromise}]}})})},t.prototype.resetSourceHooks=function(){var e=this,n=function(c){var l=c;l.path.off("step",e.attemptSynchronousUpdate),l.path.off("complete",e.attemptSynchronousUpdate),l.path.off("invalidated",e.attemptSynchronousUpdate),l.path.on("step",e.attemptSynchronousUpdate),l.path.on("complete",e.attemptSynchronousUpdate),l.path.on("invalidated",e.attemptSynchronousUpdate)};this._sourceSelector.forEach(function(c){return n(c.source)})},t.prototype.dropSourcesWithIds=function(e){for(var n=function(o){var a=c._sourceSelector.findIndex(function(g){return g.source.identifier==o});if(a>-1){var B=c._sourceSelector.splice(a,1)[0];B.matchPromise.resolve({matcher:null,result:{matched:!1,action:{type:"none",item:null}}})}},c=this,l=0,r=e;l0)throw new Error("Set '".concat(t,"' cannot find definitions for gestures with ids ").concat(c));return n}var Zi={gestures:[],sets:{default:[]}};var cs=function(){function i(t){var e,n,c=t.matcher,l=t.result;this.matchedId=c==null?void 0:c.model.id,this.linkType=l.action.type,this.item=l.action.item,this.sources=c==null?void 0:c.sources,(e=this.sources)===null||e===void 0||e.forEach(function(r){return r.disconnect()}),(n=this.sources)===null||n===void 0||n.sort(function(r,s){return(c==null?void 0:c.primaryPath)==r?-1:(c==null?void 0:c.primaryPath)==s?1:0}),this.allSourceIds=(c==null?void 0:c.allSourceIds)||[]}return i}();var fi=function(i){(0,d.__extends)(t,i);function t(e,n,c,l){var r=i.call(this)||this;return r.markedComplete=!1,r.selectionHandler=function(s){return(0,d.__awaiter)(r,void 0,void 0,function(){var o,a,B,g,F,u,Q,y,I,h,C,U,x=this,G,m,X,p,Z,R,A,Y;return(0,d.__generator)(this,function(S){switch(S.label){case 0:return o=new cs(s),s.matcher&&this.stageReports.push(o),a=(G=s.matcher)!==null&&G!==void 0?G:this.stageReports[this.stageReports.length-1],B=(m=a==null?void 0:a.sources.map(function(L){return L instanceof le?L.baseSource:L}))!==null&&m!==void 0?m:[],g=s.result.action.type,(g=="complete"||g=="none")&&(B.forEach(function(L){L.isPathComplete||L.terminate(g=="none")}),!s.result.matched)?(this.markedComplete||(this.markedComplete=!0,this.emit("complete")),[2]):g=="complete"&&this.touchpointCoordinator&&this.pushedSelector?(F=(X=this.touchpointCoordinator)===null||X===void 0?void 0:X.sustainSelectorSubstack(this.pushedSelector),u=F.map(function(L){var v=new J;return L.path.on("invalidated",function(){return v.resolve()}),L.path.on("complete",function(){return v.resolve()}),v.corePromise}),u.length>0&&s.result.action.awaitNested?[4,Promise.all(u)]:[3,3]):[3,4];case 1:return S.sent(),[4,xe(0)];case 2:S.sent(),S.label=3;case 3:(p=this.touchpointCoordinator)===null||p===void 0||p.popSelector(this.pushedSelector),this.pushedSelector=null,S.label=4;case 4:return this.emit("stage",o,function(L){L.type=="pop"?B.forEach(function(v){return v.popRecognizerConfig()}):B.forEach(function(v){return v.pushRecognizerConfig(L.config)})}),Q=!1,this.touchpointCoordinator&&(Q=!this.touchpointCoordinator.selectorStackIncludes(this.selector)),y=is(s.result.action,this.gestureConfig,this.baseGestureSetId),Q&&(y=y.filter(function(L){return L.sustainWhenNested})),y.length>0?(g=="chain"&&s.result.action.selectionMode==((Z=this.pushedSelector)===null||Z===void 0?void 0:Z.baseGestureSetId)||(this.pushedSelector&&(this.pushedSelector.off("rejectionwithaction",this.modelResetHandler),(R=this.touchpointCoordinator)===null||R===void 0||R.popSelector(this.pushedSelector),this.pushedSelector=null),g=="chain"&&(I=s.result.action.selectionMode,I&&(h=new zt(I),h.on("rejectionwithaction",this.modelResetHandler),this.pushedSelector=h,(A=this.touchpointCoordinator)===null||A===void 0||A.pushSelector(h)))),C=(Y=this.pushedSelector)!==null&&Y!==void 0?Y:this.selector,U=C.matchGesture(s.matcher,y),U.then(function(L){return(0,d.__awaiter)(x,void 0,void 0,function(){var v;return(0,d.__generator)(this,function(H){switch(H.label){case 0:return v=this.selectionHandler,[4,L.selectionPromise];case 1:return[2,v.apply(this,[H.sent()])]}})})})):this.markedComplete||(this.markedComplete=!0,this.emit("complete")),[2]}})})},r.modelResetHandler=function(s,o){var a=s.matcher.allSourceIds;if(!r.allSourceIds.find(function(B){return a.indexOf(B)==-1}))if(s.result.action.type=="replace")o(qe(r.gestureConfig,s.result.action.replace));else throw new Error("Missed a case in implementation!")},r.stageReports=[],r.selector=c,r.selector.on("rejectionwithaction",r.modelResetHandler),r.once("complete",function(){var s;r.pushedSelector&&((s=r.touchpointCoordinator)===null||s===void 0||s.popSelector(r.pushedSelector),r.pushedSelector=null),r.selector.off("rejectionwithaction",r.modelResetHandler),r.selector.dropSourcesWithIds(r.allSourceIds),r.selector=null}),r.gestureConfig=n,r.touchpointCoordinator=l,Promise.resolve().then(function(){return r.selectionHandler(e)}),r}return Object.defineProperty(t.prototype,"allSourceIds",{get:function(){var e,n;return(n=(e=this.stageReports[this.stageReports.length-1])===null||e===void 0?void 0:e.allSourceIds)!==null&&n!==void 0?n:[]},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"baseGestureSetId",{get:function(){var e,n;return(n=(e=this.selector)===null||e===void 0?void 0:e.baseGestureSetId)!==null&&n!==void 0?n:null},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"potentialModelMatchIds",{get:function(){if(!this.selector)return[];var e=[this.selector];this.pushedSelector&&e.push(this.pushedSelector);var n=this.stageReports[this.stageReports.length-1],c=n.sources,l=c.map(function(r){return e.map(function(s){return s.potentialMatchersForSource(r).map(function(o){return o.model.id})})}).reduce(function(r,s){return r.concat(s)}).reduce(function(r,s){for(var o=0,a=s;othis.gestureParams.flick.triggerDist?c=t.keyEventFromSpec(e):c=t.keyEventFromSpec(this.baseSpec),c.keyDistribution=this.currentStageKeyDistribution(this.baseKeyDistances),t.raiseKeyEvent(c,null)},i.prototype.buildPopupRecognitionConfig=function(t){var e={getBoundingClientRect:function(){var n=Number.MAX_SAFE_INTEGER;return new DOMRect(-n,-n,2*n,2*n)}};return(0,d.__assign)((0,d.__assign)({},t.gestureEngine.config),{maxRoamingBounds:e,safeBounds:e})},i.prototype.cancel=function(){},i.prototype.flickDistribution=function(t,e){var n=this.baseSpec.flick,c=[{spec:this.baseSpec,coord:[NaN,0]}];c=c.concat(Object.keys(n).map(function(F){return{spec:n[F],coord:Pn.get(F)}}));var l=t.angle,r=this.gestureParams.flick.triggerDist,s=Math.min(r,e?r:t.netDistance),o=s/r,a=0,B=c.map(function(F){var u=0,Q=F.coord;if(!isNaN(Q[0])){var y=l-Q[0],I=2*Si+Q[0]-l;u=Math.min(y*y,I*I)}var h=_t*(Q[1]-o),C=h*h,U=1/(u+C+1e-6);return a+=U,{keySpec:F.spec,p:U}}),g=1/a;return B.forEach(function(F){return F.p*=g}),B.sort(function(F,u){return u.p-F.p})},i.prototype.currentStageKeyDistribution=function(t){var e=this.baseSpec,n=this.baseKeyDistances,c=this.computedFlickDistribution,l=n.get(e);if(!l){var r=c[0];return[{keySpec:r.keySpec,p:1}]}var s=c.findIndex(function(B){return B.keySpec==e}),o=c.splice(s,1)[0].p,a=Re(n);return c.concat(a.map(function(B){return{keySpec:B.keySpec,p:B.p*o}}))},i}(),Ri=rs;var jn={longpress:{permitsFlick:function(){return!0},flickDist:5,waitLength:500,noiseTolerance:10},multitap:{waitLength:500,holdLength:500},flick:{startDist:10,dirLockDist:25,triggerDist:40}};function Ai(i){var t=i.getBoundingClientRect();return{clientX:t.left+t.width/2,clientY:t.top+t.height/2}}function Hi(i){var t=Ai(i);return function(e){var n=e.lastSample.clientX-t.clientX,c=e.lastSample.clientY-t.clientY;return Math.sqrt(n*n+c*c)}}function bt(i){var t,e,n=i.key.spec,c=["K_SHIFT","K_ALT","K_CTRL","K_NUMERALS","K_SYMBOLS","K_CURRENCIES"];try{for(var l=(0,d.__values)(c),r=l.next();!r.done;r=l.next()){var s=r.value;if(n.id==s)return!0}}catch(o){t={error:o}}finally{try{r&&!r.done&&(e=l.return)&&e.call(l)}finally{if(t)throw t.error}}if(n.nextlayer)switch(n.sp){case 1:case 2:case 3:case 4:return!0;default:return!1}else return!1}function qn(i,t){var e=function(F,u){if(!F)return!1;var Q=F.key.spec;switch(u){case"modipress-start":return bt(F);case"special-key-start":return["K_LOPT","K_ROPT","K_BKSP"].indexOf(Q.baseKeyID)!=-1;case"longpress":return!0;case"multitap-start":case"modipress-multitap-start":return i.hasMultitaps?!!Q.multitap:!1;case"flick-start":return!!Q.flick;default:return!0}},n=t.roamingEnabled||(t.roamingEnabled=!i.hasFlicks),c=te(n?ms(t):Yi(t)),l=te(n?ec(t):wi(t)),r=te(Ei(t,!0,n));function s(F,u){F=te(F);var Q=F.id;return typeof u=="number"&&(u=[u]),F.contacts.forEach(function(y,I){var h;if(u.indexOf(I)!=-1){var C=(h=y.model.allowsInitialState)!==null&&h!==void 0?h:function(){return!0};y.model=(0,d.__assign)((0,d.__assign)({},y.model),{allowsInitialState:function(U,x,G){return e(G,Q)&&C(U,x,G)}})}}),F}var o=Fs(),a=Vs(),B=[s(r,0),s(ps(t),0),Gs(t),c,l,s(o,0),us(t),Xs(),s(a,0),Zs(t),fs(),vs(),s(Ls(t),0),Js(t),Ss()],g=[r.id,c.id,a.id,o.id];return n?(B.push(s(ys(t),0)),B.push(Is())):(B.push(s(ki(t),0)),B.push(hs(t)),B.push(Cs(t)),B.push(bs(t)),B.push(Qs(t)),B.push(Us()),B.push(xs(t)),g.push("flick-start")),{gestures:B,sets:{default:g,modipress:g.filter(function(F){return F!=a.id}),none:[]}}}function $n(){return{itemPriority:0,pathResolutionAction:"reject",pathModel:{evaluate:function(i){return"resolve"}}}}function Xe(){return{itemPriority:0,pathResolutionAction:"resolve",pathModel:{evaluate:function(i){return"resolve"}}}}function ss(i){return{itemPriority:1,pathModel:{evaluate:function(t){return t.stats.netDistance>i.flick.startDist?"resolve":null}},pathResolutionAction:"resolve",pathInheritance:"partial"}}function Wi(i,t){var e,n,c=t.key.spec.flick,l=Object.keys(c),r,s=0;try{for(var o=(0,d.__values)(l),a=o.next();!a.done;a=o.next()){var B=a.value,g=Ct(i,B);g>s&&(s=g,r=B)}}catch(F){e={error:F}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(e)throw e.error}}return{dir:r,dist:s}}function os(i){return{itemPriority:1,pathModel:{evaluate:function(t,e,n){var c=Wi(t.stats,n),l=c.dir,r=c.dist;if(r>i.flick.dirLockDist){var s=t.stats.angle,o=Kt(l),a=Math.abs(s-o),B=Math.abs(2*Math.PI+o-s);if(a<=_t||B<=_t)return"resolve"}else if(t.isComplete)return"reject"}},pathResolutionAction:"resolve",pathInheritance:"full"}}function as(i){return{itemPriority:1,pathModel:{evaluate:function(t,e,n,c){if(t.isComplete)return"resolve";var l=Wi(c,n).dir;if(Ct(t.stats,l)n.flickDist)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 c.isComplete?"reject":null}}}}function Ni(){return{itemPriority:-1,pathResolutionAction:"resolve",pathModel:{evaluate:function(i){return"resolve"}}}}function ds(){return{itemPriority:-1,itemChangeAction:"resolve",pathResolutionAction:"resolve",pathModel:{evaluate:function(i){if(i.isComplete)return"reject"}}}}function Ti(){return{itemPriority:-1,itemChangeAction:"resolve",pathResolutionAction:"resolve",pathModel:{evaluate:function(i){if(i.isComplete)return"resolve"}}}}function Pt(i,t){var e,n=(e=i==null?void 0:i.roamingEnabled)!==null&&e!==void 0?e:!0;return{itemPriority:0,itemChangeAction:n?"reject":void 0,pathResolutionAction:"resolve",pathInheritance:!n&&t?"full":"chop",pathModel:{evaluate:function(c){if(c.isComplete&&!c.wasCancelled)return"resolve"}}}}function gs(){return{itemPriority:0,pathResolutionAction:"resolve",pathModel:{evaluate:function(i){if(i.isComplete&&!i.wasCancelled)return"resolve"}}}}function Fs(){return{id:"special-key-start",resolutionPriority:0,contacts:[{model:(0,d.__assign)({},Xe()),endOnResolve:!1}],resolutionAction:{type:"chain",next:"special-key-end",item:"current"}}}function us(i){return{id:"special-key-end",resolutionPriority:0,contacts:[{model:(0,d.__assign)((0,d.__assign)({},Pt(i)),{itemChangeAction:"resolve"}),endOnResolve:!0}],resolutionAction:{type:"complete",item:"none"}}}function Ei(i,t,e){var n={id:"longpress",resolutionPriority:0,contacts:[{model:(0,d.__assign)((0,d.__assign)({},Bs(i,t,e)),{itemPriority:1,pathInheritance:"chop"}),endOnResolve:!1},{model:$n()}],resolutionAction:{type:"chain",next:"subkey-select",selectionMode:"none",item:"none"}};return e?(0,d.__assign)((0,d.__assign)({},n),{rejectionActions:{path:{type:"replace",replace:"longpress-roam"},timer:{type:"replace",replace:"longpress-roam-restore"}}}):n}function ys(i){var t=Ei(i,!1,!0);return(0,d.__assign)((0,d.__assign)({},t),{id:"longpress-roam"})}function Is(){return{id:"longpress-roam-restore",contacts:[{model:{pathModel:{evaluate:function(i){return null}},itemChangeAction:"reject",pathInheritance:"full",pathResolutionAction:"reject",itemPriority:0}}],resolutionPriority:-1,rejectionActions:{item:{type:"replace",replace:"longpress-roam"}},resolutionAction:{type:"chain",next:"longpress-roam"}}}function ki(i){return{id:"flick-start",resolutionPriority:3,contacts:[{model:ss(i)}],resolutionAction:{type:"chain",item:"none",next:"flick-mid"}}}function Qs(i){var t=ki(i);return(0,d.__assign)((0,d.__assign)({},t),{contacts:[(0,d.__assign)((0,d.__assign)({},t.contacts[0]),{model:(0,d.__assign)((0,d.__assign)({},t.contacts[0].model),{baseCoordReplacer:function(e,n){var c=Ai(n),l=Hi(n),r=e.initialSample,s=l(e);if(s>i.flick.triggerDist)return c;var o=i.flick.dirLockDist;if(sRs},enumerable:!1,configurable:!0}),i}();var Ki="kmw-suggest-touched";var As="kmw-suggest-banner-scroller",_i=.666,Pi="swallow-fade-transition",Hs=function(){function i(t,e){this.index=t,this.rtl=e!=null?e:!1,this.constructRoot();var n=this.display=k("span");n.className="kmw-suggestion-text",this.container.appendChild(n)}return Object.defineProperty(i.prototype,"computedStyle",{get:function(){return getComputedStyle(this.display)},enumerable:!1,configurable:!0}),i.prototype.constructRoot=function(){var t=this.div=k("div"),e=t.style;t.className="kmw-suggest-option",t.id=i.BASE_ID+this.index,this.div.suggestion=this;var n=this.container=document.createElement("div");n.className="kmw-suggestion-container";var c=100-We.MARGIN*(We.LONG_SUGGESTION_DISPLAY_LIMIT-1),l=c/We.LONG_SUGGESTION_DISPLAY_LIMIT;n.style.minWidth=l+"%",t.appendChild(n)},i.prototype.matchKeyboardProperties=function(t){var e=this.div;if(t){t.KLC&&(e.lang=t.KLC);var n=t.KFont;n&&n.family&&n.family!=""&&(e.style.fontFamily=this.fontFamily=n.family)}},Object.defineProperty(i.prototype,"suggestion",{get:function(){return this._suggestion},enumerable:!1,configurable:!0}),i.prototype.update=function(t,e){this._suggestion=t;var 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){var c=xt(t.displayAs,e.emSize,e.styleForFont);this._textWidth=c.width}else this._textWidth=0;this.currentWidth=this.collapsedWidth,this.updateLayout()},i.prototype.updateLayout=function(){if(!this.suggestion&&this.index!=0){this.div.style.width="0px";return}else this.div.style.width="";var 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()},i.prototype.updateFade=function(){var t=this;this.div.classList.add(Pi),window.requestAnimationFrame(function(){t.div.classList.remove(Pi)}),this.div.classList.add("kmw-hide-fade-".concat(this.rtl?"left":"right"));var e="kmw-hide-fade-".concat(this.rtl?"right":"left");this.expandedWidth-this.collapsedWidth?this.div.classList.remove(e):this.div.classList.add(e)},Object.defineProperty(i.prototype,"targetCollapsedWidth",{get:function(){return this._collapsedWidth},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"textWidth",{get:function(){return this._textWidth},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"paddingWidth",{get:function(){return this._paddingWidth},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"minWidth",{get:function(){return this._minWidth},set:function(t){this._minWidth=t},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"expandedWidth",{get:function(){return this.minWidth>this.spanWidth?this.minWidth:this.spanWidth},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"spanWidth",{get:function(){var t,e,n=(t=this.textWidth)!==null&&t!==void 0?t:0;return n&&(n+=(e=this.paddingWidth)!==null&&e!==void 0?e:0),n},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"collapsedWidth",{get:function(){var t=this.spanWidthe?this.minWidth:e},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"currentWidth",{get:function(){return this.div.offsetWidth},set:function(t){tthis.expandedWidth&&(t=this.expandedWidth),this.rtl?this.container.style.marginRight="".concat(t-this.expandedWidth,"px"):this.container.style.marginLeft="".concat(t-this.expandedWidth,"px")},enumerable:!1,configurable:!0}),i.prototype.highlight=function(t){var e=this.div;t?e.classList.add(Ki):e.classList.remove(Ki)},i.prototype.isEmpty=function(){return!this._suggestion},i.prototype.generateSuggestionText=function(t){var e=this._suggestion,n,c=k("span");if(c.className="kmw-suggestion-text",e==null)return c;if(e.displayAs==null||e.displayAs=="")n="\xA0";else{var l=t?8238:8237;n=String.fromCharCode(l)+e.displayAs}return c.innerHTML=n,c},i.BASE_ID="kmw-suggestion-",i}();var We=function(i){(0,d.__extends)(t,i);function t(e,n){var c=i.call(this,n||ue.DEFAULT_HEIGHT)||this;return c.type="suggestion",c.currentSuggestions=[],c.options=[],c.separators=[],c.isRTL=!1,c.onSuggestionUpdate=function(l){var r;c.currentSuggestions=l,(r=c.highlightAnimation)===null||r===void 0||r.cancel();for(var s=c.options[0].computedStyle,o={fontSize:s.fontSize,fontFamily:s.fontFamily},a=getComputedStyle(document.body).fontSize,B=Ve(a).val,g=getComputedStyle(c.options[0].container.firstChild),F=c.width/t.LONG_SUGGESTION_DISPLAY_LIMIT,u=new f(g.paddingLeft||"4px"),Q=new f(g.paddingRight||"4px"),y={paddingWidth:u.val+Q.val,emSize:B,styleForFont:o,collapsedWidth:F,minWidth:0},I=0;II){var C=l[I];h.update(C,y)}else h.update(null,y)}c.refreshLayout()},c.refreshLayout=function(){for(var l=[],r=0,s=Math.min(c.currentSuggestions.length,8),o=0;o0&&(l.forEach(function(C){return C.minWidth=C.collapsedWidth+h}),r+=h*l.length),l.splice(0,1)};r0;)g();for(var F=(c.width-r-B)/s,o=0;o0&&(this.options=[],this.separators=[]);for(var n=0;nu.right||a.clientYu.bottom)return null;var Q=null,y=Number.MAX_VALUE;try{for(var I=(0,d.__values)(e.options),h=I.next();!h.done;h=I.next()){var C=h.value,U=C.div.getBoundingClientRect();if(U.left<=a.clientX&&a.clientXi.TRANSITION_TIME;s&&(r=i.TRANSITION_TIME);var o=c.option.expandedWidth-c.option.collapsedWidth,a=r/i.TRANSITION_TIME,B=o*a;c.option.currentWidth=B+c.option.collapsedWidth,s?c.clear():c.pendingAnimation=window.requestAnimationFrame(c._expand),c.setScrollOffset()}},this._collapse=function(l){if(c.startTimestamp!==void 0){var r=l-c.startTimestamp,s=r>i.TRANSITION_TIME;s&&(r=i.TRANSITION_TIME);var o=c.option.expandedWidth-c.option.collapsedWidth,a=1-r/i.TRANSITION_TIME,B=o*a;c.option.currentWidth=B+c.option.collapsedWidth,s?c.clear():c.pendingAnimation=window.requestAnimationFrame(c._collapse),c.setScrollOffset()}},this.scrollContainer=t,this.option=e,this.collapsedScrollOffset=t.scrollLeft,this.rootScrollOffset=t.scrollLeft}return i.prototype.setBaseScroll=function(t){this.collapsedScrollOffset=t,this.option.rtl?t>this.rootScrollOffset&&(this.rootScrollOffset=t):t10)&&(c=0),n.className="kmw-key kmw-key-"+Oe[c]},i.prototype.setToggleState=function(t){var e;switch(e=this.spec.sp,Oe[e]){case"shift":case"shift-on":t===void 0&&(t=Oe[e]=="shift"),this.spec.sp=1+(t?1:0);break;case"special":case"special-on":t===void 0&&(t=Oe[e]=="special"),this.spec.sp=3+(t?1:0);break;default:return}this.setButtonClass()},i.prototype.isFrameKey=function(){var t=this.spec.sp||0;switch(Oe[t]){case"default":case"deadkey":return!1;default:return!0}},i.prototype.allowsKeyTip=function(){return this.isFrameKey()?!1:!this.btn.classList.contains("kmw-spacebar")},i.prototype.highlight=function(t){var e=this.btn.classList;t?e.contains(i.HIGHLIGHT_CLASS)||e.add(i.HIGHLIGHT_CLASS):e.remove(i.HIGHLIGHT_CLASS)},i.prototype.getIdealFontSize=function(t,e,n,c){var l=getComputedStyle(this.btn),r=parseFloat(l.width),s=t.getKeyEmFontSize(),o=this.label?getComputedStyle(this.label).fontFamily:void 0;o&&(l={fontFamily:o,fontSize:l.fontSize,height:l.height});var a=Ve(n.fontSize||"1em");c||(n=l);var B=Ve(n.fontSize||"1em"),g=xt(e,s,n),F=.9,u=.9,Q=2,y,I;g.fontBoundingBoxAscent&&(y=g.fontBoundingBoxAscent+g.fontBoundingBoxDescent);var h=y!=null?y:0;n.height&&n.height.indexOf("px")!=-1&&(I=Number.parseFloat(n.height.substring(0,n.height.indexOf("px"))));var C=r*F/(g.width+Q),U=h&&I?I*u/h:void 0,x=C;return U&&U90)&&(e=0)}var n=document.createElement("div");return n.className="kmw-key-label",e>0&&(n.innerText=String.fromCharCode(e)),n},t.prototype.processSubkeys=function(e,n){var c,l=e.subKeys=this.spec.sk;for(c=0;c0)return this.keys[0].displaysKeyCap},set:function(t){var e,n;try{for(var c=(0,d.__values)(this.keys),l=c.next();!l.done;l=c.next()){var r=l.value;r.displaysKeyCap=t}}catch(s){e={error:s}}finally{try{l&&!l.done&&(n=c.return)&&n.call(c)}finally{if(e)throw e.error}}},enumerable:!1,configurable:!0}),i.prototype.refreshLayout=function(t){var e,n,c=this.element.style,l=t.internalHeight.scaledBy(this.heightFraction);c.maxHeight=c.lineHeight=c.height=l.styleString;var r=.15,s=t.usesFixedHeightScaling?l:f.forScalar(1),o=s.scaledBy(r/2),a=s.scaledBy(1-r);try{for(var B=(0,d.__values)(this.keys),g=B.next();!g.done;g=B.next()){var F=g.value,u=F.btn.parentElement,Q=F.btn,y=u.style;y.height=y.minHeight=s.styleString;var I=Q.style;I.top=o.styleString,I.height=I.lineHeight=I.minHeight=a.styleString,Q.key&&Q.key.refreshLayout(t)}}catch(h){e={error:h}}finally{try{g&&!g.done&&(n=B.return)&&n.call(B)}finally{if(e)throw e.error}}},i}(),tl=Os;var Ms=function(){function i(t,e,n){this.spec=n;var c=this.element=document.createElement("div"),l=c.style;c.className="kmw-key-layer";var r=n.row.length;r>4&&t.device.formFactor=="phone"&&(c.className=c.className+" kmw-5rows"),l.fontFamily="font"in e?e.font:"",this.nextlayer=c.layer=n.id,typeof n.nextlayer=="string"&&(c.nextLayer=this.nextlayer=n.nextlayer);var s=n.row;this.rows=[];for(var o=0;ot.clientY?g.top-t.clientY:t.clientY-g.bottom;F=0||G.className.indexOf("key-blank")>=0))){var m=x.left,X=x.right;if(h>=m&&h<=X)return G;Q=m-h,Q>=0&&Q=0&&Q40&&(y=.6*x.width),m-h>=0&&m-h=0&&h-Xwindow.innerWidth-g-v?(this.cap.style.left=X-g-1+"px",B-=v-1):this.cap.style.left=v+"px",y.left=B-v+"px";var H=getComputedStyle(this.element),E=h.height,W=parseFloat(H.bottom),Te=parseFloat(H.height),Ue=Math.ceil(p/2);this.cap.style.width=g+"px",this.tip.style.height=Ue+"px";var nt=3,ct=Ue-nt+"px";x=="top"?(this.cap.style.top=ct,this.cap.style.bottom=""):(this.cap.style.top="",this.cap.style.bottom=ct);var it=G-Math.floor(U)+p-(x=="top"?Ue:-nt*2);if(this.cap.style.height=it+"px",this.constrain&&Te+W>E){var Ee=Te+W-E;y.height=p-Ee+"px";var lt=Math.max(0,p-Ee-p/2+2);this.cap.style.height=lt+"px"}else W<0&&(y.bottom="0px",this.cap.style.height=Math.max(0,it+W)+"px");if(y.display="block",this.previewHost==n)return;var vt=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,vt),n.setCancellationHandler(function(){return c.show(null,!1,null)}),n.on("startFade",function(){c.element.classList.remove("kmw-preview-fade"),c.element.offsetWidth,c.element.classList.add("kmw-preview-fade")}))}else{this.element.style.display="none",(l=this.previewHost)===null||l===void 0||l.off("preferredOrientation",this.reorient),this.previewHost=null;var Pl=this.preview;this.preview=document.createElement("div"),this.tip.replaceChild(this.preview,Pl),this.element.classList.remove("kmw-preview-fade"),this.orientation=ll}this.key=t,this.state=e},i}(),rl=zs;var nc="kmw-keypreview",Ks="kmw-preview-overlay",_s="kmw-keytip",sl=function(){function i(t){this.state=!1,this.vkbd=t;var e=this.element=document.createElement("div");e.className=nc,e.id="kmw-keytip",e.style.pointerEvents="none",e.style.display="none",this.preview=document.createElement("div"),e.appendChild(this.preview)}return i.prototype.show=function(t,e,n){var c=this,l=this.vkbd,r=t==null?void 0:t.key.spec.displayLayer;if(e&&l.layerGroup.blinkLayer(r),e&&(t==null?void 0:t.offsetParent)){var s=this.vkbd.topContainer.getBoundingClientRect(),o=t.getBoundingClientRect(),a=r!=l.layerId?Ks:"";this.element.className="".concat(nc," ").concat(t.className," ").concat(a),this.element.id="".concat(_s,"-").concat(t.id);var B=this.element.style,g=this.vkbd.currentLayer.element.style.fontFamily;if(B.fontFamily=t.key.spec.font||g,B.left=o.left-s.left+"px",B.top=o.top-s.top+"px",B.width=o.width+"px",B.height=o.height+"px",this.element.style.display="block",this.previewHost==n)return;var F=this.preview;this.previewHost=n,n&&(this.preview=this.previewHost.element,this.element.replaceChild(this.preview,F),n.setCancellationHandler(function(){return c.show(null,!1,null)}),n.on("startFade",function(){c.element.classList.remove("kmw-preview-fade"),c.element.offsetWidth,c.element.classList.add("kmw-preview-fade")}))}else{this.element.style.display="none",this.element.className=nc,this.previewHost=null;var u=this.preview;this.preview=document.createElement("div"),this.element.replaceChild(this.preview,u),this.element.classList.remove("kmw-preview-fade")}this.key=t,this.state=e},i}();function Ie(i){try{if(i=="desktop")return 1;var t=document.documentElement.clientWidth;if(screen.width>t)return 1;var e=screen.width;return ee()?screen.widthscreen.height&&(e=screen.height),Math.round(100*e/t)/100}catch(n){return 1}}var ol=function(){function i(t,e){var n=this;this.directlyEmitsKeys=!0,this.hasModalVisualization=!1,this.deleteRepeater=function(){n.repeatClosure(),n.timerHandle=window.setTimeout(n.deleteRepeater,i.REPEAT_DELAY)},this.source=t;var c=t.stageReports[0].item;c.key.highlight(!0),this.repeatClosure=function(){e(),c.key.highlight(!0)},this.timerHandle=window.setTimeout(this.deleteRepeater,i.INITIAL_DELAY),this.source.on("complete",function(){window.clearTimeout(n.timerHandle),n.timerHandle=void 0,c.key.highlight(!1)})}return i.prototype.cancel=function(){this.deleteRepeater(),this.source.cancel()},i.prototype.currentStageKeyDistribution=function(){return null},i.INITIAL_DELAY=500,i.REPEAT_DELAY=100,i}();var Ps=function(i){(0,d.__extends)(t,i);function t(e,n){if(typeof n!="string"||n=="")throw"The 'layer' parameter for subkey construction must be properly defined.";return i.call(this,e,n)||this}return t.prototype.getId=function(){return"popup-"+this.layer+"-"+this.spec.id},t.prototype.construct=function(e,n,c){var l=this.spec,r=document.createElement("div"),s=e.getDefaultKeyObject(),o=r.style;r.className="kmw-key-square-ex",c&&(o.marginTop="5px"),typeof l.width!="undefined"?o.width=l.width*n.offsetWidth/100+"px":o.width=n.offsetWidth+"px",o.height=n.offsetHeight+"px";var a=document.createElement("div"),B=this.btn=en(a,new $t(this,l.id));this.setButtonClass(),B.id=this.getId();var g=B.style;return g.height=o.height,g.lineHeight=n.style.lineHeight,g.width=o.width,g.position="absolute",B.appendChild(this.label=this.generateKeyText(e)),r.appendChild(B),this.square=r},t.prototype.allowsKeyTip=function(){return!1},t}(Ne),al=Ps;var Bl=.2,js=1.2,gl=3,qs=5,dl=6+gl,$s=function(){function i(t,e,n,c,l){var r=this;this.directlyEmitsKeys=!0,this.shouldLockLayer=!1,this.baseKey=c,this.source=t,this.gestureParams=l,n.layerLocked&&(this.shouldLockLayer=!0),t.on("complete",function(){var m;(m=r.currentSelection)===null||m===void 0||m.key.highlight(!1),r.clear()}),t.on("stage",function(){var m=r.currentSelection;if(m){var X=n.keyEventFromSpec(m.key.spec);X.keyDistribution=r.currentStageKeyDistribution(),n.raiseKeyEvent(X,m)}});var s=t.stageReports[0].sources[0].constructSubview(!0,!1);s.path.on("step",function(m){var X,p;s.path.stats.netDistance>=4&&((X=r.currentSelection)===null||X===void 0||X.key.highlight(!1),(p=m.item)===null||p===void 0||p.key.highlight(!0),r.currentSelection=m.item)}),this.currentSelection=c,c.key.highlight(!0);var o=c.subKeys,a=this.element=document.createElement("div"),B;a.id="kmw-popup-keys";var g=a.style;g.fontFamily=n.fontFamily;var F=getComputedStyle(c);g.fontSize=F.fontSize,g.visibility="hidden";var u=o.length,Q,y;for(Q=Math.min(Math.ceil(u/9),2),y=Math.ceil(u/Q),this.menuWidth=y*c.offsetWidth+y*qs,g.width=this.menuWidth+"px",this.subkeys=[],B=0;B1&&h>0&&(I=!0);var C=c.key.layer;(typeof C!="string"||C=="")&&(C=n.layerId);var U=new al(o[B],C),x=U.construct(n,c,I);this.subkeys.push(x.firstChild),a.appendChild(x)}this.shim=document.createElement("div"),this.shim.id="kmw-popup-shim",n.device.formFactor==V.FormFactor.Phone&&this.selectDefaultSubkey(c,a),n.element.appendChild(this.element),n.topContainer.appendChild(this.shim),this.reposition(n);var G=this.buildPopupRecognitionConfig(n);e({type:"push",config:G})}return i.prototype.buildPopupRecognitionConfig=function(t){var e=this,n=this.element.getBoundingClientRect(),c=this.baseKey.getBoundingClientRect(),l=this.subkeys[0].style,r=Number.parseInt(l.height,10),s=-.666*r,o=3,a=c.bottom-n.bottom,B=new ye(this.element,[s*o,s,-aI.right||F.clientYI.bottom)return null;try{for(var x=(0,d.__values)(e.subkeys),G=x.next();!G.done;G=x.next()){var m=G.value,X=m.getBoundingClientRect(),p=Number.MAX_VALUE,Z=Number.MAX_VALUE;if(X.left<=F.clientX&&F.clientX=F.clientX?X.left-F.clientX:F.clientX-X.right,X.top<=F.clientY&&F.clientY=F.clientY?X.top-F.clientY:F.clientY-X.bottom,p==0&&Z==0)return m;(pa&&(o=a),o<0&&(o=0),r.left=o+"px";var B=c.getBoundingClientRect(),g=l.getBoundingClientRect();r.top=g.top-B.top-e.offsetHeight-gl+"px",r.visibility="visible";var F=t.isEmbedded,u=getComputedStyle(e),Q=parseFloat(u.top),y=0,I=0;Q0){var h=document.createElement("div"),C=h.style;h.id="kmw-popup-callout",n.appendChild(h),C.top=B+"px",C.borderTopWidth=I+"px";var U=js*Q,x=o.width*U,G=this.menuWidth-2*s,m=G1?1:s.x,s.y=s.y<0?0:s.y>1?1:s.y;var o=Et(s,this.buildCorrectiveLayout()),a=o.get(l.item.key.spec),B=Math.min(c.path.stats.duration-n.sources[0].path.stats.duration,this.gestureParams.longpress.waitLength)/(2*this.gestureParams.longpress.waitLength),g=Math.min(c.path.stats.rawDistance,this.gestureParams.longpress.noiseTolerance*4)/(this.gestureParams.longpress.noiseTolerance*8),F=Math.min(B*B,g*g),u=a+F,Q=new Map,y=this.subkeys.find(function(I){return I.keyId==t.baseKey.keyId});return y?Q.set(y.key.spec,u):Q.set(this.baseKey.key.spec,u),Re([o,Q])},i.prototype.cancel=function(){this.clear(),this.source.cancel()},i.prototype.clear=function(){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)},i}(),cc=$s;var eo=function(){function i(t,e,n){var c=this;this.directlyEmitsKeys=!0,this.shouldRestore=!1,this.hasModalVisualization=!1;var l=t.stageReports[0];this.originalLayer=l.sources[0].stateToken,this.source=t,this.completionCallback=function(){e.lockLayer(!1),c.shouldRestore&&(e.layerId=c.originalLayer,e.updateState()),n==null||n()},e.lockLayer(!0),t.on("stage",function(r){var s=r.matchedId;s.includes("modipress")&&s.includes("-end")?c.clear():s.includes("modipress")&&s.includes("-hold")&&(c.shouldRestore=!0)}),t.on("complete",function(){return c.cancel()})}return Object.defineProperty(i.prototype,"isLocked",{get:function(){return this.shouldRestore},enumerable:!1,configurable:!0}),i.prototype.setLocked=function(){this.shouldRestore=!0},Object.defineProperty(i.prototype,"completed",{get:function(){return this.completionCallback===null},enumerable:!1,configurable:!0}),i.prototype.clear=function(){var t=this.completionCallback;this.completionCallback=null,t==null||t()},i.prototype.cancel=function(){this.clear(),this.source.cancel()},i.prototype.currentStageKeyDistribution=function(t){return null},i}(),pt=eo;var to=function(){function i(t,e,n,c,l){var r=this;this.directlyEmitsKeys=!0,this.hasModalVisualization=!1,this.tapIndex=0,this.baseKey=n,this.baseContextToken=c,this.multitaps=[n.key.spec].concat(n.key.spec.multitap),this.sequence=t;var s=function(F){var u;(u=r.modipress)===null||u===void 0||u.clear();var Q=new pt(t,e,function(){r.modipress=e.activeModipress=null});r.modipress=e.activeModipress=Q};this.originalLayer=e.layerId;var o=function(F){return(r.tapIndex+F)%r.multitaps.length},a=function(){l==null||l.setMultitapHint(r.multitaps[o(0)],r.multitaps[o(1)],e)};t.on("complete",function(){var F;(F=r.modipress)===null||F===void 0||F.cancel(),r.clear()});var B=function(F){var u;switch(F.matchedId){case"modipress-hold":r.clear(),t.off("stage",B);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":(u=r.modipress)===null||u===void 0||u.setLocked();return;case"modipress-multitap-start":case"multitap-start":break;default:throw new Error("Unsupported gesture state encountered during multitap: ".concat(F.matchedId))}r.tapIndex=o(1);var Q=r.multitaps[r.tapIndex];a();var y=e.keyEventFromSpec(Q);y.baseTranscriptionToken=r.baseContextToken;var I=F.sources[0].currentSample,h=e.getSimpleTapCorrectionDistances(I,r.baseKey.key.spec);if(I.stateToken!=e.layerId&&!F.matchedId.includes("modipress")){var C=e.layerGroup.findNearestKey((0,d.__assign)((0,d.__assign)({},I),{stateToken:e.layerId})),U=h.get(C.key.spec);U==null&&console.warn("Could not find current layer's key"),h.delete(C.key.spec),h.set(I.item.key.spec,U)}y.keyDistribution=r.currentStageKeyDistribution(h),y.kNextLayer||(y.kNextLayer=r.originalLayer),e.raiseKeyEvent(y,null),F.matchedId=="modipress-multitap-start"&&s(F)};t.on("stage",B);var g=t.stageReports[0];g.matchedId=="modipress-start"&&s(t.stageReports[0]),a()}return i.prototype.currentStageKeyDistribution=function(t){var e=this,n=Re(t),c=n.findIndex(function(y){return y.keySpec==e.baseKey.key.spec});if(c==-1)return bt(this.baseKey)||console.warn("Could not find base key's probability for multitap correction"),n;for(var l=n.splice(c,1)[0].p,r=0,s=[],o=0;o0?h.left=U*Gt*a+"px":h.left="0px",h.height="100%",h.lineHeight="100%",x<0?h.bottom=-x*Gt*a+"px":x>0?h.top=x*Gt*a+"px":h.top="0px",r.flickPreviews.set(y,I),g.appendChild(I)})}var Q=r.hintLabel=document.createElement("div");return Q.className="kmw-key-popup-icon",n||(Q.textContent=o==o.hintSrc?o.hint:(s=o.hintSrc)===null||s===void 0?void 0:s.text,Q.style.fontWeight=Q.textContent=="\u2022"?"bold":""),B.appendChild(Q),r}return Object.defineProperty(t.prototype,"element",{get:function(){return this.div},enumerable:!1,configurable:!0}),t.prototype.refreshLayout=function(){var e=getComputedStyle(this.div),n=Number.parseInt(e.height,10);this.flickPreviews.forEach(function(c){c.style.lineHeight=c.style.height="".concat(n,"px")})},t.prototype.cancel=function(){var e;(e=this.onCancel)===null||e===void 0||e.call(this),this.onCancel=null},t.prototype.setCancellationHandler=function(e){this.onCancel=e},t.prototype.setMultitapHint=function(e,n,c){var l,r,s=Me(e.text,c),o=Me(n.text,c);this.label.textContent=s,this.hintLabel.textContent=o,this.label.style.fontFamily=s!=e.text?"SpecialOSK":(l=e.font)!==null&&l!==void 0?l:this.label.style.fontFamily,this.hintLabel.style.fontFamily=o!=n.text?"SpecialOSK":(r=n.font)!==null&&r!==void 0?r:this.hintLabel.style.fontFamily,this.emit("startFade"),this.clearFlick()},t.prototype.scrollFlickPreview=function(e,n){this.clearHint();var c=this.previewImgContainer.style,l=this.flickEdgeLength*Gt;c.marginLeft="".concat(l*e,"px"),c.marginTop="".concat(l*n,"px");var r=ic(n)<0?"bottom":"top";this.orientation!=r&&(this.orientation=r,this.emit("preferredOrientation",r))},t.prototype.clearFlick=function(){this.previewImgContainer.style.marginTop="0px",this.previewImgContainer.style.marginLeft="0px",this.previewImgContainer.classList.add("kmw-flick-clear")},t.prototype.clearHint=function(){this.hintLabel.classList.add("kmw-hint-clear")},t.prototype.clearAll=function(){this.clearFlick()},t}(ul.default);var no=function(i){(0,d.__extends)(t,i);function t(e){var n=this,c,l;n=i.call(this)||this,n.gestureParams=(0,d.__assign)({},jn),n._layerId="default",n.layerLocked=!1,n.layerIndex=0,n.isStatic=!1,n._fixedWidthScaling=!1,n._fixedHeightScaling=!0,n.stateKeys={K_CAPS:!1,K_NUMLOCK:!1,K_SCROLL:!1},n.activeGestures=[],n.activeModipress=null,n.repeatDelete=function(){this.deleting&&(this.modelKeyClick(this.deleteKey),this.deleting=window.setTimeout(this.repeatDelete,100))}.bind(n),n.config=e,n.config.device=e.device||e.hostDevice,n.config.isEmbedded=e.isEmbedded||!1,e.isStatic&&(n.isStatic=e.isStatic),n._fixedWidthScaling=n.device.touchable&&!n.isStatic,n._fixedHeightScaling=n.device.touchable&&!n.isStatic;var r=document.createElement("div");n.config.styleSheetManager=e.styleSheetManager||new Ce(r);var s;if(e.keyboard)s=n.kbdLayout=e.keyboard.layout(e.device.formFactor),n.layoutKeyboardProperties=e.keyboardMetadata,n.isRTL=e.keyboard.isRTL;else{var o=O.buildDefaultLayout(null,null,e.device.formFactor);s=n.kbdLayout=At.polyfill(o,null,e.device.formFactor),n.layoutKeyboardProperties=null,n.isRTL=!1}"font"in s?n.fontFamily=s.font:n.fontFamily="";var a=e.device.formFactor;n.layoutKeyboard=e.keyboard,n.layoutKeyboard||(n.layoutKeyboard=new q(null)),n.layerGroup=new cl(n,n.layoutKeyboard,a),n.layoutKeyboard.markLayoutCalibrated(a),r.appendChild(n.layerGroup.element),n.kbdDiv=r,n.isStatic||(n.gestureEngine=n.constructGestureEngine()),r.classList.add(e.device.formFactor,"kmw-osk-inner-frame");var B=(l=(c=n.layoutKeyboard)===null||c===void 0?void 0:c.id.replace("Keyboard_",""))!==null&&l!==void 0?l:"",g=B.indexOf("::");g!=-1&&(B=B.substring(g+2));var F="kmw-keyboard-"+B;return n.element.classList.add(F),n}return Object.defineProperty(t.prototype,"layerId",{get:function(){return this._layerId},set:function(e){var n=e!=this._layerId;if(this.layerGroup.layers[e])this._layerId=e,this.layerGroup.activeLayerId=e,this.gestureEngine&&(this.gestureEngine.stateToken=e);else throw new Error("Keyboard ".concat(this.layoutKeyboard.id," does not have a layer with id ").concat(e));n&&(this.updateState(),this.refreshLayout())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"currentLayer",{get:function(){var e;return this.layerId?(e=this.layerGroup)===null||e===void 0?void 0:e.layers[this.layerId]:null},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"lgKey",{get:function(){var e,n;return(n=(e=this.currentLayer)===null||e===void 0?void 0:e.globeKey)===null||n===void 0?void 0:n.btn},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"hkKey",{get:function(){var e,n;return(n=(e=this.currentLayer)===null||e===void 0?void 0:e.hideKey)===null||n===void 0?void 0:n.btn},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"spaceBar",{get:function(){var e,n;return(n=(e=this.currentLayer)===null||e===void 0?void 0:e.spaceBarKey)===null||n===void 0?void 0:n.btn},enumerable:!1,configurable:!0}),t.prototype.constructGestureEngine=function(){var e=this,n=this.kbdLayout.layerMap.default.row.length,c={targetRoot:this.element,mouseEventRoot:document.body,maxRoamingBounds:new ye(this.topContainer,[NaN]),itemIdentifier:function(a,B){return e.layerGroup.findNearestKey(a)}};this.gestureParams.longpress.permitsFlick=function(a){var B=a==null?void 0:a.key.spec.flick;return!B||!(B.n||B.nw||B.ne)};var l=new ht(qn(this.kbdLayout,this.gestureParams),c);l.stateToken=this.layerId;var r={},s=function(a){var B,g;try{for(var F=(0,d.__values)(Object.keys(r)),u=F.next();!u.done;u=F.next()){var Q=u.value;if(Q!=a){var y=r[Q];y.source.terminate(!0)}}}catch(I){B={error:I}}finally{try{u&&!u.done&&(g=F.return)&&g.call(F)}finally{if(B)throw B.error}}},o=new Map;return l.on("inputstart",function(a){var B,g=e.highlightKey(a.currentSample.item,!0);g&&((B=e.gesturePreviewHost)===null||B===void 0||B.cancel(),e.gesturePreviewHost=g);var F=r[a.identifier]={source:a,roamingHighlightHandler:null,key:a.currentSample.item,previewHost:g},u=function(){xe(0).then(function(){var Q=F.previewHost;Q&&(Q.cancel(),e.gesturePreviewHost=null,F.previewHost=null),F.key&&(e.highlightKey(F.key,!1),F.key=null)})};F.roamingHighlightHandler=function(Q){var y,I=Q.item,h=r[a.identifier].key;if(!e.kbdLayout.hasFlicks&&I!=h){e.highlightKey(h,!1),(y=e.gesturePreviewHost)===null||y===void 0||y.cancel(),e.gesturePreviewHost=null;var C=e.highlightKey(I,!0);C&&(e.gesturePreviewHost=C,F.previewHost=C,r[a.identifier].key=I)}},a.path.on("invalidated",u),a.path.on("complete",u),a.path.on("step",F.roamingHighlightHandler)}),l.on("recognizedgesture",function(a){var B;(B=e.activeModipress)===null||B===void 0||B.setLocked(),a.on("complete",function(){var g,F,u;try{for(var Q=(0,d.__values)(a.allSourceIds),y=Q.next();!y.done;y=Q.next()){var I=y.value;!((u=r[I])===null||u===void 0)&&u.previewHost&&(e.gesturePreviewHost=null,r[I].previewHost.cancel()),delete r[I]}}catch(h){g={error:h}}finally{try{y&&!y.done&&(F=Q.return)&&F.call(Q)}finally{if(g)throw g.error}}}),a.on("stage",function(g,F){var u,Q,y=a.allSourceIds.map(function(v){var H;return(H=r[v])===null||H===void 0?void 0:H.previewHost}).find(function(v){return!!v}),I=o.get(a);!I&&y&&!g.matchedId.includes("flick")&&y.clearFlick();var h,C=function(v){var H=function(W){W.key&&(e.highlightKey(W.key,!1),W.key=null),W.source.path.off("step",W.roamingHighlightHandler)};if(h=r[v],h)H(h);else{var E=v;xe(0).then(function(){var W=r[E];W&&H(W)})}};try{for(var U=(0,d.__values)(g.allSourceIds),x=U.next();!x.done;x=U.next()){var G=x.value;C(G)}}catch(v){u={error:v}}finally{try{x&&!x.done&&(Q=U.return)&&Q.call(U)}finally{if(u)throw u.error}}var m=g.item,X=g.sources[0],p=X?X.currentSample:null,Z=null;if(m&&!(I&&I[0].directlyEmitsKeys)){var R=void 0,A=e.getSimpleTapCorrectionDistances(X.currentSample,m.key.spec);I&&(R=I[0].currentStageKeyDistribution(A)),R||(R=Re(A));var Y=!e.layerLocked&&I&&I[0]instanceof cc&&I[0].shouldLockLayer;try{Y&&e.lockLayer(!0),Z=e.modelKeyClick(g.item,p)}finally{Y&&e.lockLayer(!1)}}if(!(a.stageReports.length>1&&g.matchedId!="modipress-end")){var S=a.stageReports[0].item;if(g.matchedId=="special-key-start")m.key.spec.baseKeyID=="K_BKSP"?(y==null||y.cancel(),I=[new ol(a,function(){return e.modelKeyClick(m,p)})]):m.key.spec.baseKeyID=="K_LOPT"&&(a.on("complete",function(){return e.emit("globekey",m,!1)}),s(X.identifier));else if(g.matchedId.indexOf("longpress")>-1)y==null||y.cancel(),I=[new cc(a,F,e,a.stageReports[0].sources[0].baseItem,e.gestureParams)];else if((S==null?void 0:S.key.spec.multitap)&&(g.matchedId=="initial-tap"||g.matchedId=="multitap"||g.matchedId=="modipress-start"))h.previewHost=null,a.on("complete",function(){y==null||y.cancel(),e.gesturePreviewHost=null}),I=[new Fl(a,e,S,Z.contextToken,y)];else if(g.matchedId.indexOf("flick")>-1)I=[new Ri(a,F,e,a.stageReports[0].sources[0].baseItem,e.gestureParams,y)];else if(g.matchedId.includes("modipress")&&g.matchedId.includes("-start"))if(y==null||y.cancel(),e.layerLocked)console.warn("Unexpected state: modipress start attempt during an active modipress");else{I||(I=[]);var L=new pt(a,e,function(){var v=I.indexOf(L);v>-1&&I.splice(v,1),e.activeModipress=null});I.push(L),e.activeModipress=L}else y==null||y.cancel();I&&(e.activeGestures=e.activeGestures.concat(I),o.set(a,I),a.on("complete",function(){var v=e.activeGestures.filter(function(H){return I.includes(H)});e.activeGestures=e.activeGestures.filter(function(H){return!I.includes(H)}),v.forEach(function(H){H instanceof pt&&H.cancel()})}))}})}),l},Object.defineProperty(t.prototype,"element",{get:function(){return this.kbdDiv},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"device",{get:function(){return this.config.device},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"hostDevice",{get:function(){return this.config.hostDevice},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"fontRootPath",{get:function(){return this.config.pathConfig.fonts},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"styleSheetManager",{get:function(){return this.config.styleSheetManager},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"topContainer",{get:function(){return this.config.topContainer},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"isEmbedded",{get:function(){return this.config.isEmbedded},enumerable:!1,configurable:!0}),t.prototype.postInsert=function(){},Object.defineProperty(t.prototype,"width",{get:function(){return this._width},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"height",{get:function(){return this._height},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"layoutWidth",{get:function(){if(this.usesFixedWidthScaling){var e=this.width,n=getComputedStyle(this.element);if(n.border){var c=new f(n.borderWidth).val;e-=c*2}return f.inPixels(e)}else return f.forScalar(1)},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"layoutHeight",{get:function(){if(this.usesFixedHeightScaling){var e=this.height,n=getComputedStyle(this.element);if(n.border){var c=new f(n.borderWidth).val;e-=c*2}return f.inPixels(e)}else return f.forScalar(1)},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"internalHeight",{get:function(){return this.usesFixedHeightScaling?f.inPixels(this.layoutHeight.val-this.getVerticalLayerGroupPadding()):f.forScalar(1)},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"fontSize",{get:function(){return this._fontSize||(this._fontSize=new f("1em")),this._fontSize},set:function(e){this._fontSize=e,this.kbdDiv.style.fontSize=e.styleString},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"usesFixedWidthScaling",{get:function(){return this._fixedWidthScaling},set:function(e){this._fixedWidthScaling=e},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"usesFixedHeightScaling",{get:function(){return this._fixedHeightScaling},set:function(e){this._fixedHeightScaling=e},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"usesFixedPositioning",{get:function(){for(var e=this.element;e;){if(getComputedStyle(e).position=="fixed")return!0;e=e.offsetParent}return!1},enumerable:!1,configurable:!0}),t.prototype.setSize=function(e,n,c){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 f(this._height/8+"px")),c||this.refreshLayout())},t.prototype.getDefaultKeyObject=function(){var e=(0,d.__assign)({},M.DEFAULT_KEY);return M.polyfill(e,this.layoutKeyboard,this.kbdLayout,this.layerId),e},t.prototype.getTouchCoordinatesOnKeyboard=function(e){var n={x:e.targetX,y:e.targetY};return n.x/=this.layerGroup.element.offsetWidth,n.y/=this.kbdDiv.offsetHeight,n},t.prototype.getSimpleTapCorrectionDistances=function(e,n){var c=this.getTouchCoordinatesOnKeyboard(e),l=this.layerGroup.element,r=l.offsetWidth,s=this.kbdDiv.offsetHeight;if(!r||!s)return new Map;var o=r/s,a=ri(this.kbdLayout.getLayer(this.layerId),o);return Et(c,a)},t.prototype.keyTarget=function(e){var n=e;try{if(n){if(n.classList.contains("kmw-key"))return tn(n);if(n.parentNode&&n.parentNode.classList.contains("kmw-key"))return tn(n.parentNode);if(n.firstChild&&n.firstChild.classList.contains("kmw-key"))return tn(n.firstChild)}}catch(c){}return null},t.prototype.cancelDelete=function(){this.deleting&&window.clearTimeout(this.deleting),this.deleting=0},t.prototype.modelKeyClick=function(e,n,c){var l=this.initKeyEvent(e);return n&&(l.source=n),c&&(l.keyDistribution=c),this.raiseKeyEvent(l,e)},t.prototype.initKeyEvent=function(e){this.highlightKey(e,!1);var n=e.key?e.key.spec:null;return n?this.keyEventFromSpec(n):null},t.prototype.keyEventFromSpec=function(e){var n=this.layoutKeyboard.constructKeyEvent(e,this.device,this.stateKeys);return n.srcKeyboard=this.layoutKeyboard,n},t.prototype._UpdateVKShiftStyle=function(e){var n,c;e||(e=this.layerId);var l=this.layerGroup.layers[e];if(!!l&&(this.gestureEngine&&(this.gestureEngine.stateToken=e),!!(!((n=this.layoutKeyboard)===null||n===void 0)&&n.usesDesktopLayoutOnDevice(this.device)))){var r=["K_CAPS","K_NUMLOCK","K_SCROLL"],s=[l.capsKey,l.numKey,l.scrollKey];for(c=0;c=0)){var c=e.key.allowsKeyTip(),l=this.activeGestures.find(function(r){return r.hasModalVisualization});return n=l?!1:n,e.key.highlight(n),n&&c?this.gesturePreviewHost?null:this.showGesturePreview(e):null}},t.prototype.getKeyEmFontSize=function(){if(!this.fontSize)return 0;if(this.device.formFactor=="desktop"){var e=.8;return this.fontSize.scaledBy(e).val}else{var n=getComputedStyle(document.body).fontSize,c=Ve(n).val,l=1;if(!this.isStatic){if(this.fontSize.absolute)return this.fontSize.val;l=this.fontSize.val}return c*l}},t.prototype.updateState=function(){if(!!this.currentLayer){var e,n=this.kbdDiv.childNodes[0].childNodes;this.nextLayer=this.layerId,this.currentLayer.nextlayer&&(this.nextLayer=this.currentLayer.nextlayer),this.layerGroup.activeLayerId=this.layerId,this._UpdateVKShiftStyle()}},t.prototype.refreshLayout=function(){var e=this.device,n=1;e.OS==V.OperatingSystem.iOS&&!this.isEmbedded&&(n=n/Ie(this.device.formFactor));var c;this.height&&(c=this.computedAdjustedOskHeight(this.height));var l=this.kbdDiv.style;this.usesFixedHeightScaling&&this.height&&(l.height=l.maxHeight=this.height+"px"),l.fontSize=this.fontSize.scaledBy(n).styleString;var r=this.width&&this.height,s=getComputedStyle(this.kbdDiv),o=s.height!=""&&s.height!="auto";if(this.showLanguage(),r)this._computedWidth=this.width,this._computedHeight=this.height;else if(o){if(this._computedWidth=parseInt(s.width,10),!this._computedWidth){var a=getComputedStyle(this.kbdDiv.firstElementChild);this._computedWidth=parseInt(a.width,10)}this._computedHeight=parseInt(s.height,10)}else return;if(!this.isStatic){var B=this.gestureEngine.config.maxRoamingBounds;B.updatePadding([-.333*this.currentLayer.rowHeight]),this.gestureParams.longpress.flickDist=.25*this.currentLayer.rowHeight,this.gestureParams.flick.startDist=.15*this.currentLayer.rowHeight,this.gestureParams.flick.dirLockDist=.35*this.currentLayer.rowHeight,this.gestureParams.flick.triggerDist=.75*this.currentLayer.rowHeight}this.currentLayer&&this.currentLayer.refreshLayout(this,this._computedHeight-this.getVerticalLayerGroupPadding())},t.prototype.getVerticalLayerGroupPadding=function(){var e=getComputedStyle(this.layerGroup.element),n=parseInt(e.paddingTop,10)||0,c=parseInt(e.paddingBottom,10)||0;return n+c},t.prototype.computedAdjustedOskHeight=function(e){if(!this.layerGroup)return e;var n=this.layerGroup.layers,c=0;for(var l in n){var r=n[l],s=r.rows.length,o=Math.floor(e/(s==0?1:s)),a=s*o;a>c&&(c=a)}var B=0,g=c+B;return g},t.prototype.appendStyleSheet=function(){var e=this,n=this.layoutKeyboard,c=this.layoutKeyboardProperties;if(c!=null){this.styleSheet&&this.styleSheet.parentNode&&this.styleSheet.parentNode.removeChild(this.styleSheet);var l=c.textFont,r=c.oskFont;this.styleSheetManager.addStyleSheetForFont(l,this.fontRootPath,this.device.OS),this.styleSheetManager.addStyleSheetForFont(r,this.fontRootPath,this.device.OS),this.config.specialFont&&this.styleSheetManager.addStyleSheetForFont(this.config.specialFont,"",this.device.OS);var s=this.addFontStyle(l,r);n!=null&&typeof n.oskStyling=="string"&&(s=s+n.oskStyling),s&&(this.styleSheet=Se(s),this.styleSheetManager.linkStylesheet(this.styleSheet)),this.styleSheetManager.allLoadedPromise().then(function(){return e.refreshLayout()})}},t.prototype.addFontStyle=function(e,n){var c="",l=function(r){return r.family.replace(/\u0022/g,"").replace(/,/g,'","')};return(e||n)&&(c='\n.kmw-key-text {\n font-family: "'.concat(l(n||e),'";\n}\n\n.kmw-suggestion-text {\n font-family: "').concat(l(e||n),'";\n}\n')),c},t.buildDocumentationKeyboard=function(e,n,c,l,r,s){var o,a,B=this;if(!e)return null;var g=typeof l=="undefined"?"desktop":l,F=typeof r=="undefined"?"default":r,u={};u.formFactor=g,g!="desktop"?(u.OS=V.OperatingSystem.iOS,u.touchable=!0):(u.OS=V.OperatingSystem.Windows,u.touchable=!1);var Q=e.layout(g),y=new V("other",u.formFactor,u.OS,u.touchable),I=new t({keyboard:e,keyboardMetadata:n,hostDevice:y,isStatic:!0,topContainer:null,pathConfig:c,styleSheetManager:null});I.layerGroup.element.className=I.kbdDiv.className,I.layerGroup.element.classList.add(u.formFactor+"-static");var h=I.kbdDiv.childNodes[0],C=document.createElement("div");C.classList.add(u.OS.toLowerCase(),u.formFactor),Q!=null?(I.layerId=F,I.layerGroup.activeLayerId=F,I.setSize(800,s),I.fontSize=Di(y,s,!1),C.style.fontSize=I.element.style.fontSize,I.refreshLayout(),h.style.height=I.kbdDiv.style.height,h.style.maxHeight=I.kbdDiv.style.maxHeight):h.innerHTML="

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

",h.style.border="1px solid #ccc",I.updateState();var U=function(){return(0,d.__awaiter)(B,void 0,void 0,function(){var R,A,Y,S,L,v,H;return(0,d.__generator)(this,function(E){switch(E.label){case 0:if(!document.contains(h))return[3,4];E.label=1;case 1:return E.trys.push([1,,3,4]),[4,I.styleSheetManager.allLoadedPromise()];case 2:E.sent(),R=I.styleSheet,R&&h.appendChild(R),A=[].concat(I.styleSheetManager.sheets);try{for(Y=(0,d.__values)(A),S=Y.next();!S.done;S=Y.next())L=S.value,L!=R&&(L.href||(I.styleSheetManager.unlink(L),document.head.appendChild(L)))}catch(W){v={error:W}}finally{try{S&&!S.done&&(H=Y.return)&&H.call(Y)}finally{if(v)throw v.error}}return I.refreshLayout(),I.styleSheet=null,I.shutdown(),[3,4];case 3:return x.disconnect(),[7];case 4:return[2]}})})},x=new MutationObserver(U);x.observe(document.body,{childList:!0,subtree:!0}),C.append(h);try{for(var G=(0,d.__values)(ve.STYLESHEET_FILES),m=G.next();!m.done;m=G.next()){var X=m.value,p="".concat(c.resources,"/osk/").concat(X),Z=I.styleSheetManager.linkExternalSheet(p,!0);Z.parentNode.removeChild(Z),C.appendChild(Z)}}catch(R){o={error:R}}finally{try{m&&!m.done&&(a=G.return)&&a.call(G)}finally{if(o)throw o.error}}return I.appendStyleSheet(),C},t.prototype.onHide=function(){this.hkKey&&this.highlightKey(this.hkKey,!1)},t.prototype.optionKey=function(e,n,c){n.indexOf("K_LOPT")>=0?this.emit("globekey",e,c):n.indexOf("K_ROPT")>=0&&c&&this.emit("hiderequested",e)},t.prototype.showGesturePreview=function(e){var n=this.keytip,c=getComputedStyle(e),l=Number.parseInt(c.height,10),r=Number.parseInt(c.width,10),s=new yl(e,this.device.formFactor=="phone",r,l);if(n==null){var o=e.key;o.setPreview(s)}else n.show(e,!0,s);return s.refreshLayout(),s},t.prototype.createKeyTip=function(){if(this.keytip==null)if(this.device.formFactor=="phone"){var e=this.isEmbedded;this.keytip=new rl(this,e)}else this.keytip=new sl(this);this.keytip&&this.keytip.element&&this.element.appendChild(this.keytip.element)},t.prototype.createGlobeHint=function(){return this.config.embeddedGestureConfig.createGlobeHint?this.config.embeddedGestureConfig.createGlobeHint(this):null},t.prototype.shutdown=function(){var e;this.styleSheet&&this.styleSheet.parentNode&&this.styleSheet.parentNode.removeChild(this.styleSheet),this.activeGestures.forEach(function(n){return n.cancel()}),this.gestureEngine&&this.gestureEngine.destroy(),this.deleting&&window.clearTimeout(this.deleting),(e=this.keytip)===null||e===void 0||e.show(null,!1,null)},t.prototype.lockLayer=function(e){this.layerLocked=e},t.prototype.raiseKeyEvent=function(e,n){if(e.kName=="K_LOPT"||e.kName=="K_ROPT")return this.optionKey(n,e.kName,!0),{};var c={},l=function(r,s){var o,a;c.contextToken=(o=r==null?void 0:r.transcription)===null||o===void 0?void 0:o.token;var B=(a=r==null?void 0:r.transcription)===null||a===void 0?void 0:a.transform;c.alteredText=r&&(!B||_e(B))};return this.layerLocked&&(e.kNextLayer=this.layerId),this.emit("keyevent",e,l),c},t.specialCharacters=Ne.specialCharacters,t}(Il.default),Ze=no;var Ql=N(T(),1),hl=function(i){(0,d.__extends)(t,i);function t(){return i!==null&&i.apply(this,arguments)||this}return t}(Ql.default),mt=hl,Xt=function(i){(0,d.__extends)(t,i);function t(){return i!==null&&i.apply(this,arguments)||this}return Object.defineProperty(t.prototype,"enabled",{get:function(){return!0},set:function(e){},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"activate",{get:function(){return!0},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"conditionsMet",{get:function(){return!0},enumerable:!1,configurable:!0}),t}(hl);var co=function(){function i(){this.map={}}return i.prototype.promiseForTouchpoint=function(t){return this.map[t]||(this.map[t]=new J),this.map[t]},i.prototype.maintainTouches=function(t){for(var e,n,c=Object.keys(this.map),l=0;l0&&(B-=this.bannerView.height+5),this.vkbd.setSize(this.computedWidth,B,e);var g=this._Box.style;g.width=g.maxWidth=this.computedWidth+"px",g.height=g.maxHeight=this.computedHeight+"px",this.vkbd.showLanguage()}else{var g=this._Box.style;g.width="auto",g.height="auto",g.maxWidth=g.maxHeight=""}}}},t.prototype.refreshLayoutIfNeeded=function(e){this.needsLayout&&this.refreshLayout(e)},t.prototype.postKeyboardLoad=function(){this._Visible=!1,this.postKeyboardAdjustments(),this.displayIfActive&&this.present()},t.prototype.loadActiveKeyboard=function(){var e,n,c,l,r,s,o;this.setBoxStyling(),this.vkbd&&this.vkbd.shutdown(),this.keyboardView=null,this.needsLayout=!0,this._Box.innerHTML="",this.uiStyleSheetManager.unlinkAll(),this.kbdStyleSheetManager.unlinkAll();var a=bl(this.config);try{for(var B=(0,d.__values)(t.STYLESHEET_FILES),g=B.next();!g.done;g=B.next()){var F=g.value,u="".concat(a).concat(F);this.uiStyleSheetManager.linkExternalSheet(u)}}catch(I){e={error:I}}finally{try{g&&!g.done&&(n=B.return)&&n.call(B)}finally{if(e)throw e.error}}this.headerView&&this._Box.appendChild(this.headerView.element),this._Box.appendChild(this.banner.element),(c=this.bannerController)===null||c===void 0||c.configureForKeyboard((l=this.keyboardData)===null||l===void 0?void 0:l.keyboard,(r=this.keyboardData)===null||r===void 0?void 0:r.metadata);var Q=this.keyboardView=this._GenerateKeyboardView((s=this.keyboardData)===null||s===void 0?void 0:s.keyboard,(o=this.keyboardData)===null||o===void 0?void 0:o.metadata);if(this._Box.appendChild(Q.element),Q.postInsert(),this.footerView&&this._Box.appendChild(this.footerView.element),this.banner.appendStyles(),this.vkbd){this.vkbd.createKeyTip();var y=this.vkbd.createGlobeHint();y&&this._Box.appendChild(y.element),this.vkbd.appendStyleSheet()}this.postKeyboardLoad()},t.prototype._GenerateKeyboardView=function(e,n){var c=this.targetDevice;return this.vkbd&&this.vkbd.shutdown(),this._Box.className="",e==null&&!c.touchable?new jt:e&&e.layout(c.formFactor)?this._GenerateVisualKeyboard(e,n):!e||!n?this._GenerateVisualKeyboard(null,null):new $i(e)},t.prototype._GenerateVisualKeyboard=function(e,n){var c=this,l=this.targetDevice,r=bl(this.config),s=new Ze({keyboard:e,keyboardMetadata:n,device:l,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:["".concat(r,"/keymanweb-osk.ttf")],path:""}});return s.on("keyevent",function(o,a){return c.emit("keyevent",o,a)}),s.on("globekey",function(o,a){return c.emit("globekey",o,a)}),s.on("hiderequested",function(o){c.doHide(!0),c.emit("hiderequested",o)}),this._Box.className=l.formFactor+" "+l.OS.toLowerCase()+" kmw-osk-frame",s},t.prototype.present=function(){if(!!this.mayShow()){if(this.keyboardView.updateState(),this._Box.style.display="block",this.refreshLayoutIfNeeded(),this.keyboardView instanceof Ze&&this.keyboardView.showLanguage(),this._Visible=!0,this._Box.style.opacity="1",this._Box.style.visibility=="hidden"){var e=this;window.setTimeout(function(){e._Box.style.visibility="visible"},0)}this.setDisplayPositioning()}},t.prototype.startHide=function(e){if(!!this.mayHide(e)){e&&(this.activationModel.enabled=!!(this.keyboardData.keyboard.isCJK||this.hostDevice.touchable));var n=null;this._Box&&this.hostDevice.touchable&&!(this.keyboardView instanceof jt)&&this.config.allowHideAnimations?n=this.useHideAnimation():n=Promise.resolve(!0);var c=this;n.then(function(l){l&&c.finalizeHide()}),this.doHide(e)}},t.prototype.finalizeHide=function(){if(!(document.body.className.indexOf("osk-always-visible")>=0&&this.hostDevice.formFactor=="desktop")){if(this._Box){var e=this._Box.style;e.display="none",e.transition="",e.opacity="1",this._Visible=!1}this.vkbd&&this.vkbd.onHide()}},t.prototype.mayShow=function(){return!(!this.activationModel.conditionsMet||!this.keyboardView||this.keyboardView instanceof jt||!this.activationModel.enabled||!this._Box)},t.prototype.mayHide=function(e){return!(this.activationModel.conditionsMet&&!this.mayDisable||this.activationModel instanceof Xt||!e&&this.hostDevice.formFactor=="desktop"&&document.body.className.indexOf("osk-always-visible")>=0)},t.prototype.useHideAnimation=function(){var e=this._Box.style,n=this;return new Promise(function(c){var l=function(){return n._Box.removeEventListener("transitionend",l,!1),n._Box.removeEventListener("webkitTransitionEnd",l,!1),n._Box.removeEventListener("transitioncancel",l,!1),n._Box.removeEventListener("webkitTransitionCancel",l,!1),n._animatedHideTimeout!=0&&window.clearTimeout(n._animatedHideTimeout),n._animatedHideTimeout=0,n._Visible&&n.activationModel.conditionsMet?(e.transition="",e.opacity="1",c(!1),!1):(c(!0),!0)},r=function(){n._Box.removeEventListener("transitionrun",r,!1),n._Box.removeEventListener("webkitTransitionRun",r,!1),n._Box.addEventListener("transitionend",l,!1),n._Box.addEventListener("webkitTransitionEnd",l,!1),n._Box.addEventListener("transitioncancel",l,!1),n._Box.addEventListener("webkitTransitionCancel",l,!1)};n._Box.addEventListener("transitionrun",r,!1),n._Box.addEventListener("webkitTransitionRun",r,!1),e.transition="opacity 0.5s linear 0",e.opacity="0",n._animatedHideTimeout=window.setTimeout(l,200)})},t.prototype.hideNow=function(){if(!(!this.mayHide(!1)||!this._Box)){this._animatedHideTimeout&&(window.clearTimeout(this._animatedHideTimeout),this._animatedHideTimeout=0);var e=this._Box.style;e.transition="",e.opacity="0",this.finalizeHide()}},t.prototype.shutdown=function(){this.removeBaseMouseEventListeners(),this.removeBaseTouchEventListeners();var e=this._Box;e.parentElement&&e.parentElement.removeChild(e),this.kbdStyleSheetManager.unlinkAll(),this.uiStyleSheetManager.unlinkAll(),this.bannerController.shutdown()},t.prototype.getRect=function(){var e={};return e.left=e.left=K(this._Box),e.top=e.top=_(this._Box),e.width=this.computedWidth,e.height=this.computedHeight,e},t.prototype.isEnabled=function(){return this.displayIfActive},t.prototype.isVisible=function(){return this._Visible},t.prototype.hide=function(){this.activationModel.enabled=!1,this.startHide(!0)},t.prototype.show=function(e){arguments.length>0?this.activationModel.enabled=e:this.activationModel.conditionsMet&&(this.activationModel.enabled=!this.activationModel.enabled)},t.prototype.doShow=function(e){this.legacyEvents.callEvent("show",e)},t.prototype.doHide=function(e){var n={HiddenByUser:e};this.legacyEvents.callEvent("hide",n)},t.prototype.addEventListener=function(e,n){this.legacyEvents.addEventListener(e,n)},t.prototype.removeEventListener=function(e,n){this.legacyEvents.removeEventListener(e,n)},t.STYLESHEET_FILES=["kmwosk.css","globe-hint.css"],t}(Ul.default),ve=io;var xl=N(T(),1);var lo=function(i){(0,d.__extends)(t,i);function t(e){var n=i.call(this)||this;return n.mouseCancellingHandler=function(c){return c.preventDefault(),c.cancelBubble=!0,!1},n._element=n.buildTitleBar(),n.helpEnabled=!1,n.configEnabled=!1,e&&(n.element.onmousedown=e.mouseDownHandler),n}return Object.defineProperty(t.prototype,"helpEnabled",{get:function(){return this._helpEnabled},set:function(e){this._helpEnabled=e,this._helpButton.style.display=e?"inline":"none"},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"configEnabled",{get:function(){return this._configEnabled},set:function(e){this._configEnabled=e,this._configButton.style.display=e?"inline":"none"},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"layoutHeight",{get:function(){return t.DISPLAY_HEIGHT},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"element",{get:function(){return this._element},enumerable:!1,configurable:!0}),t.prototype.setPinCJKOffset=function(){this._unpinButton.style.left="15px"},t.prototype.showPin=function(e){this._unpinButton.style.display=e?"block":"none"},t.prototype.setTitle=function(e){this._caption.innerHTML=e},t.prototype.setTitleFromKeyboard=function(e){var n=""+(e==null?void 0:e.name)+"";this._caption.innerHTML=n},t.prototype.buildTitleBar=function(){var e=this,n=k("div");n.id="keymanweb_title_bar",n.className="kmw-title-bar";var c=this._caption=k("span");c.className="kmw-title-bar-caption",c.style.color="#fff",n.appendChild(c);var l=this._closeButton=this.buildCloseButton();return this._closeButton.onclick=function(){return e.emit("close"),!1},n.appendChild(l),l=this._helpButton=this.buildHelpButton(),this._helpButton.onclick=function(){return e.emit("help"),!1},n.appendChild(l),l=this._configButton=this.buildConfigButton(),this._configButton.onclick=function(){return e.emit("config"),!1},n.appendChild(l),l=this._unpinButton=this.buildUnpinButton(),this._unpinButton.onclick=function(){return e.emit("unpin"),!1},n.appendChild(l),n},t.prototype.buildCloseButton=function(){var e=k("div");return e.id="kmw-close-button",e.className="kmw-title-bar-image",e.onmousedown=this.mouseCancellingHandler,e},t.prototype.buildHelpButton=function(){var e=k("div");return e.id="kmw-help-image",e.className="kmw-title-bar-image",e.title="KeymanWeb Help",e.onmousedown=this.mouseCancellingHandler,e},t.prototype.buildConfigButton=function(){var e=k("div");return e.id="kmw-config-image",e.className="kmw-title-bar-image",e.title="KeymanWeb Configuration Options",e.onmousedown=this.mouseCancellingHandler,e},t.prototype.buildUnpinButton=function(){var e=k("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},t.prototype.refreshLayout=function(){},t.DISPLAY_HEIGHT=f.inPixels(20),t}(xl.default),lc=lo;var pl=N(T(),1);var ro=function(i){(0,d.__extends)(t,i);function t(e){var n=i.call(this)||this;return n.mouseCancellingHandler=function(c){return c.preventDefault(),c.cancelBubble=!0,!1},n._element=n.buildResizeBar(),e&&(n._resizeHandle.onmousedown=e.mouseDownHandler),n}return Object.defineProperty(t.prototype,"layoutHeight",{get:function(){return t.DISPLAY_HEIGHT},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"element",{get:function(){return this._element},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"handle",{get:function(){return this._resizeHandle},enumerable:!1,configurable:!0}),t.prototype.allowResizing=function(e){this._resizeHandle.style.display=e?"block":"none"},t.prototype.buildResizeBar=function(){var e=this,n=k("div");n.className="kmw-footer",n.onmousedown=this.mouseCancellingHandler;var c=k("div");c.className="kmw-footer-caption",c.innerHTML='KeymanWeb',c.id="keymanweb-osk-footer-caption",c.addEventListener("dblclick",function(r){return e.emit("showbuild"),!1},!1),n.appendChild(c);var l=k("div");return l.className="kmw-footer-resize",n.appendChild(l),this._resizeHandle=l,n},t.prototype.refreshLayout=function(){},t.DISPLAY_HEIGHT=f.inPixels(16),t}(pl.default),Gl=ro;var ml=function(){function i(t,e,n){this.x=t,this.y=e,n&&(this.source=n)}return i.fromEvent=function(t){var e;if(window.TouchEvent&&t instanceof TouchEvent||t.changedTouches?e=t.changedTouches[0]:e=t,e.pageX)return new i(e.pageX,e.pageY,t);if(e.clientX){var n=e.clientX+document.body.scrollLeft,c=e.clientY+document.body.scrollTop;return new i(n,c,t)}else return new i(null,null,t)},i}(),so=function(){function i(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}return i.prototype.restore=function(){document.onmousemove=this._VPreviousMouseMove,document.onmouseup=this._VPreviousMouseUp,document.body.style.cursor&&(document.body.style.cursor=this._VPreviousCursor)},i.prototype.matchesCausingClick=function(t){return this._VPreviousMouseButton==(typeof t.which=="undefined"?t.button:t.which)},i}(),oo=function(){function i(t){this.startHandler=this._VMoveMouseDown.bind(this),this.cursorType=t}return Object.defineProperty(i.prototype,"enabled",{get:function(){return this._enabled},set:function(t){this._enabled=t},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"isActive",{get:function(){return!!this._mouseStartSnapshot},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"mouseDownHandler",{get:function(){return this.startHandler},enumerable:!1,configurable:!0}),i.prototype._VMoveMouseDown=function(t){return!t||!this._enabled?!0:(this._mouseStartSnapshot||(this._mouseStartSnapshot=new so(t)),this._startCoord=ml.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)},i.prototype._VMoveMouseMove=function(t){if(!t||!this.enabled)return!0;if(t.preventDefault(),t.cancelBubble=!0,this._mouseStartSnapshot.matchesCausingClick(t)){var e=ml.fromEvent(t),n=e.x-this._startCoord.x,c=e.y-this._startCoord.y;return this.onDragMove(n,c),!1}else return this._VMoveMouseUp(t)},i.prototype._VMoveMouseUp=function(t){return t?(this._mouseStartSnapshot.restore(),this._mouseStartSnapshot=null,t.preventDefault(),t.cancelBubble=!0,this.onDragRelease(),!1):!0},i}(),rc=oo;var ao=function(i){(0,d.__extends)(t,i);function t(){var e=i!==null&&i.apply(this,arguments)||this;return e._enabled=!0,e.actValue=null,e}return Object.defineProperty(t.prototype,"activate",{get:function(){return this._enabled&&!!this.actValue},enumerable:!1,configurable:!0}),t.prototype.checkState=function(e){this.activate!=e&&this.emit("activate",this.activate)},Object.defineProperty(t.prototype,"enabled",{get:function(){return this._enabled},set:function(e){var n=this.activate;this._enabled=e,this.checkState(n)},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"activationTrigger",{get:function(){return this.actValue},set:function(e){var n=this.activate,c=this.actValue;this.actValue=e,this.checkState(n),c!=e&&this.emit("triggerchange",e)},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"conditionsMet",{get:function(){return!!this.activationTrigger},enumerable:!1,configurable:!0}),t}(mt),Vt=ao;var Xl=function(i){(0,d.__extends)(t,i);function t(){return i.call(this,"KeymanWeb_OnScreenKeyboard")||this}return t.prototype.loadWithDefaults=function(e){return(0,d.__assign)((0,d.__assign)({},e),this.load())},t.prototype.load=function(){var e=i.prototype.load.call(this,function(n,c){switch(c){case"version":return n;default:return Number.parseInt(n,10)}});return e.width||delete e.width,e.height||delete e.height,e},t.prototype.save=function(e){i.prototype.save.call(this,e)},t}(Fe);var Bo=function(i){(0,d.__extends)(t,i);function t(e){var n,c,l=this;e.activator=e.activator||new Vt,l=i.call(this,e)||this,l.userPositioned=!1,l.specifiedPosition=!1,l.noDrag=!1,l.layoutSerializer=new Xl,l.restorePosition=function(F){var u=this._Visible,Q=new J;this.emit("dragmove",Q.corePromise),this.loadPersistedLayout(),this.userPositioned=!1,F||(delete this.dfltX,delete this.dfltY),this.savePersistedLayout(),u&&this.present(),this.titleBar.showPin(!1),Q.resolve(),this.doResizeMove()}.bind(l),l.typedActivationModel.on("triggerchange",function(){return l.setDisplayPositioning()}),document.body.appendChild(l._Box),l.titleBar=new lc(l.titleDragHandler),l.titleBar.on("help",function(){l.legacyEvents.callEvent("helpclick",{})}),l.titleBar.on("config",function(){l.legacyEvents.callEvent("configclick",{})}),l.titleBar.on("close",function(){return l.startHide(!0)}),l.titleBar.on("unpin",function(){return l.restorePosition(!0)}),l.resizeBar=new Gl(l.resizeDragHandler),l.resizeBar.on("showbuild",function(){return l.emit("showbuild")}),l.headerView=l.titleBar;var r=function(F){var u=l.headerView;if(u&&u instanceof lc)switch(F){case"configclick":u.configEnabled=l.legacyEvents.listenerCount("configclick")>0;break;case"helpclick":u.helpEnabled=l.legacyEvents.listenerCount("helpclick")>0;break;default:return}},s=new Yt(l),o=new Yt(l.legacyEvents);try{for(var a=(0,d.__values)([s,o]),B=a.next();!B.done;B=a.next()){var g=B.value;g.on("listeneradded",r),g.on("listenerremoved",r)}}catch(F){n={error:F}}finally{try{B&&!B.done&&(c=a.return)&&c.call(a)}finally{if(n)throw n.error}}return l.loadPersistedLayout(),l}return Object.defineProperty(t.prototype,"typedActivationModel",{get:function(){return this.activationModel},enumerable:!1,configurable:!0}),t.prototype._Unload=function(){this.keyboardView=null,this.bannerView=null,this._Box=null},t.prototype.setBoxStyling=function(){var e=this._Box.style;e.zIndex="9999",e.display="none",e.width="auto",e.position="absolute"},t.prototype.postKeyboardAdjustments=function(){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()},t.prototype.isEnabled=function(){return this.displayIfActive},t.prototype.isVisible=function(){return this._Visible},t.prototype.savePersistedLayout=function(){var e=this.getPos(),n={visible:this.displayIfActive?1:0,userSet:this.userPositioned?1:0,left:e.left,top:e.top,_version:ne.CURRENT.toString()};this.vkbd&&(n.width=this.width.val,n.height=this.height.val),this.layoutSerializer.save(n)},t.prototype.loadPersistedLayout=function(){var 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;var n=e._version,c=n===void 0,l=e.width,r=e.height;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),(c||!n)&&(this.headerView&&this.headerView.layoutHeight.absolute&&(r+=this.headerView.layoutHeight.val),this.footerView&&this.footerView.layoutHeight.absolute&&(r+=this.footerView.layoutHeight.val)),this.setSize(l,r),(this.x==-1||this.y==-1||!this._Box)&&(this.userPositioned=!1),this.x.9*screen.width&&(s=.9*screen.width),r.width=s+"px",this.setSize(s,this.computedHeight,!0)}if("height"in e){var o=e.height-(n.offsetHeight-l.offsetHeight);o<.1*screen.height&&(o=.1*screen.height),o>.5*screen.height&&(o=.5*screen.height),r.height=o+"px",r.fontSize=o/8+"px",this.setSize(this.computedWidth,o,!0)}"nosize"in e&&(this.resizingEnabled=!e.nosize)}"nomove"in e&&(this.noDrag=e.nomove,this.movementEnabled=!this.noDrag),this.savePersistedLayout()}},t.prototype.getPos=function(){var e=this._Box,n={left:this._Visible?e.offsetLeft:this.x,top:this._Visible?e.offsetTop:this.y};return n},t.prototype.setPos=function(e){if(typeof this._Box!="undefined"){if(this.userPositioned){var n=e.left,c=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 c!="undefined"&&(c<0&&(c=0),this.userPositioned&&(this._Box.style.top=c+"px",this.y=c))}this.titleBar.showPin(this.userPositioned)}},t.prototype.setDisplayPositioning=function(){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{var 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=_(n)+n.offsetHeight+"px")}this.specifiedPosition=!1},t.prototype.presentAtPosition=function(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())},t.prototype.present=function(){if(!!this.mayShow()){this.titleBar.showPin(this.userPositioned),i.prototype.present.call(this);var e={};e.x=this._Box.offsetLeft,e.y=this._Box.offsetTop,e.userLocated=this.userPositioned,this.doShow(e)}},t.prototype.startHide=function(e){i.prototype.startHide.call(this,e),e&&this.savePersistedLayout()},t.prototype.show=function(e){e!==void 0?i.prototype.show.call(this,e):i.prototype.show.call(this),this.savePersistedLayout()},t.prototype.userLocated=function(){return this.userPositioned},Object.defineProperty(t.prototype,"movementEnabled",{get:function(){return this.titleDragHandler.enabled},set:function(e){this.titleDragHandler.enabled=e,this.titleBar.showPin(e&&this.userPositioned)},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"resizingEnabled",{get:function(){return this.resizeDragHandler.enabled},set:function(e){this.resizeDragHandler.enabled=e,this.resizeBar.allowResizing(e)},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"isBeingMoved",{get:function(){return this.titleDragHandler.isActive},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"isBeingResized",{get:function(){return this.resizeDragHandler.isActive},enumerable:!1,configurable:!0}),t.prototype.enableMoveResizeHandlers=function(){this.titleDragHandler.enabled=!this.noDrag,this.resizeDragHandler.enabled=!0},Object.defineProperty(t.prototype,"titleDragHandler",{get:function(){var e=this;return this._moveHandler?this._moveHandler:(this._moveHandler=new(function(n){(0,d.__extends)(c,n);function c(){return n.call(this,"move")||this}return c.prototype.onDragStart=function(){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 J,e.emit("dragmove",this.dragPromise.corePromise)},c.prototype.onDragMove=function(l,r){e.titleBar.showPin(!0),e.userPositioned=!0,e._Box.style.left=this.startX+l+"px",e._Box.style.top=this.startY+r+"px";var s=e.getRect();e.setSize(s.width,s.height,!0),e.x=s.left,e.y=s.top},c.prototype.onDragRelease=function(){e.vkbd&&(e.vkbd.currentKey=null),this.dragPromise.resolve(),this.dragPromise.then(function(){e.userPositioned=!0,e.doResizeMove(),e.savePersistedLayout()}),this.dragPromise=null},c}(rc)),this._moveHandler)},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"resizeDragHandler",{get:function(){var e=this;return this._resizeHandler?this._resizeHandler:(this._resizeHandler=new(function(n){(0,d.__extends)(c,n);function c(){return n.call(this,"se-resize")||this}return c.prototype.onDragStart=function(){this.startWidth=e.computedWidth,this.startHeight=e.computedHeight,this.dragPromise&&this.dragPromise.resolve(),this.dragPromise=new J,e.emit("resizemove",this.dragPromise.corePromise)},c.prototype.onDragMove=function(l,r){var s=this.startWidth+l,o=this.startHeight+r;s<.2*screen.width&&(s=.2*screen.width),o<.1*screen.height&&(o=.1*screen.height),s>.9*screen.width&&(s=.9*screen.width),o>.5*screen.height&&(o=.5*screen.height),e.setSize(s,o,!0)},c.prototype.onDragRelease=function(){e.vkbd&&(e.vkbd.currentKey=null),e.vkbd&&(this.startWidth=e.computedWidth,this.startHeight=e.computedHeight),e.refreshLayout(),this.dragPromise.resolve(),this.dragPromise.then(function(){e.doResizeMove(),e.savePersistedLayout()}),this.dragPromise=null},c}(rc)),this._resizeHandler)},enumerable:!1,configurable:!0}),t}(ve),$e=Bo;var go=function(i){(0,d.__extends)(t,i);function t(e){var n=this;return e.isEmbedded?e.activator=e.activator||new Xt:e.activator=e.activator||new Vt,n=i.call(this,e)||this,n.isResizing=!1,n.restorePosition=function(c){}.bind(n),document.body.appendChild(n._Box),n}return t.prototype._Unload=function(){this.keyboardView=null,this.bannerView=null,this._Box=null},t.prototype.setBoxStyling=function(){var e=this._Box.style;e.zIndex="9999",e.display="none",e.width="100%",e.position="fixed"},t.prototype.refreshLayout=function(e){if(!this.isResizing){try{this.isResizing=!0,this.doResize()}finally{this.isResizing=!1}i.prototype.refreshLayout.call(this,e)}},t.prototype.doResize=function(){if(this.vkbd){var e=this.getDefaultKeyboardHeight();this.setSize(this.getDefaultWidth(),e+this.banner.height)}},t.prototype.postKeyboardAdjustments=function(){this.doResize()},t.prototype.getDefaultKeyboardHeight=function(){var e,n,c=this.targetDevice;if(this.configuration.heightOverride)return this.configuration.heightOverride();var l=(e=document==null?void 0:document.documentElement)===null||e===void 0?void 0:e.clientWidth,r=(n=document==null?void 0:document.documentElement)===null||n===void 0?void 0:n.clientHeight;if(typeof l=="undefined"&&(l=Math.min(screen.height,screen.width),r=Math.max(screen.height,screen.width),ee())){var s=l;l=r,r=s}var o=Math.floor(Math.min(r,l)/2),a=o;return c.formFactor=="phone"&&(ee()?a=Math.floor(r/1.6):a=Math.floor(r/2.4)),this.targetDevice.OS==V.OperatingSystem.iOS&&(a=a/Ie(this.targetDevice.formFactor)),a},t.prototype.getDefaultWidth=function(){var e,n=this.targetDevice;if(this.configuration.widthOverride)return this.configuration.widthOverride();var c;return c=(e=document==null?void 0:document.documentElement)===null||e===void 0?void 0:e.clientWidth,typeof c=="undefined"&&(this.targetDevice.OS==V.OperatingSystem.iOS?c=window.innerWidth:n.OS==V.OperatingSystem.Android?c=screen.availWidth:c=screen.width),c},t.prototype.setRect=function(e){},t.prototype.getPos=function(){var e=this._Box,n={left:this._Visible?e.offsetLeft:this.x,top:this._Visible?e.offsetTop:this.y};return n},t.prototype.setPos=function(e){},t.prototype.setDisplayPositioning=function(){var e=this._Box.style;this.vkbd&&(e.position="fixed",e.left=e.bottom="0px",e.border="none",e.borderTop="1px solid gray")},t.prototype.present=function(){i.prototype.present.call(this),this.legacyEvents.callEvent("show",{})},t}(ve),sc=go;var Fo=function(i){(0,d.__extends)(t,i);function t(){var e=i!==null&&i.apply(this,arguments)||this;return e.flag=!0,e}return Object.defineProperty(t.prototype,"enabled",{get:function(){return this.flag},set:function(e){this.activate=e},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"activate",{get:function(){return this.flag},set:function(e){this.flag!=e&&(this.flag=e,this.emit("activate",e))},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"conditionsMet",{get:function(){return!0},enumerable:!1,configurable:!0}),t}(mt),oc=Fo;var uo=function(i){(0,d.__extends)(t,i);function t(e){var n=this;return e.activator=e.activator||new oc,n=i.call(this,e)||this,n.restorePosition=function(c){}.bind(n),n}return Object.defineProperty(t.prototype,"element",{get:function(){return this._Box},enumerable:!1,configurable:!0}),t.prototype._Unload=function(){this.keyboardView=null,this.bannerView=null,this._Box=null},t.prototype.setBoxStyling=function(){var e=this._Box.style;e.display="none",e.position="relative"},t.prototype.postKeyboardAdjustments=function(){},t.prototype.getDefaultKeyboardHeight=function(){return this.keyboardView instanceof Ze?this.keyboardView.height:this.computedHeight},t.prototype.getDefaultWidth=function(){return this.computedWidth},t.prototype.setRect=function(e){},t.prototype.getPos=function(){var e=this._Box,n={left:this._Visible?e.offsetLeft:void 0,top:this._Visible?e.offsetTop:void 0};return n},t.prototype.setPos=function(e){},t.prototype.present=function(){i.prototype.present.call(this),this.legacyEvents.callEvent("show",{})},t.prototype.setDisplayPositioning=function(){},t.prototype.allowsDeviceChange=function(e){return!0},t}(ve),ac=uo;var Fc={};pc(Fc,{AnchoredOSKView:function(){return dc},FloatingOSKView:function(){return gc},InlinedOSKView:function(){return yo}});function Bc(i){return{hostDevice:i.config.hostDevice,pathConfig:i.config.paths,predictionContextManager:i.contextManager.predictionContext,isEmbedded:!1}}var dc=function(i){(0,d.__extends)(t,i);function t(e,n){var c=(0,d.__assign)((0,d.__assign)({},Bc(e)),n||{});return i.call(this,c)||this}return t}(sc),gc=function(i){(0,d.__extends)(t,i);function t(e,n){var c=(0,d.__assign)((0,d.__assign)({},Bc(e)),n||{});return i.call(this,c)||this}return t}($e),yo=function(i){(0,d.__extends)(t,i);function t(e,n){var c=(0,d.__assign)((0,d.__assign)({},Bc(e)),n||{});return i.call(this,c)||this}return t}(ac);var Vl=N(T(),1),Io=function(i){(0,d.__extends)(t,i);function t(){var e=i!==null&&i.apply(this,arguments)||this;return e.events=new Vl.default,e.changed=!1,e}return t.prototype.focus=function(){var e=this.getElement();e.focus&&e.focus()},t.prototype.isForcingScroll=function(){return!1},t.prototype.dispatchInputEventOn=function(e){var n;window.InputEvent&&(n=new InputEvent("input",{bubbles:!0,cancelable:!1})),e&&n&&e.dispatchEvent(n)},t}(Cn),ce=Io;var Qo=function(i){(0,d.__extends)(t,i);function t(e){var n=i.call(this)||this;return n.root=e,n._cachedSelectionStart=-1,n}return Object.defineProperty(t.prototype,"isSynthetic",{get:function(){return!1},enumerable:!1,configurable:!0}),t.isSupportedType=function(e){return e=="email"||e=="search"||e=="text"||e=="url"},t.prototype.getElement=function(){return this.root},t.prototype.clearSelection=function(){this.getCaret(),this.root.value=this.root.value._kmwSubstring(0,this.processedSelectionStart)+this.root.value._kmwSubstring(this.processedSelectionEnd),this.setCaret(this.processedSelectionStart)},t.prototype.isSelectionEmpty=function(){return this.root.selectionStart==this.root.selectionEnd},t.prototype.hasSelection=function(){return!0},t.prototype.invalidateSelection=function(){this._cachedSelectionStart=-1},t.prototype.getCaret=function(){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},t.prototype.getDeadkeyCaret=function(){return this.getCaret()},t.prototype.setCaret=function(e){this.setSelection(e,e,"none")},t.prototype.setSelection=function(e,n,c){var l=this.root.value._kmwCodePointToCodeUnit(e),r=this.root.value._kmwCodePointToCodeUnit(n);this.root.setSelectionRange(l,r,c),this.processedSelectionStart=e,this.processedSelectionEnd=n,this.forceScroll(),this.root.setSelectionRange(l,r,c)},t.prototype.forceScroll=function(){var e=this.getElement(),n=e.selectionStart,c=e.selectionEnd;this._activeForcedScroll=!0;try{e.blur(),e.focus()}finally{e.selectionStart=n,e.selectionEnd=c,this._activeForcedScroll=!1}},t.prototype.isForcingScroll=function(){return this._activeForcedScroll},t.prototype.getSelectionDirection=function(){return this.root.selectionDirection},t.prototype.getTextBeforeCaret=function(){return this.getCaret(),this.getText()._kmwSubstring(0,this.processedSelectionStart)},t.prototype.getSelectedText=function(){return this.getCaret(),this.getText()._kmwSubstring(this.processedSelectionStart,this.processedSelectionEnd)},t.prototype.setTextBeforeCaret=function(e){this.getCaret();var n=this.processedSelectionEnd-this.processedSelectionStart,c=this.getSelectionDirection(),l=e._kmwLength();this.root.value=e+this.getText()._kmwSubstring(this.processedSelectionStart),this.setSelection(l,l+n,c)},t.prototype.setTextAfterCaret=function(e){var n=this.getCaret(),c=this.getSelectionDirection();this.root.value=this.getTextBeforeCaret()+e,this.setSelection(this.processedSelectionStart,this.processedSelectionEnd,c)},t.prototype.getTextAfterCaret=function(){return this.getCaret(),this.getText()._kmwSubstring(this.processedSelectionEnd)},t.prototype.getText=function(){return this.root.value},t.prototype.deleteCharsBeforeCaret=function(e){if(e>0){var n=this.getTextBeforeCaret(),c=this.processedSelectionStart;e>c&&(e=c),this.adjustDeadkeys(-e),this.setTextBeforeCaret(n.kmwSubstring(0,c-e)),this.setCaret(c-e)}},t.prototype.insertTextBeforeCaret=function(e){if(!!e){var n=this.getCaret(),c=this.getTextBeforeCaret(),l=this.getText()._kmwSubstring(this.processedSelectionStart);this.adjustDeadkeys(e._kmwLength()),this.root.value=c+e+l,this.setCaret(n+e._kmwLength())}},t.prototype.handleNewlineAtCaret=function(){var e=this.root;e&&(e.type=="search"||e.type=="submit")?(e.disabled=!1,e.form.submit()):this.events.emit("unhandlednewline",e)},t.prototype.doInputEvent=function(){this.dispatchInputEventOn(this.root)},t}(ce),et=Qo;var ho=function(i){(0,d.__extends)(t,i);function t(e){var n=i.call(this)||this;return n.root=e,n._cachedSelectionStart=-1,n}return Object.defineProperty(t.prototype,"isSynthetic",{get:function(){return!1},enumerable:!1,configurable:!0}),t.prototype.getElement=function(){return this.root},t.prototype.clearSelection=function(){this.getCaret(),this.root.value=this.root.value._kmwSubstring(0,this.processedSelectionStart)+this.root.value._kmwSubstring(this.processedSelectionEnd),this.setCaret(this.processedSelectionStart)},t.prototype.isSelectionEmpty=function(){return this.root.selectionStart==this.root.selectionEnd},t.prototype.hasSelection=function(){return!0},t.prototype.invalidateSelection=function(){this._cachedSelectionStart=-1},t.prototype.getCaret=function(){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},t.prototype.getDeadkeyCaret=function(){return this.getCaret()},t.prototype.setCaret=function(e){this.setSelection(e,e,"none")},t.prototype.setSelection=function(e,n,c){var l=this.root.value._kmwCodePointToCodeUnit(e),r=this.root.value._kmwCodePointToCodeUnit(n);this.root.setSelectionRange(l,r,c),this.processedSelectionStart=e,this.processedSelectionEnd=n,this.forceScroll(),this.root.setSelectionRange(l,r,c)},t.prototype.forceScroll=function(){var e=this.getElement(),n=e.selectionStart,c=e.selectionEnd;this._activeForcedScroll=!0;try{e.blur(),e.focus()}finally{e.selectionStart=n,e.selectionEnd=c,this._activeForcedScroll=!1}},t.prototype.isForcingScroll=function(){return this._activeForcedScroll},t.prototype.getSelectionDirection=function(){return this.root.selectionDirection},t.prototype.getTextBeforeCaret=function(){return this.getCaret(),this.getText()._kmwSubstring(0,this.processedSelectionStart)},t.prototype.setTextBeforeCaret=function(e){this.getCaret();var n=this.processedSelectionEnd-this.processedSelectionStart,c=this.getSelectionDirection(),l=e._kmwLength();this.root.value=e+this.getText()._kmwSubstring(this.processedSelectionStart),this.setSelection(l,l+n,c)},t.prototype.setTextAfterCaret=function(e){var n=this.getCaret(),c=this.getSelectionDirection();this.root.value=this.getTextBeforeCaret()+e,this.setSelection(this.processedSelectionStart,this.processedSelectionEnd,c)},t.prototype.getTextAfterCaret=function(){return this.getCaret(),this.getText()._kmwSubstring(this.processedSelectionEnd)},t.prototype.getSelectedText=function(){return this.getCaret(),this.getText()._kmwSubstring(this.processedSelectionStart,this.processedSelectionEnd)},t.prototype.getText=function(){return this.root.value},t.prototype.deleteCharsBeforeCaret=function(e){if(e>0){var n=this.getTextBeforeCaret(),c=this.processedSelectionStart;e>c&&(e=c),this.adjustDeadkeys(-e),this.setTextBeforeCaret(n.kmwSubstring(0,c-e)),this.setCaret(c-e)}},t.prototype.insertTextBeforeCaret=function(e){if(!!e){var n=this.getCaret(),c=this.getTextBeforeCaret(),l=this.getText()._kmwSubstring(this.processedSelectionStart);this.adjustDeadkeys(e._kmwLength()),this.root.value=c+e+l,this.setCaret(n+e._kmwLength())}},t.prototype.handleNewlineAtCaret=function(){this.insertTextBeforeCaret("\n")},t.prototype.doInputEvent=function(){this.dispatchInputEventOn(this.root)},t}(ce),uc=ho;var yc=function(){function i(t,e){this.node=t,this.offset=e}return i}(),Ic=function(){function i(t,e){this.start=t,this.end=e}return i}(),Qe=function(){function i(t,e){this.cmd=t,this.stateType=e}return i}(),Co=function(i){(0,d.__extends)(t,i);function t(e){var n=i.call(this)||this;if(n.root=e,e.contentWindow&&e.contentWindow.document&&e.contentWindow.document.designMode=="on")n.doc=e.contentWindow.document,n.docRoot=e.contentWindow.document.documentElement;else throw"Specified IFrame is not in design-mode!";return n}return Object.defineProperty(t.prototype,"isSynthetic",{get:function(){return!1},enumerable:!1,configurable:!0}),t.prototype.getElement=function(){return this.root},t.prototype.focus=function(){this.doc.defaultView.focus()},t.prototype.isSelectionEmpty=function(){return this.hasSelection()?this.doc.getSelection().isCollapsed:!0},t.prototype.hasSelection=function(){var e=this.doc.getSelection(),n=document.getSelection();return n.anchorNode==e.anchorNode&&n.focusNode==e.focusNode,!0},t.prototype.clearSelection=function(){if(this.hasSelection()){var e=this.doc.getSelection();e.isCollapsed||e.deleteFromDocument()}else console.warn("Attempted to clear an unowned Selection!")},t.prototype.invalidateSelection=function(){},t.prototype.getCarets=function(){var e=this.doc.getSelection(),n=e.anchorNode.compareDocumentPosition(e.focusNode);if(e.isCollapsed){var c=new yc(e.anchorNode,e.anchorOffset);return new Ic(c,c)}else{var l=new yc(e.anchorNode,e.anchorOffset),r=new yc(e.focusNode,e.focusOffset);return l.node==r.node&&(n=r.offset-l.offset>0?2:4),n&2?new Ic(l,r):new Ic(r,l)}},t.prototype.getDeadkeyCaret=function(){return this.getTextBeforeCaret().kmwLength()},t.prototype.getTextBeforeCaret=function(){if(!this.hasSelection())return this.getText();var e=this.getCarets().start;return e.node.nodeType!=3?"":e.node.textContent.substr(0,e.offset)},t.prototype.getSelectedText=function(){return""},t.prototype.getTextAfterCaret=function(){if(!this.hasSelection())return"";var e=this.getCarets().end;return e.node.nodeType!=3?"":e.node.textContent.substr(e.offset)},t.prototype.getText=function(){return this.docRoot.innerText},t.prototype.deleteCharsBeforeCaret=function(e){if(!(!this.hasSelection()||e<=0)){var 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}var c=this.doc.createRange(),l=n.offset-n.node.nodeValue.substr(0,n.offset)._kmwSubstr(-e).length;c.setStart(n.node,l),c.setEnd(n.node,n.offset),this.adjustDeadkeys(-e),c.deleteContents()}},t.prototype.insertTextBeforeCaret=function(e){if(!!this.hasSelection()){var n=this.getCarets().start,c=e._kmwLength(),l=this.doc.getSelection();if(c!=0){this.adjustDeadkeys(c);var r=this.root.ownerDocument.createRange();if(n.node.nodeType==3){var s=n.node;s.insertData(n.offset,e),r.setStart(s,n.offset+e.length)}else{var o=this.doc.createTextNode(e),a=this.doc.createRange();a.setStart(n.node,n.offset),a.collapse(!0),a.insertNode(o),r.setStart(o,e.length)}r.collapse(!0),l.removeAllRanges();try{l.addRange(r)}catch(B){n.node.parentElement.scrollIntoView(),l.addRange(r)}l.collapseToEnd()}}},t.prototype.handleNewlineAtCaret=function(){},t.prototype.setTextAfterCaret=function(e){if(!!this.hasSelection()){var n=this.getCarets().end,c=e._kmwLength(),l=this.doc.getSelection();if(c!=0)if(n.node.nodeType==3){var r=n.node;r.replaceData(n.offset,r.length,e)}else{var s=n.node.ownerDocument.createTextNode(e),o=this.root.ownerDocument.createRange();o.setStart(n.node,n.offset),o.collapse(!0),o.insertNode(s)}}},t.prototype.saveProperties=function(){var e=[new Qe("backcolor",1),new Qe("fontname",1),new Qe("fontsize",1),new Qe("forecolor",1),new Qe("bold",0),new Qe("italic",0),new Qe("strikethrough",0),new Qe("subscript",0),new Qe("superscript",0),new Qe("underline",0)];this.doc.defaultView&&e.push(new Qe("hilitecolor",1));for(var n=0;n0?2:4),n&2?new hc(l,r):new hc(r,l)}},t.prototype.getDeadkeyCaret=function(){return this.getTextBeforeCaret().kmwLength()},t.prototype.getTextBeforeCaret=function(){if(!this.hasSelection())return this.getText();var e=this.getCarets().start;return e.node.nodeType!=3?"":e.node.textContent.substr(0,e.offset)},t.prototype.getSelectedText=function(){return""},t.prototype.getTextAfterCaret=function(){if(!this.hasSelection())return"";var e=this.getCarets().end;return e.node.nodeType!=3?"":e.node.textContent.substr(e.offset)},t.prototype.getText=function(){return this.root.innerText},t.prototype.deleteCharsBeforeCaret=function(e){if(!(!this.hasSelection()||e<=0)){var 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}var c=this.root.ownerDocument.createRange(),l=n.offset-n.node.nodeValue.substr(0,n.offset)._kmwSubstr(-e).length;c.setStart(n.node,l),c.setEnd(n.node,n.offset),this.adjustDeadkeys(-e),c.deleteContents()}},t.prototype.insertTextBeforeCaret=function(e){if(!!this.hasSelection()){var n=this.getCarets().start,c=e._kmwLength(),l=this.root.ownerDocument.getSelection();if(c!=0){this.adjustDeadkeys(c);var r=this.root.ownerDocument.createRange();if(n.node.nodeType==3){var s=n.node;s.insertData(n.offset,e),r.setStart(s,n.offset+e.length)}else{var o=n.node.ownerDocument.createTextNode(e),a=this.root.ownerDocument.createRange();a.setStart(n.node,n.offset),a.collapse(!0),a.insertNode(o),r.setStart(o,e.length)}r.collapse(!0),l.removeAllRanges();try{l.addRange(r)}catch(B){n.node.parentElement.scrollIntoView(),l.addRange(r)}l.collapseToEnd()}}},t.prototype.handleNewlineAtCaret=function(){},t.prototype.setTextAfterCaret=function(e){if(!!this.hasSelection()){var n=this.getCarets().end,c=e._kmwLength(),l=this.root.ownerDocument.getSelection();if(c!=0)if(n.node.nodeType==3){var r=n.node;r.replaceData(n.offset,r.length,e)}else{var s=n.node.ownerDocument.createTextNode(e),o=this.root.ownerDocument.createRange();o.setStart(n.node,n.offset),o.collapse(!0),o.insertNode(s)}}},t.prototype.doInputEvent=function(){this.dispatchInputEventOn(this.root)},t}(ce),nn=bo;function P(i,t){var e;if(!i)return!1;if(i.Window)return t=="Window";if(i.defaultView)e=i.defaultView[t];else if(i.ownerDocument)e=i.ownerDocument.defaultView[t];else if(i.target){var n=i;this.instanceof(n.target,"Window")?e=n.target[t]:this.instanceof(n.target,"Document")?e=n.target.defaultView[t]:this.instanceof(n.target,"HTMLElement")&&(e=n.target.ownerDocument.defaultView[t])}return e?i instanceof e:!1}function cn(i){if(P(i,"HTMLInputElement"))return new et(i);if(P(i,"HTMLTextAreaElement"))return new uc(i);if(P(i,"HTMLIFrameElement")){var t=i;return t.contentWindow&&t.contentWindow.document&&t.contentWindow.document.designMode=="on"?new re(t):i.isContentEditable?new nn(i):null}else if(i.isContentEditable)return new nn(i);return null}var tt=function(){function i(){var t=this;this.pending=!1;var e=this.bg=document.createElement("div"),n=this.lb=document.createElement("div"),c=this.lt=document.createElement("div"),l=this.gr=document.createElement("div"),r=this.bx=document.createElement("div");e.className="kmw-wait-background",n.className="kmw-wait-box",this.dismiss=null,c.className="kmw-wait-text",l.className="kmw-wait-graphic",r.className="kmw-alert-close",n.onmousedown=n.onclick=function(s){r.style.display=="block"&&(e.style.display="none",t.dismiss&&t.dismiss())},n.addEventListener("touchstart",n.onclick,!1),e.onmousedown=e.onclick=function(s){s.preventDefault(),s.stopPropagation()},e.addEventListener("touchstart",e.onclick,!1),n.appendChild(r),n.appendChild(c),n.appendChild(l),e.appendChild(n),document.body.appendChild(e)}return Object.defineProperty(i.prototype,"rootElement",{get:function(){return this.bg},enumerable:!1,configurable:!0}),i.prototype.wait=function(t){var e=this,n=this.bg;typeof n=="undefined"||n==null||(t?(this.pending=!0,window.setTimeout(function(){e.pending&&(window.scrollTo(0,0),e.bx.style.display="none",e.lt.className="kmw-wait-text",e.lt.innerHTML=t,e.gr.style.display="block",n.style.display="block")},1e3)):this.pending&&(this.lt.innerHTML="",this.pending=!1,n.style.display="none"))},i.prototype.alert=function(t,e){var 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},i.prototype.shutdown=function(){this.bg.parentNode.removeChild(this.bg)},i}();function Zt(){return document.readyState==="complete"?Promise.resolve():new Promise(function(i,t){var e=function(){window.removeEventListener("load",e),i()};window.addEventListener("load",e)})}var Zl=function(i){(0,d.__extends)(t,i);function t(){return i!==null&&i.apply(this,arguments)||this}return t.prototype.initialize=function(e){var n=this;this._options?this._options=(0,d.__assign)((0,d.__assign)({},this._options),e):this._options=(0,d.__assign)({},e),i.prototype.initialize.call(this,e),this._options=e,this._ui=e.ui,this._attachType=e.attachType,Zt().then(function(){var c;e.useAlerts&&!n.alertHost?n._alertHost=new tt:!e.useAlerts&&n.alertHost&&((c=n._alertHost)===null||c===void 0||c.shutdown(),n._alertHost=null)})},Object.defineProperty(t.prototype,"options",{get:function(){return this._options},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"attachType",{get:function(){return this._attachType},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"alertHost",{get:function(){return this._alertHost},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"signalUser",{set:function(e){(!e||e!=this.alertHost)&&this.alertHost.shutdown(),this._alertHost=e},enumerable:!1,configurable:!0}),t.prototype.debugReport=function(){var e=i.prototype.debugReport.call(this);return e.attachType=this.attachType,e.ui=this._ui,e.keymanEngine="app/browser",e},t.prototype.onRuleFinalization=function(e,n){var c=e.transcription.transform;_e(c)||n instanceof ce&&(n.changed=!0)},t}(pn);var vl=(0,d.__assign)({ui:"",attachType:"",useAlerts:!0},Gn);var Cc=function(){function i(t,e,n){this.interface=t,this.keyboard=e}return i}();function fe(i){var t=i==null?void 0:i.target;return Le(t)}function Le(i){var t;if(i==null)return null;if(i.body&&(i=i.body),i.nodeType==3&&(i=i.parentNode),P(i,"HTMLInputElement")){var e=i.type.toLowerCase();if(!(e=="text"||e=="search"))return null}return(t=i._kmwAttachment)===null||t===void 0?void 0:t.interface}var fl=N(T(),1);var bc=function(i){(0,d.__extends)(t,i);function t(e,n){var c=this;if(!e)throw new Error("Cannot attach to a null/undefined document");return c=i.call(this)||this,c.baseFont="",c.appliedFont="",c.embeddedPageContexts=[],c._inputList=[],c._sortedInputs=[],c._InputModeObserverCore=function(l){c.disableInputModeObserver();try{for(var r=0,s=l;r=0:!1,a=s.target.className.indexOf("kmw-disabled")>=0;if(o&&!a?c._EnableControl(s.target):!o&&a&&c._DisableControl(s.target),!a&&s.attributeName=="readonly"){var B=s.oldValue?s.oldValue!=null:!1,g=s.target;if(g instanceof g.ownerDocument.defaultView.HTMLInputElement||g instanceof g.ownerDocument.defaultView.HTMLTextAreaElement){var F=g.readOnly;B&&!F?c._EnableControl(s.target):!B&&F&&c._DisableControl(s.target)}}}},c._AutoAttachObserverCore=function(l){for(var r=[],s=[],o=0;o=0)},t.prototype.enableInputElement=function(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!==void 0?n:"text",this.disableInputModeObserver(),e.inputMode="none",this.enableInputModeObserver(),e.classList.add("keymanweb-font"),this._inputList.push(e),this.emit("enabled",e)))},t.prototype.disableInputElement=function(e){var n;if(!!e)if(e.ownerDocument.defaultView&&e instanceof e.ownerDocument.defaultView.HTMLIFrameElement||e instanceof HTMLIFrameElement)this._DetachFromIframe(e);else{if(this.isAttached(e)){var c=(n=e._kmwAttachment)===null||n===void 0?void 0:n.inputMode;this.disableInputModeObserver(),e.inputMode=c,this.enableInputModeObserver()}var l=e.className.indexOf("keymanweb-font");l>=0&&(e.className=e.className.replace("keymanweb-font","").trim());var r=this.inputList.indexOf(e);r>-1&&this._inputList.splice(r,1),this.emit("disabled",e)}},t.prototype.enableTouchElement=function(e){return this.isKMWDisabled(e)?(this.emit("disabled",e),!1):(this.isAttached(e)||this.setupElementAttachment(e),this.enableInputElement(e),!0)},t.prototype.disableTouchElement=function(e){if(this.isAttached(e)){var n=e._kmwAttachment.inputMode;this.disableInputModeObserver(),e.inputMode=n,this.enableInputModeObserver()}},t.prototype._AttachToIframe=function(e){var n=this;try{var c=e.contentWindow.document;if(c){if(c.designMode.toLowerCase()=="on")this.setupElementAttachment(e),c.body._kmwAttachment=e._kmwAttachment,this._inputList.push(e),this.emit("enabled",e);else if(this.embeddedPageContexts.filter(function(r){return r.document==c}).length==0){var l=new t(c,(0,d.__assign)((0,d.__assign)({},this.options),{owner:e}));this.embeddedPageContexts.push(l),l.on("enabled",function(r){return n.emit("enabled",r)}),l.on("disabled",function(r){return n.emit("disabled",r)}),l.install(this.manualAttach)}}}catch(r){}},t.prototype._DetachFromIframe=function(e){var n=this,c=function(){n.clearElementAttachment(e);var o=n._inputList.indexOf(e);o!=-1&&n._inputList.splice(o,1),n.emit("disabled",e)};try{var l=e.contentWindow.document;if(l){if(l.designMode.toLowerCase()=="on")l.body._kmwAttachment=null,c();else for(var r=0;r=0&&(e.className=n.replace("kmw-disabled","").trim())},t.prototype.listInputs=function(){for(var e=[],n=document.getElementsByTagName("input"),c=document.getElementsByTagName("textarea"),l=0;l=l.length?c-l.length:c,c=c<0?c+l.length:c,l[c]},t.prototype._GetDocumentEditables=function(e){var n=[];if(e.ownerDocument&&e instanceof e.ownerDocument.defaultView.HTMLElement){var c=e.ownerDocument.defaultView;(e instanceof c.HTMLInputElement||e instanceof c.HTMLTextAreaElement||e instanceof c.HTMLIFrameElement)&&n.push(e)}if(e.getElementsByTagName){var l=function(r){return Tt(e.getElementsByTagName(r))};n=n.concat(l("INPUT"),l("TEXTAREA"),l("IFRAME"))}return e.querySelectorAll&&(n=n.concat(Tt(e.querySelectorAll("[contenteditable]")))),e.ownerDocument&&e instanceof e.ownerDocument.defaultView.HTMLElement&&e.isContentEditable&&n.push(e),n},t.prototype._SetupDocument=function(e){for(var n=this._GetDocumentEditables(e),c=0;c0&&n.length==0)c=1;else if(e.length==0&&n.length>0)c=2;else{var s=e[0],o=n[0];s.offsetTopo.offsetTop?c=2:s.offsetLefto.offsetLeft&&(c=2)}switch(c){case 0:l=r;break;case 1:l=getComputedStyle(e[0]).fontFamily||"";break;case 2:l=getComputedStyle(n[0]).fontFamily||"";break}return(typeof l=="undefined"||l=="monospace")&&(l=r),l},t.prototype.buildAttachmentFontStyle=function(e){var n=e,c=this.baseFont;n&&typeof n.family!="undefined"&&(c=n.family),c=c.replace(/\u0022/g,"");var l=new RegExp("\\s?"+c+",?"),r=this.appliedFont.replace(/\u0022/g,"");r=r.replace(l,""),r=r.replace(/,$/,""),r==""?r=c:r=c+","+r,r='"'+r.replace(/\,\s?/g,'","')+'"';var s=".keymanweb-font{\nfont-family:"+r+" !important;\n}\n";return this.appliedFont=r,s},t.prototype.setAttachmentFont=function(e,n,c){this.stylesheetManager.unlinkAll(),this.stylesheetManager.addStyleSheetForFont(e,n,c),this.stylesheetManager.linkStylesheet(Se(this.buildAttachmentFontStyle(e)))},t.prototype.shutdown=function(){var e,n,c,l;try{(e=this.enablementObserver)===null||e===void 0||e.disconnect(),(n=this.attachmentObserver)===null||n===void 0||n.disconnect(),(c=this.inputModeObserver)===null||c===void 0||c.disconnect(),(l=this.stylesheetManager)===null||l===void 0||l.unlinkAll(),this.inputModeObserver=null,this.embeddedPageContexts.forEach(function(a){try{a.shutdown()}catch(B){}});for(var r=0,s=this.inputList;r"+n.name),c.then(function(){var r;(r=e.engineConfig.alertHost)===null||r===void 0||r.wait()})}),this.engineConfig.deferForInitialization.then(function(){var n=e.engineConfig.hostDevice,c=function(l){return l.stopPropagation()};e.page.on("enabled",function(l){if(!(l._kmwAttachment.interface instanceof re))n.touchable&&(e.domEventTracker.detachDOMEvent(l,"touchstart",e.nonKMWTouchHandler),e.domEventTracker.attachDOMEvent(l,"touchmove",c,!1),e.domEventTracker.attachDOMEvent(l,"touchend",c,!1)),e.domEventTracker.attachDOMEvent(l,"focus",e._ControlFocus),e.domEventTracker.attachDOMEvent(l,"blur",e._ControlBlur),e.domEventTracker.attachDOMEvent(l,"click",e._Click);else{var r=l.contentWindow.document;n.browser=="firefox"?(e.domEventTracker.attachDOMEvent(r,"focus",e._ControlFocus),e.domEventTracker.attachDOMEvent(r,"blur",e._ControlBlur)):(e.domEventTracker.attachDOMEvent(r.body,"focus",e._ControlFocus),e.domEventTracker.attachDOMEvent(r.body,"blur",e._ControlBlur))}}),e.page.on("disabled",function(l){var r;if(!P(l,"HTMLIFrameElement"))n.touchable&&e.domEventTracker.attachDOMEvent(l,"touchstart",e.nonKMWTouchHandler,!1),e.domEventTracker.detachDOMEvent(l,"focus",e._ControlFocus),e.domEventTracker.detachDOMEvent(l,"blur",e._ControlBlur),e.domEventTracker.detachDOMEvent(l,"click",e._Click);else{var s=l.contentWindow.document;n.browser=="firefox"?(e.domEventTracker.detachDOMEvent(s,"focus",e._ControlFocus),e.domEventTracker.detachDOMEvent(s,"blur",e._ControlBlur)):(e.domEventTracker.detachDOMEvent(s.body,"focus",e._ControlFocus),e.domEventTracker.detachDOMEvent(s.body,"blur",e._ControlBlur))}var o=(r=e.mostRecentTarget)===null||r===void 0?void 0:r.getElement();o&&o==l&&e.forgetActiveTarget()}),e.page.install(e.engineConfig.attachType=="manual")})},Object.defineProperty(t.prototype,"activeTarget",{get:function(){var e=this.focusAssistant.maintainingFocus;return this.currentTarget||(e?this.mostRecentTarget:null)},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"lastActiveTarget",{get:function(){return this.mostRecentTarget},enumerable:!1,configurable:!0}),t.prototype.deactivateCurrentTarget=function(){var e=this.activeTarget||this.lastActiveTarget;e&&this.page.isAttached(e.getElement())&&this._BlurKeyboardSettings(e.getElement()),this.activeTarget||this.setActiveTarget(null,!0)},t.prototype.forgetActiveTarget=function(){this.focusAssistant.maintainingFocus=!1,this.focusAssistant.restoringFocus=!1;var e=this.activeTarget||this.mostRecentTarget;e&&this._BlurKeyboardSettings(e.getElement()),this.setActiveTarget(null,!0),e==this.lastActiveTarget&&(this.mostRecentTarget=null)},t.prototype.setActiveTarget=function(e,n){var c,l=this.mostRecentTarget,r=this.activeTarget;if(e==r){r&&(this.currentTarget=r);return}var s=!!l;if(this.currentTarget=this.mostRecentTarget=e,this.predictionContext.setCurrentTarget(e),this.focusAssistant.restoringFocus?this._BlurKeyboardSettings(e.getElement()):e&&this._FocusKeyboardSettings(e.getElement(),!s),this._CommonFocusHelper(e))return!0;var o=e==null?void 0:e.getElement();e instanceof re&&(o=e.docRoot),o&&o.ownerDocument&&o instanceof o.ownerDocument.defaultView.HTMLElement&&Sl(o,(c=this.activeKeyboard)===null||c===void 0?void 0:c.keyboard),e!=r&&this.emit("targetchange",e),n&&this.apiEvents.callEvent("controlfocused",{target:(e==null?void 0:e.getElement())||null,activeControl:l==null?void 0:l.getElement()})},Object.defineProperty(t.prototype,"activeKeyboard",{get:function(){return this._activeKeyboard},enumerable:!1,configurable:!0}),t.prototype.restoreLastActiveTarget=function(){!this.mostRecentTarget||(this.focusAssistant.restoringFocus=!0,this.mostRecentTarget.focus(),this.focusAssistant.restoringFocus=!1)},t.prototype.insertText=function(e,n,c){this.restoreLastActiveTarget();var l=this.activeTarget;return l==null&&this.mostRecentTarget&&(l=this.activeTarget),l!=null?i.prototype.insertText.call(this,e,n,c):!1},t.prototype.currentKeyboardSrcTarget=function(){var e=this.currentTarget||this.mostRecentTarget;return this.isTargetKeyboardIndependent(e)?e:null},t.prototype.isTargetKeyboardIndependent=function(e){var n=e==null?void 0:e.getElement()._kmwAttachment;return!!((n==null?void 0:n.keyboard)||(n==null?void 0:n.keyboard)==="")},t.prototype.activateKeyboardForTarget=function(e,n){var c,l,r=n==null?void 0:n.getElement()._kmwAttachment;if(r?(r.keyboard=(c=e==null?void 0:e.metadata.id)!==null&&c!==void 0?c:"",r.languageCode=(l=e==null?void 0:e.metadata.langId)!==null&&l!==void 0?l:""):this.globalKeyboard=e,this.currentKeyboardSrcTarget()==n){this._activeKeyboard=e;var s=e==null?void 0:e.metadata;this.page.setAttachmentFont(s==null?void 0:s.KFont,this.engineConfig.paths.fonts,this.engineConfig.hostDevice.OS)}},t.prototype.setKeyboardForTarget=function(e,n,c){if(e instanceof re){console.warn("'keymanweb.setKeyboardForControl' cannot set keyboard on iframes.");return}var l=e.getElement()._kmwAttachment,r=this.currentKeyboardSrcTarget()==e;if(l){if(l.keyboard=n||null,l.languageCode=c||null,r||this.currentKeyboardSrcTarget()==e){var s=this.globalKeyboard.metadata;this.activateKeyboard(l.keyboard||s.id,l.languageCode||s.langId,!0)}}else return},t.prototype.getKeyboardStubForTarget=function(e){if(this.isTargetKeyboardIndependent(e)){var n=e.getElement()._kmwAttachment;return this.keyboardCache.getStub(n.keyboard,n.languageCode)}else return this.globalKeyboard.metadata},t.prototype.getFallbackStubKey=function(){var e={id:"",langId:""};return this.engineConfig.hostDevice.touchable&&this.keyboardCache.defaultStub||e},t.prototype.activateKeyboard=function(e,n,c){var l,r,s,o,a,B;return(0,d.__awaiter)(this,void 0,void 0,function(){var g,F,u,Q,y,I=this;return(0,d.__generator)(this,function(h){switch(h.label){case 0:c||(c=!1),g=this.currentKeyboardSrcTarget(),e||(e=this.getFallbackStubKey().id,n=this.getFallbackStubKey().langId),h.label=1;case 1:return h.trys.push([1,3,,7]),[4,i.prototype.activateKeyboard.call(this,e,n,c)];case 2:return F=h.sent(),(l=this.engineConfig.alertHost)===null||l===void 0||l.wait(),c&&!g&&this.cookieManager.save({current:"".concat(e,":").concat(n)}),g==this.currentKeyboardSrcTarget()&&(Sl((r=this.currentTarget)===null||r===void 0?void 0:r.getElement(),this.keyboardCache.getKeyboard(e)),this.page.setAttachmentFont((o=(s=this.activeKeyboard)===null||s===void 0?void 0:s.metadata)===null||o===void 0?void 0:o.KFont,this.engineConfig.paths.fonts,this.engineConfig.hostDevice.OS),this.restoreLastActiveTarget()),[2,F];case 3:return u=h.sent(),Q=function(){return(0,d.__awaiter)(I,void 0,void 0,function(){var C;return(0,d.__generator)(this,function(U){switch(U.label){case 0:return C=this.getFallbackStubKey(),C.id==e?[3,2]:[4,this.activateKeyboard(C.id,C.langId,!0).catch(function(){})];case 1:U.sent(),U.label=2;case 2:return[2]}})})},(a=this.engineConfig.alertHost)===null||a===void 0||a.wait(),y=(u==null?void 0:u.message)||"Sorry, the "+e+" keyboard for "+n+" is not currently available.",u instanceof ut?console.error(u||y):console.warn(u||y),this.engineConfig.alertHost?((B=this.engineConfig.alertHost)===null||B===void 0||B.alert(y,Q),[3,6]):[3,4];case 4:return[4,Q()];case 5:h.sent(),h.label=6;case 6:throw u;case 7:return[2]}})})},t.prototype._BlurKeyboardSettings=function(e,n,c){var l,r=this.activeKeyboard?this.activeKeyboard.keyboard.id:"",s=(l=this.activeKeyboard)===null||l===void 0?void 0:l.metadata.langId;n!==void 0&&c!==void 0&&(r=n,s=c),e&&e._kmwAttachment.keyboard!=null?(e._kmwAttachment.keyboard=r,e._kmwAttachment.languageCode=s):this.globalKeyboard=this.activeKeyboard},t.prototype._FocusKeyboardSettings=function(e,n){var c,l=e._kmwAttachment,r=this.globalKeyboard;l.keyboard!=null?this.activateKeyboard(l.keyboard,l.languageCode,!0):!n&&(r==null?void 0:r.metadata)!=((c=this._activeKeyboard)===null||c===void 0?void 0:c.metadata)&&this.activateKeyboard(r==null?void 0:r.metadata.id,r==null?void 0:r.metadata.langId,!0)},t.prototype._CommonFocusHelper=function(e){var n,c=this.focusAssistant,l=(n=this.activeKeyboard)===null||n===void 0?void 0:n.keyboard;return c.restoringFocus||(e==null||e.deadkeys().clear(),l==null||l.notify(0,e,1)),!c.restoringFocus&&this.mostRecentTarget!=e&&(c.maintainingFocus=!1),c.restoringFocus=!1,this.resetContext(),!1},t.prototype.doChangeEvent=function(e){if(e.changed){var n=new Event("change",{bubbles:!0,cancelable:!1});e.getElement().dispatchEvent(n)}e.changed=!1},t.prototype.getSavedKeyboardRaw=function(){var e=new Fe("KeymanWeb_Keyboard"),n=e.load(decodeURIComponent);return typeof n.current!="string"||n.current=="Keyboard_us:eng"?"Keyboard_us:en":n.current},t.prototype.getSavedKeyboard=function(){for(var e=this.getSavedKeyboardRaw(),n=this.keyboardCache.getStubList(),c,l=0;l0?n[0].KI+":"+n[0].KLC:"Keyboard_us:en"},t.prototype.restoreSavedKeyboard=function(e){var n=e,c=n.split(":");c.length<2&&(c[1]="");var l=this.keyboardCache.getStub(c[0],c[1])||this.keyboardCache.defaultStub;l&&this.activateKeyboard(c[0],c[1])},t.prototype.shutdown=function(){this.page.shutdown(),this.domEventTracker.shutdown()},t}(mn),Rl=xo;var po=function(i){(0,d.__extends)(t,i);function t(e){var n=i.call(this)||this;return n.contextManager=e,n}return t.prototype.isCommand=function(e){var n=this.codeForEvent(e);switch(n){case b.keyCodes.K_TAB:case b.keyCodes.K_TABBACK:case b.keyCodes.K_TABFWD:return!0;default:return i.prototype.isCommand.call(this,e)}},t.prototype.applyCommand=function(e,n){var c=this,l=this.codeForEvent(e),r=function(s){var o,a=c.contextManager,B=(o=a.activeTarget)===null||o===void 0?void 0:o.getElement(),g=a.page.findNeighboringInput(B,s);g.focus()};switch(l){case b.keyCodes.K_TAB:r((e.Lmodifiers&b.modifierCodes.SHIFT)!=0);break;case b.keyCodes.K_TABBACK:r(!0);break;case b.keyCodes.K_TABFWD:r(!1);break}i.prototype.applyCommand.call(this,e,n)},t}(ke),Al=po;function Uc(i){return i.keyCode?i.keyCode:i.which?i.which:null}function ln(i,t,e){if(i.cancelBubble===!0)return null;var n=Uc(i);if(n==null)return null;var c=t.modStateFlags,l=0,r=!1,s=!1,o=b.keyCodes;switch(n){case o.K_CTRL:case o.K_LCTRL:case o.K_RCTRL:case o.K_CONTROL:case o.K_LCONTROL:case o.K_RCONTROL:r=!0;break;case o.K_LMENU:case o.K_RMENU:case o.K_ALT:case o.K_LALT:case o.K_RALT:s=!0;break}l|=i.getModifierState("Shift")?16:0;var a=b.modifierCodes;i.getModifierState("Control")&&(l|=i.location!=0&&r?i.location==1?a.LCTRL:a.RCTRL:c&3),i.getModifierState("Alt")&&(l|=i.location!=0&&s?i.location==1?a.LALT:a.RALT:c&12);var B=0;B|=i.getModifierState("CapsLock")?a.CAPS:a.NO_CAPS,B|=i.getModifierState("NumLock")?a.NUM_LOCK:a.NO_NUM_LOCK,B|=i.getModifierState("ScrollLock")?a.SCROLL_LOCK:a.NO_SCROLL_LOCK,l|=B;var g=t.modStateFlags!=l;t.modStateFlags=l;var F=a.RALT|a.LCTRL;(c&F)==F&&(l&F)!=F&&(l&=~F),l&a.RALT&&(l&=~a.LCTRL);var u=b.modifierBitmasks,Q=t.activeKeyboard,y;Q&&Q.isChiral?(y=l&u.CHIRAL,Q.emulatesAltGr&&(y&u.ALT_GR_SIM)==u.ALT_GR_SIM&&(y^=u.ALT_GR_SIM,y|=a.RALT)):y=l&16|(l&(a.LCTRL|a.RCTRL)?32:0)|(l&(a.LALT|a.RALT)?64:0),y|=i.metaKey?a.META:0,e.browser==V.Browser.Firefox&&ae.browserMap.FF["k"+n]&&(n=ae.browserMap.FF["k"+n]);var I=new oe({device:e,kName:"",Lcode:n,Lmodifiers:y,Lstates:B,LmodifierChange:g,isSynthetic:!1}),h=typeof i.charCode!="undefined"&&i.charCode!=null&&(i.charCode==0||(y&111)!=0);I.LisVirtualKey=h||i.type!="keypress",I=Vn(I,Q,t.baseLayout);var C=new oe(I);return C.source=i,C}var Go=function(i){(0,d.__extends)(t,i);function t(e,n,c){var l=i.call(this)||this;l.domEventTracker=new me,l.swallowKeypress=!1,l._KeyDown=function(o){var a,B=l.contextManager.activeKeyboard,g=fe(o);if(!g||B==null)return!0;var F=g.getElement();return((a=F==null?void 0:F.className)===null||a===void 0?void 0:a.indexOf("kmw-disabled"))>=0?!0:l.keyDown(o)},l._KeyPress=function(o){var a=fe(o);return!a||l.activeKeyboard==null?!0:l.keyPress(o)},l._KeyUp=function(o){var a=fe(o),B=ln(o,l.processor,l.hardDevice);if(B==null||a==null)return!0;var g=a.getElement();if(B.Lcode==13){var F=!1;if(P(g,"HTMLTextAreaElement")&&(F=!0),!F){if(g instanceof g.ownerDocument.defaultView.HTMLInputElement)if(g.form&&(g.type=="search"||g.type=="submit"))g.form.submit();else{var u=l.contextManager.page.findNeighboringInput(g,!1);u.focus()}return!0}}return l.keyUp(o)},l.hardDevice=e,l.contextManager=c,l.processor=n;var r=c.page,s=l.domEventTracker;return r.on("enabled",function(o){var a=Le(o);if(!(a instanceof re))s.attachDOMEvent(o,"keypress",l._KeyPress),s.attachDOMEvent(o,"keydown",l._KeyDown),s.attachDOMEvent(o,"keyup",l._KeyUp);else{var B=a.getElement().contentDocument;s.attachDOMEvent(B.body,"keydown",l._KeyDown),s.attachDOMEvent(B.body,"keypress",l._KeyPress),s.attachDOMEvent(B.body,"keyup",l._KeyUp)}}),r.on("disabled",function(o){var a=Le(o);if(!(a instanceof re))s.detachDOMEvent(o,"keypress",l._KeyPress),s.detachDOMEvent(o,"keydown",l._KeyDown),s.detachDOMEvent(o,"keyup",l._KeyUp);else{var B=a.getElement().contentDocument;s.detachDOMEvent(B.body,"keydown",l._KeyDown),s.detachDOMEvent(B.body,"keypress",l._KeyPress),s.detachDOMEvent(B.body,"keyup",l._KeyUp)}}),l}return Object.defineProperty(t.prototype,"activeKeyboard",{get:function(){return this.contextManager.activeKeyboard.keyboard},enumerable:!1,configurable:!0}),t.prototype.keyDown=function(e){var n=this;this.swallowKeypress=!1;var c=ln(e,this.processor,this.hardDevice);if(c==null)return!0;var l={LeventMatched:!1};return this.emit("keyevent",c,function(r,s){l.LeventMatched=r&&!r.triggerKeyDefault,l.LeventMatched?(e&&e.preventDefault&&(e.preventDefault(),e.stopPropagation()),n.swallowKeypress=!!c.Lcode,c.Lcode==8&&(n.swallowKeypress=!1)):n.swallowKeypress=!1}),!l.LeventMatched},t.prototype.keyUp=function(e){var n=ln(e,this.processor,this.hardDevice);if(n==null)return!0;var c=fe(e);return this.processor.doModifierPress(n,c,!1)},t.prototype.keyPress=function(e){var n=ln(e,this.processor,this.hardDevice);if(n==null||n.LisVirtualKey)return!0;if(!this.activeKeyboard.isMnemonic)return!this.swallowKeypress||n.Lcode<32||this.hardDevice.browser==V.Browser.Safari&&n.Lcode>63232&&n.Lcode<63744;var c={};return this.swallowKeypress||this.emit("keyevent",n,function(l,r){c.preventDefaultKeystroke=!!l}),this.swallowKeypress||c.preventDefaultKeystroke?(this.swallowKeypress=!1,e&&e.preventDefault&&(e.preventDefault(),e.stopPropagation()),!1):(this.swallowKeypress=!1,!0)},t.prototype.shutdown=function(){this.domEventTracker.shutdown()},t}(Xn),Hl=Go;var Wl=function(){function i(){this.innerWidth=window.innerWidth,this.innerHeight=window.innerHeight}return i.prototype.equals=function(t){return this.innerWidth==t.innerWidth&&this.innerHeight==t.innerHeight},i}(),Nl=function(){function i(t){this.idlePermutationCounter=i.IDLE_PERMUTATION_CAP,this.keyman=t}return i.prototype.resolve=function(){var t,e=this.keyman.osk;(t=this.keyman.touchLanguageMenu)===null||t===void 0||t.hide(),this.keyman.touchLanguageMenu=null,e.setNeedsLayout(),this.oskVisible&&e.present(),this.isActive=!1,this.updateTimer&&(window.clearInterval(this.updateTimer),this.rotState=null)},i.prototype.initNewRotation=function(){this.oskVisible=this.keyman.osk.isVisible(),this.keyman.osk.hideNow(),this.isActive=!0},i.prototype.init=function(){var t=this,e=this.keyman.config.hostDevice.OS,n=this.keyman.util;e=="ios"?(n.attachDOMEvent(window,"orientationchange",function(){return t.iOSEventHandler(),!1}),n.attachDOMEvent(window,"resize",function(){return t.iOSEventHandler(),!1})):e=="android"&&("onmozorientationchange"in screen?n.attachDOMEvent(screen,"mozorientationchange",function(){return t.initNewRotation(),!1}):n.attachDOMEvent(window,"orientationchange",function(){return t.initNewRotation(),!1}),n.attachDOMEvent(window,"resize",function(){return t.resolve(),!1}))},i.prototype.iOSEventHandler=function(){this.isActive||(this.initNewRotation(),this.rotState=new Wl,this.updateTimer=window.setInterval(this.iOSEventUpdate.bind(this),i.UPDATE_INTERVAL)),this.idlePermutationCounter=0},i.prototype.iOSEventUpdate=function(){var t=new Wl;this.rotState.equals(t)?++this.idlePermutationCounter==i.IDLE_PERMUTATION_CAP&&this.resolve():(this.rotState=t,this.idlePermutationCounter=0)},i.IDLE_PERMUTATION_CAP=15,i.UPDATE_INTERVAL=20,i}();var Tl=function(){function i(t,e){var n=this;this.domEventTracker=new me,this.suppressFocusCheck=function(c){return n.focusAssistant.isTargetForcingScroll()&&(c.stopPropagation(),c.cancelBubble=!0),!0},this.pageFocusHandler=function(){var c;return!n.focusAssistant.maintainingFocus&&((c=n.engine.osk)===null||c===void 0?void 0:c.vkbd)&&(n.engine.contextManager.deactivateCurrentTarget(),n.engine.contextManager.resetContext()),!1},this.touchStartActivationHandler=function(c){var l=n.engine.osk;if(!l)return!1;var r=n.engine.config.hostDevice;if(n.deactivateOnRelease=!0,n.touchY=c.touches[0].screenY,n.deactivateOnScroll=!1,r.OS=="android"&&r.browser=="chrome"){if(typeof l._Box=="undefined"||typeof l._Box.style=="undefined")return!1;var s=c.target.parentElement;if(typeof s!="undefined"&&s!=null&&(s.className.indexOf("kmw-key-")>=0||typeof s.parentElement!="undefined"&&s.parentElement!=null&&(s=s.parentElement,s.className.indexOf("kmw-key-")>=0)))return!1;n.deactivateOnScroll=!0}return!1},this.touchMoveActivationHandler=function(c){n.deactivateOnScroll&&(n.focusAssistant.focusing=!1,n.engine.contextManager.deactivateCurrentTarget());var l=c.touches[0].screenY,r=n.touchY;return(l-r>5||r-l<5)&&(n.deactivateOnRelease=!1),!1},this.touchEndActivationHandler=function(c){return n.deactivateOnRelease&&!n.engine.touchLanguageMenu&&!n.focusAssistant.focusing&&n.engine.contextManager.deactivateCurrentTarget(),n.deactivateOnRelease=!1,!1},this._WindowLoad=function(){document.body.scrollTop=0,typeof document.documentElement!="undefined"&&(document.documentElement.scrollTop=0)},this._WindowUnload=function(){n.engine.shutdown()},this.window=t,this.engine=e,this.attachHandlers(),e.config.hostDevice.touchable&&(this.buildPageTrailer(),this.rotationProcessor=new Nl(this.engine),this.rotationProcessor.init())}return i.prototype.buildPageTrailer=function(){var t=this.mobilePageTrailer=document.createElement("div"),e=t.style;e.width="100%",e.height=screen.width/2+"px",document.body.appendChild(t)},Object.defineProperty(i.prototype,"focusAssistant",{get:function(){return this.engine.contextManager.focusAssistant},enumerable:!1,configurable:!0}),i.prototype.attachHandlers=function(){var 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)},i.prototype.shutdown=function(){var t,e=this.domEventTracker,n=this.engine.config.hostDevice,c=this.window.document.body;e.detachDOMEvent(this.window,"focus",this.pageFocusHandler,!1),e.detachDOMEvent(this.window,"blur",this.pageFocusHandler,!1),e.detachDOMEvent(c,"focus",this.suppressFocusCheck,!0),e.detachDOMEvent(c,"blur",this.suppressFocusCheck,!0),n.touchable&&(e.detachDOMEvent(c,"touchstart",this.touchStartActivationHandler,!1),e.detachDOMEvent(c,"touchmove",this.touchMoveActivationHandler,!1),e.detachDOMEvent(c,"touchend",this.touchEndActivationHandler,!1),(t=this.mobilePageTrailer)===null||t===void 0||t.parentElement.removeChild(this.mobilePageTrailer)),e.detachDOMEvent(window,"load",this._WindowLoad,!1),e.detachDOMEvent(window,"unload",this._WindowUnload,!1),e.detachDOMEvent(document,"keyup",this.engine.hotkeyManager._Process,!1)},i}();function se(i){var t=document.createElement(i);return t.style.userSelect="none",t.style.MozUserSelect="none",t.style.KhtmlUserSelect="none",t.style.UserSelect="none",t.style.WebkitUserSelect="none",t}function De(i,t){try{if(i&&typeof window.getComputedStyle!="undefined")return window.getComputedStyle(i,"").getPropertyValue(t)}catch(e){}return""}var El=function(){function i(t){this.keyman=t,this.scrolling=!1,this.shim=this.constructShim()}return i.prototype.constructShim=function(){var t=this,e=this,n=se("div"),c=this.keyman.osk;return n.id="kmw-language-menu-background",n.addEventListener("touchstart",function(l){if(l.preventDefault(),e.hide(),l.touches.length>2){var r=l.touches[1].pageX,s=l.touches[1].pageY,o=c.vkbd.spaceBar;r>o.offsetLeft&&ro.offsetTop&&sr.scrollHeight-r.offsetHeight-1&&(r.scrollTop=r.scrollHeight-r.offsetHeight-1)},!1),this.activeLgNo=this.addLanguages(o,e);var u=o.childNodes.length-1;if(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 Q=parseInt(De(c,"width"),10),y=c.style;isNaN(Q)||(y.width=y.maxWidth=2*Q/window.devicePixelRatio+"px"),Q=parseInt(De(r,"width"),10),y=r.style,isNaN(Q)||(y.width=y.maxWidth=2*Q/window.devicePixelRatio+"px"),Q=parseInt(De(o,"width"),10),y=o.style,isNaN(Q)||(y.width=y.maxWidth=2*Q/window.devicePixelRatio+"px")}this.adjust(0);var I=F.childNodes[1].offsetTop-F.childNodes[0].offsetTop,h=Math.floor(c.offsetHeight/26),C=Math.round(100*h/I)/100,U=C>.6?1:2;for(C>1.25&&(C=1.25),B=0;B<26;B++){var x=F.childNodes[B].style;U==2&&B%2==1?x.display="none":(x.fontSize=C*U+"em",x.lineHeight=h*U+"px")}var G=r.offsetWidth;r.scrollHeight>r.offsetHeight+3?G=G+F.offsetWidth:F.style.display="none",c.style.width=G+"px",this.lgList.style.visibility="",this.scrollToIndex(this.activeLgNo,r,o)}},i.prototype.adjust=function(t){var e=this.keyman.osk,n=this.keyman.config.hostDevice,c=this.lgList,l=c.firstChild,r=l.firstChild,s=0,o=c.style,a=c.childNodes[1],B=window.innerHeight-e.vkbd.lgKey.offsetHeight-16,g=r.childNodes.length+t-1,F=r.firstChild.firstChild.offsetHeight,u=g*F;n.OS=="ios"&&(n.formFactor=="phone"?(s=ee()?36:0,B=(window.innerHeight-s-16)*Ie(n.formFactor)):n.formFactor=="tablet"&&(s=ee()?16:0,B=B-s)),o.left=K(e.vkbd.lgKey)+"px",u>B&&(u=B),o.height=u+"px",o.bottom="0px",a.style.height=l.style.height=o.height},i.prototype.scrollToLanguage=function(t,e,n){t.stopImmediatePropagation(),t.stopPropagation(),t.preventDefault();var c=t.touches[0].target;if(c.nodeName=="P"){var l,r,s=c.innerHTML.charCodeAt(0),o=n.childNodes;try{for(l=0;l=s));l++);}catch(a){}this.scrollToIndex(l,e,n)}},i.prototype.scrollToIndex=function(t,e,n){var c,l=.5;try{c=n.firstChild.getBoundingClientRect().height*(t-l)+1,e.scrollTop=c}catch(r){c=0}try{e.scrollTop<0&&(e.scrollTop=0),e.scrollTop>e.scrollHeight-e.offsetHeight-1&&(e.scrollTop=e.scrollHeight-e.offsetHeight-1)}catch(r){}},i.prototype.addLanguages=function(t,e){var n,c=e.length,l=this.keyman.config.hostDevice,r,s,o,a=[];for(s=0;s1)for(F.className="kbd-list",F.innerHTML=a[r]+"...",F.scrolled=!1,F.ontouchend=function(G){G.stopPropagation(),G.target.scrolled?G.target.scrolled=!1:this.parentNode.className=this.parentNode.className=="kbd-list-closed"?"kbd-list-open":"kbd-list-closed",x.adjust(this.parentNode.className=="kbd-list-closed"?0:this.kList.length)},F.addEventListener("touchstart",function(G){G.stopPropagation()},!1),F.addEventListener("touchmove",function(G){G.target.scrolled=!0,G.stopPropagation()},!1),u=0;u=Q-1&&(l.y0=y);else if(I>0)u.scrollTop<2&&(l.y0=y);else return;return(I<-5||I>5)&&(l.scrolling=!0,this.className=this.className.replace(/\s*selected/,""),l.y0=y),!0},B=function(F){return typeof F.stopImmediatePropagation!="undefined"?F.stopImmediatePropagation():F.stopPropagation(),l.scrolling?this.className=this.className.replace(/\s*selected/,""):(l.keyman.contextManager.focusAssistant.setFocusTimer(),l.lgList.style.display="none",l.keyman.contextManager.activateKeyboard(this.kn,this.kc,!0),l.keyman.contextManager.restoreLastActiveTarget(),l.hide()),s(),!0},g=function(F){s()};e.onmspointerdown=o,e.addEventListener("touchstart",o,!1),e.onmspointermove=a,e.addEventListener("touchmove",a,!1),e.onmspointerout=B,e.addEventListener("touchend",B,!1),e.addEventListener("touchcancel",g,!1)},i.prototype.hide=function(){var t=this,e=this.keyman.osk;this.lgList&&(e.vkbd.highlightKey(e.vkbd.lgKey,!1),this.lgList.style.visibility="hidden",window.setTimeout(function(){t.shim.parentElement&&(document.body.removeChild(t.shim),document.body.removeChild(t.lgList))},500)),this.keyman.touchLanguageMenu=null},i}();function kl(i,t,e){var n=this,c=e.focusAssistant;t.on("globekey",function(l,r){r&&t.hostDevice.touchable&&(i.touchLanguageMenu=new El(i),i.touchLanguageMenu.show()),t.vkbd&&t.vkbd.highlightKey(l,!1)}),t.on("hiderequested",function(l){t&&(t.startHide(!0),e.forgetActiveTarget())}),t.addEventListener("hide",function(l){var r;l!=null&&l.HiddenByUser&&((r=e.activeTarget)===null||r===void 0||r.focus())}),t.on("showbuild",function(){var l;(l=i.config.alertHost)===null||l===void 0||l.alert("KeymanWeb Version "+gn.VERSION+'

Copyright © 2007-2023 SIL International')}),t.on("dragmove",function(l){return(0,d.__awaiter)(n,void 0,void 0,function(){return(0,d.__generator)(this,function(r){switch(r.label){case 0:return c.restoringFocus=!0,[4,l];case 1:return r.sent(),e.restoreLastActiveTarget(),c.restoringFocus=!1,c.setMaintainingFocus(!1),[2]}})})}),t.on("resizemove",function(l){return(0,d.__awaiter)(n,void 0,void 0,function(){return(0,d.__generator)(this,function(r){switch(r.label){case 0:return c.restoringFocus=!0,[4,l];case 1:return r.sent(),e.restoreLastActiveTarget(),c.restoringFocus=!1,c.setMaintainingFocus(!1),[2]}})})}),t.on("pointerinteraction",function(l){return(0,d.__awaiter)(n,void 0,void 0,function(){return(0,d.__generator)(this,function(r){switch(r.label){case 0:return c.setMaintainingFocus(!0),[4,l];case 1:return r.sent(),c.setMaintainingFocus(!1),[2]}})})})}function Xo(i){var t=document.createElement(i);return t.style.userSelect="none",t}var Yl=function(){function i(t){this.getAbsoluteX=K,this.getAbsoluteY=_,this._GetAbsoluteX=K,this._GetAbsoluteY=_,this._GetAbsolute=this.getAbsolute,this.toNzString=this.nzString,this.createElement=Xo,this.getStyleValue=De,this.config=t,this.stylesheetManager=new Ce(document.body,t.applyCacheBusting),this.domEventTracker=new me}return i.prototype.isTouchDevice=function(){return this.config.hostDevice.touchable},i.prototype.getAbsolute=function(t){return{x:K(t),y:_(t)}},i.prototype.getOption=function(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:""},i.prototype.setOption=function(t,e){switch(t){case"attachType":break;case"ui":break;case"useAlerts":this.config.signalUser=e?new tt: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.")}},i.prototype.loadCookie=function(t){var e=new Fe(t);return e.load(decodeURIComponent)},i.prototype.saveCookie=function(t,e){var n=new Fe(t);n.save(e,encodeURIComponent)},i.prototype.addStyleSheet=function(t){var e=Se(t);return this.stylesheetManager.linkStylesheet(e),e},i.prototype.removeStyleSheet=function(t){return this.stylesheetManager.unlink(t)},i.prototype.linkStyleSheet=function(t){this.stylesheetManager.linkExternalSheet(t)},i.prototype.getLanguageCodes=function(t){return t.indexOf("-")==-1?[t]:t.split("-")},i.prototype.attachDOMEvent=function(t,e,n,c){this.domEventTracker.attachDOMEvent(t,e,n,c)},i.prototype.detachDOMEvent=function(t,e,n,c){this.domEventTracker.detachDOMEvent(t,e,n,c)},Object.defineProperty(i.prototype,"alertHost",{get:function(){return this.config.alertHost?this.config.alertHost:(this._alertHost||(this._alertHost=new tt),this._alertHost)},enumerable:!1,configurable:!0}),i.prototype.alert=function(t,e){this.alertHost.alert(t,e)},i.prototype.nzString=function(t,e){var n="";return arguments.length>1&&(n=e),typeof t=="undefined"||t==null||t==0||t==""?n:""+t},i.prototype.toNumber=function(t,e){var n=parseInt(t,10);return isNaN(n)?e:n},i.prototype.toFloat=function(t,e){var n=parseFloat(t);return isNaN(n)?e:n},i.prototype.rgba=function(t,e,n,c,l){var r="transparent";try{r="rgba("+e+","+n+","+c+","+l+")"}catch(s){r="rgb("+e+","+n+","+c+")"}return r},i.prototype.shutdown=function(){var t,e,n;(t=this.stylesheetManager)===null||t===void 0||t.unlinkAll(),(e=this.domEventTracker)===null||e===void 0||e.shutdown(),(n=this._alertHost)===null||n===void 0||n.shutdown()},i}();var Vo=function(){function i(t,e,n){this.code=t,this.shift=e,this.handler=n}return i.prototype.matches=function(t,e){return this.code==t&&this.shift==e},i}(),wl=function(){function i(){var t=this;this.hotkeys=[],this._Process=function(e){e||(e=window.event);var n=Uc(e);if(n==null)return!1;for(var c=(e.shiftKey?16:0)|(e.ctrlKey?32:0)|(e.altKey?64:0),l=0;l=a&&(B-=window.innerHeight-c.osk._Box.offsetHeight-s.offsetHeight-2,B<0&&(B=0)),B!=0&&window.scrollTo(0,B+a)}}),c}return Object.defineProperty(t.prototype,"util",{get:function(){return this._util},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"views",{get:function(){return Fc},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"initialized",{get:function(){return this._initialized},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"ui",{get:function(){return this._ui},set:function(e){this._ui&&this._ui.shutdown(),this._ui=e,this.config.deferForInitialization.isFulfilled&&e.initialize()},enumerable:!1,configurable:!0}),t.prototype.init=function(e){return(0,d.__awaiter)(this,void 0,void 0,function(){var n,c,l,r,s=this;return(0,d.__generator)(this,function(o){switch(o.label){case 0:return n=new Wt,c=n.detect(),l=(0,d.__assign)((0,d.__assign)({},vl),e),this.config.hostDevice=c,this.config.initialize(l),this._initialized=1,[4,Zt()];case 1:return o.sent(),this.config.deferForInitialization.isResolved?[2,Promise.resolve()]:[4,i.prototype.init.call(this,l)];case 2:return o.sent(),this.keyboardRequisitioner.cloudQueryEngine.once("unboundregister",function(){var a;!((a=s.contextManager.activeKeyboard)===null||a===void 0)&&a.keyboard||s.setActiveKeyboard("","")}),this.contextManager.initialize(),r=this.contextManager.getSavedKeyboardRaw(),c.touchable?this.osk=new dc(this):this.osk=new gc(this),kl(this,this.osk,this.contextManager),this.pageIntegration=new Tl(window,this),this.config.finalizeInit(),this.ui&&(this.ui.initialize(),this.legacyAPIEvents.callEvent("loaduserinterface",{})),this._initialized=2,[4,Promise.resolve()];case 3:return o.sent(),this.contextManager.restoreSavedKeyboard(r),[4,Promise.resolve()];case 4:return o.sent(),[2]}})})},Object.defineProperty(t.prototype,"register",{get:function(){return this.keyboardRequisitioner.cloudQueryEngine.registerFromCloud},enumerable:!1,configurable:!0}),t.prototype.getUIState=function(){return this.contextManager.focusAssistant.getUIState()},t.prototype.activatingUI=function(e){this.contextManager.focusAssistant.setMaintainingFocus(!!e)},t.prototype.setKeyboardForControl=function(e,n,c){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}var l=null;if(n&&(l=this.keyboardRequisitioner.cache.getStub(n,c),!l))throw new Error("No keyboard has been registered with id ".concat(n," and language code ").concat(c,"."));this.contextManager.setKeyboardForTarget(e._kmwAttachment.interface,n,c)},t.prototype.getKeyboardForControl=function(e){var n=Le(e);return this.contextManager.getKeyboardStubForTarget(n).id},t.prototype.getLanguageForControl=function(e){var n=Le(e);return this.contextManager.getKeyboardStubForTarget(n).langId},t.prototype.isAttached=function(e){return this.contextManager.page.isAttached(e)},t.prototype.addKeyboards=function(){for(var e=this,n=[],c=0;c= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n };\r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n __awaiter = function (thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n };\r\n\r\n __generator = function (thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n };\r\n\r\n \r\n\r\n \r\n\r\n __values = function (o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n };\r\n\r\n __read = function (o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n };\r\n\r\n /** @deprecated */\r\n \r\n\r\n /** @deprecated */\r\n \r\n\r\n \r\n\r\n __await = function (v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n };\r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n \r\n\r\n exporter(\"__extends\", __extends);\r\n exporter(\"__assign\", __assign);\r\n \r\n exporter(\"__decorate\", __decorate);\r\n \r\n \r\n \r\n \r\n \r\n \r\n exporter(\"__awaiter\", __awaiter);\r\n exporter(\"__generator\", __generator);\r\n \r\n \r\n exporter(\"__values\", __values);\r\n exporter(\"__read\", __read);\r\n \r\n \r\n \r\n exporter(\"__await\", __await);\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n});\r\n", + "'use strict';\n\nvar has = Object.prototype.hasOwnProperty\n , prefix = '~';\n\n/**\n * Constructor to create a storage for our `EE` objects.\n * An `Events` instance is a plain object whose properties are event names.\n *\n * @constructor\n * @private\n */\nfunction Events() {}\n\n//\n// We try to not inherit from `Object.prototype`. In some engines creating an\n// instance in this way is faster than calling `Object.create(null)` directly.\n// If `Object.create(null)` is not supported we prefix the event names with a\n// character to make sure that the built-in object properties are not\n// overridden or used as an attack vector.\n//\nif (Object.create) {\n Events.prototype = Object.create(null);\n\n //\n // This hack is needed because the `__proto__` property is still inherited in\n // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5.\n //\n if (!new Events().__proto__) prefix = false;\n}\n\n/**\n * Representation of a single event listener.\n *\n * @param {Function} fn The listener function.\n * @param {*} context The context to invoke the listener with.\n * @param {Boolean} [once=false] Specify if the listener is a one-time listener.\n * @constructor\n * @private\n */\nfunction EE(fn, context, once) {\n this.fn = fn;\n this.context = context;\n this.once = once || false;\n}\n\n/**\n * Add a listener for a given event.\n *\n * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn The listener function.\n * @param {*} context The context to invoke the listener with.\n * @param {Boolean} once Specify if the listener is a one-time listener.\n * @returns {EventEmitter}\n * @private\n */\nfunction addListener(emitter, event, fn, context, once) {\n if (typeof fn !== 'function') {\n throw new TypeError('The listener must be a function');\n }\n\n var listener = new EE(fn, context || emitter, once)\n , evt = prefix ? prefix + event : event;\n\n if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;\n else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);\n else emitter._events[evt] = [emitter._events[evt], listener];\n\n return emitter;\n}\n\n/**\n * Clear event by name.\n *\n * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.\n * @param {(String|Symbol)} evt The Event name.\n * @private\n */\nfunction clearEvent(emitter, evt) {\n if (--emitter._eventsCount === 0) emitter._events = new Events();\n else delete emitter._events[evt];\n}\n\n/**\n * Minimal `EventEmitter` interface that is molded against the Node.js\n * `EventEmitter` interface.\n *\n * @constructor\n * @public\n */\nfunction EventEmitter() {\n this._events = new Events();\n this._eventsCount = 0;\n}\n\n/**\n * Return an array listing the events for which the emitter has registered\n * listeners.\n *\n * @returns {Array}\n * @public\n */\nEventEmitter.prototype.eventNames = function eventNames() {\n var names = []\n , events\n , name;\n\n if (this._eventsCount === 0) return names;\n\n for (name in (events = this._events)) {\n if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);\n }\n\n if (Object.getOwnPropertySymbols) {\n return names.concat(Object.getOwnPropertySymbols(events));\n }\n\n return names;\n};\n\n/**\n * Return the listeners registered for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @returns {Array} The registered listeners.\n * @public\n */\nEventEmitter.prototype.listeners = function listeners(event) {\n var evt = prefix ? prefix + event : event\n , handlers = this._events[evt];\n\n if (!handlers) return [];\n if (handlers.fn) return [handlers.fn];\n\n for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) {\n ee[i] = handlers[i].fn;\n }\n\n return ee;\n};\n\n/**\n * Return the number of listeners listening to a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @returns {Number} The number of listeners.\n * @public\n */\nEventEmitter.prototype.listenerCount = function listenerCount(event) {\n var evt = prefix ? prefix + event : event\n , listeners = this._events[evt];\n\n if (!listeners) return 0;\n if (listeners.fn) return 1;\n return listeners.length;\n};\n\n/**\n * Calls each of the listeners registered for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @returns {Boolean} `true` if the event had listeners, else `false`.\n * @public\n */\nEventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {\n var evt = prefix ? prefix + event : event;\n\n if (!this._events[evt]) return false;\n\n var listeners = this._events[evt]\n , len = arguments.length\n , args\n , i;\n\n if (listeners.fn) {\n if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);\n\n switch (len) {\n case 1: return listeners.fn.call(listeners.context), true;\n case 2: return listeners.fn.call(listeners.context, a1), true;\n case 3: return listeners.fn.call(listeners.context, a1, a2), true;\n case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;\n case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;\n case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;\n }\n\n for (i = 1, args = new Array(len -1); i < len; i++) {\n args[i - 1] = arguments[i];\n }\n\n listeners.fn.apply(listeners.context, args);\n } else {\n var length = listeners.length\n , j;\n\n for (i = 0; i < length; i++) {\n if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);\n\n switch (len) {\n case 1: listeners[i].fn.call(listeners[i].context); break;\n case 2: listeners[i].fn.call(listeners[i].context, a1); break;\n case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;\n case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;\n default:\n if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {\n args[j - 1] = arguments[j];\n }\n\n listeners[i].fn.apply(listeners[i].context, args);\n }\n }\n }\n\n return true;\n};\n\n/**\n * Add a listener for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn The listener function.\n * @param {*} [context=this] The context to invoke the listener with.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.on = function on(event, fn, context) {\n return addListener(this, event, fn, context, false);\n};\n\n/**\n * Add a one-time listener for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn The listener function.\n * @param {*} [context=this] The context to invoke the listener with.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.once = function once(event, fn, context) {\n return addListener(this, event, fn, context, true);\n};\n\n/**\n * Remove the listeners of a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn Only remove the listeners that match this function.\n * @param {*} context Only remove the listeners that have this context.\n * @param {Boolean} once Only remove one-time listeners.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {\n var evt = prefix ? prefix + event : event;\n\n if (!this._events[evt]) return this;\n if (!fn) {\n clearEvent(this, evt);\n return this;\n }\n\n var listeners = this._events[evt];\n\n if (listeners.fn) {\n if (\n listeners.fn === fn &&\n (!once || listeners.once) &&\n (!context || listeners.context === context)\n ) {\n clearEvent(this, evt);\n }\n } else {\n for (var i = 0, events = [], length = listeners.length; i < length; i++) {\n if (\n listeners[i].fn !== fn ||\n (once && !listeners[i].once) ||\n (context && listeners[i].context !== context)\n ) {\n events.push(listeners[i]);\n }\n }\n\n //\n // Reset the array, or remove it completely if we have no more listeners.\n //\n if (events.length) this._events[evt] = events.length === 1 ? events[0] : events;\n else clearEvent(this, evt);\n }\n\n return this;\n};\n\n/**\n * Remove all listeners, or those of the specified event.\n *\n * @param {(String|Symbol)} [event] The event name.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {\n var evt;\n\n if (event) {\n evt = prefix ? prefix + event : event;\n if (this._events[evt]) clearEvent(this, evt);\n } else {\n this._events = new Events();\n this._eventsCount = 0;\n }\n\n return this;\n};\n\n//\n// Alias methods names because people roll like that.\n//\nEventEmitter.prototype.off = EventEmitter.prototype.removeListener;\nEventEmitter.prototype.addListener = EventEmitter.prototype.on;\n\n//\n// Expose the prefix.\n//\nEventEmitter.prefixed = prefix;\n\n//\n// Allow `EventEmitter` to be imported as module namespace.\n//\nEventEmitter.EventEmitter = EventEmitter;\n\n//\n// Expose the module.\n//\nif ('undefined' !== typeof module) {\n module.exports = EventEmitter;\n}\n", + "export * from '../../../../node_modules/tslib/tslib.js';\r\n\r\nexport * as tslib from '../../../../node_modules/tslib/tslib.js';", + "import EventEmitter from \"eventemitter3\";\r\n\r\nimport { DeviceSpec, KeyboardProperties, ManagedPromise, OutputTarget, physicalKeyDeviceAlias, RuleBehavior, SpacebarText } from \"@keymanapp/keyboard-processor\";\r\nimport { PathConfiguration, PathOptionDefaults, PathOptionSpec } from \"keyman/engine/paths\";\r\nimport { Device } from \"keyman/engine/device-detect\";\r\nimport { KeyboardStub } from \"keyman/engine/package-cache\";\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?: (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 Device();\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}", + "// 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// In fact, there's a file there (on its branch) that should be merged with this one!\r\n\r\nconst Codes = {\r\n // Define Keyman Developer modifier bit-flags (exposed for use by other modules)\r\n // Compare against /common/include/kmx_file.h. CTRL+F \"#define LCTRLFLAG\" to find the secton.\r\n modifierCodes: {\r\n \"LCTRL\":0x0001, // LCTRLFLAG\r\n \"RCTRL\":0x0002, // RCTRLFLAG\r\n \"LALT\":0x0004, // LALTFLAG\r\n \"RALT\":0x0008, // RALTFLAG\r\n \"SHIFT\":0x0010, // K_SHIFTFLAG\r\n \"CTRL\":0x0020, // K_CTRLFLAG\r\n \"ALT\":0x0040, // 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\":0x0080, // K_METAFLAG\r\n \"CAPS\":0x0100, // CAPITALFLAG\r\n \"NO_CAPS\":0x0200, // NOTCAPITALFLAG\r\n \"NUM_LOCK\":0x0400, // NUMLOCKFLAG\r\n \"NO_NUM_LOCK\":0x0800, // NOTNUMLOCKFLAG\r\n \"SCROLL_LOCK\":0x1000, // SCROLLFLAG\r\n \"NO_SCROLL_LOCK\":0x2000, // NOTSCROLLFLAG\r\n \"VIRTUAL_KEY\":0x4000, // ISVIRTUALKEY\r\n \"VIRTUAL_CHAR_KEY\":0x8000 // VIRTUALCHARKEY // Unused by KMW, but reserved for use by other Keyman engines.\r\n },\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 },\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 },\r\n\r\n // Define standard keycode numbers (exposed for use by other modules)\r\n keyCodes: {\r\n \"K_BKSP\":8,\"K_TAB\":9,\"K_ENTER\":13,\r\n \"K_SHIFT\":16,\"K_CONTROL\":17,\"K_ALT\":18,\"K_PAUSE\":19,\"K_CAPS\":20,\r\n \"K_ESC\":27,\"K_SPACE\":32,\"K_PGUP\":33,\r\n \"K_PGDN\":34,\"K_END\":35,\"K_HOME\":36,\"K_LEFT\":37,\"K_UP\":38,\r\n \"K_RIGHT\":39,\"K_DOWN\":40,\"K_SEL\":41,\"K_PRINT\":42,\"K_EXEC\":43,\r\n \"K_INS\":45,\"K_DEL\":46,\"K_HELP\":47,\"K_0\":48,\r\n \"K_1\":49,\"K_2\":50,\"K_3\":51,\"K_4\":52,\"K_5\":53,\"K_6\":54,\"K_7\":55,\r\n \"K_8\":56,\"K_9\":57,\"K_A\":65,\"K_B\":66,\"K_C\":67,\"K_D\":68,\"K_E\":69,\r\n \"K_F\":70,\"K_G\":71,\"K_H\":72,\"K_I\":73,\"K_J\":74,\"K_K\":75,\"K_L\":76,\r\n \"K_M\":77,\"K_N\":78,\"K_O\":79,\"K_P\":80,\"K_Q\":81,\"K_R\":82,\"K_S\":83,\r\n \"K_T\":84,\"K_U\":85,\"K_V\":86,\"K_W\":87,\"K_X\":88,\"K_Y\":89,\"K_Z\":90,\r\n \"K_NP0\":96,\"K_NP1\":97,\"K_NP2\":98,\r\n \"K_NP3\":99,\"K_NP4\":100,\"K_NP5\":101,\"K_NP6\":102,\r\n \"K_NP7\":103,\"K_NP8\":104,\"K_NP9\":105,\"K_NPSTAR\":106,\r\n \"K_NPPLUS\":107,\"K_SEPARATOR\":108,\"K_NPMINUS\":109,\"K_NPDOT\":110,\r\n \"K_NPSLASH\":111,\"K_F1\":112,\"K_F2\":113,\"K_F3\":114,\"K_F4\":115,\r\n \"K_F5\":116,\"K_F6\":117,\"K_F7\":118,\"K_F8\":119,\"K_F9\":120,\r\n \"K_F10\":121,\"K_F11\":122,\"K_F12\":123,\"K_NUMLOCK\":144,\"K_SCROLL\":145,\r\n \"K_LSHIFT\":160,\"K_RSHIFT\":161,\"K_LCONTROL\":162,\"K_RCONTROL\":163,\r\n \"K_LALT\":164,\"K_RALT\":165,\r\n \"K_COLON\":186,\"K_EQUAL\":187,\"K_COMMA\":188,\"K_HYPHEN\":189,\r\n \"K_PERIOD\":190,\"K_SLASH\":191,\"K_BKQUOTE\":192,\r\n \"K_LBRKT\":219,\"K_BKSLASH\":220,\"K_RBRKT\":221,\r\n \"K_QUOTE\":222,\"K_oE2\":226,\"K_OE2\":226,\r\n \"K_LOPT\":50001,\"K_ROPT\":50002,\r\n \"K_NUMERALS\":50003,\"K_SYMBOLS\":50004,\"K_CURRENCIES\":50005,\r\n \"K_UPPER\":50006,\"K_LOWER\":50007,\"K_ALPHA\":50008,\r\n \"K_SHIFTED\":50009,\"K_ALTGR\":50010,\r\n \"K_TABBACK\":50011,\"K_TABFWD\":50012\r\n },\r\n\r\n codesUS: [\r\n ['0123456789',';=,-./`', '[\\\\]\\''],\r\n [')!@#$%^&*(',':+<_>?~', '{|}\"']\r\n ],\r\n\r\n isKnownOSKModifierKey(keyID: string): boolean {\r\n switch(keyID) {\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 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 // Refer to text/codes.ts - these are Keyman-custom \"keycodes\" used for\r\n // layer shifting keys. To be safe, we currently let K_TABBACK and\r\n // K_TABFWD through, though we might be able to drop them too.\r\n const code = Codes[keyID];\r\n if(code > 50000 && code < 50011) {\r\n return true;\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 |= Codes.modifierCodes['SHIFT'];\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 |= Codes.modifierCodes['LCTRL'];\r\n ctrlMatched=true;\r\n }\r\n if(layerId.indexOf('rightctrl') >= 0) {\r\n modifier |= Codes.modifierCodes['RCTRL'];\r\n ctrlMatched=true;\r\n }\r\n if(layerId.indexOf('ctrl') >= 0 && !ctrlMatched) {\r\n modifier |= Codes.modifierCodes['CTRL'];\r\n }\r\n\r\n var altMatched=false;\r\n if(layerId.indexOf('leftalt') >= 0) {\r\n modifier |= Codes.modifierCodes['LALT'];\r\n altMatched=true;\r\n }\r\n if(layerId.indexOf('rightalt') >= 0) {\r\n modifier |= Codes.modifierCodes['RALT'];\r\n altMatched=true;\r\n }\r\n if(layerId.indexOf('alt') >= 0 && !altMatched) {\r\n modifier |= Codes.modifierCodes['ALT'];\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 |= Codes.modifierCodes['CAPS'];\r\n } else {\r\n modifier |= Codes.modifierCodes['NO_CAPS'];\r\n }\r\n\r\n return modifier;\r\n }\r\n}\r\n\r\nexport default Codes;", + "// 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\nimport Codes from \"./codes.js\";\r\nimport type KeyEvent from \"./keyEvent.js\";\r\nimport type OutputTarget from \"./outputTarget.js\";\r\n\r\n// The only members referenced are to produce warning and error logs. A little abstraction\r\n// via an optional 'logger' interface can maintain it while facilitating a the split alluded\r\n// to above.\r\n//\r\n// Alternatively, we could just... not take in the parameter at all, which'd also facilitate\r\n// the future modularization effort.\r\nimport RuleBehavior from \"./ruleBehavior.js\";\r\n\r\nexport enum EmulationKeystrokes {\r\n Enter = '\\n',\r\n Backspace = '\\b'\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 public constructor() {\r\n }\r\n\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, ruleBehavior?: RuleBehavior) {\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, ruleBehavior)) != null) {\r\n return char;\r\n } else if((char = this.forBaseKeys(Lkc, ruleBehavior)) != 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, ruleBehavior?: RuleBehavior) {\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(ruleBehavior) {\r\n ruleBehavior.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, ruleBehavior?: RuleBehavior) {\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 == Codes.modifierCodes['SHIFT']) {\r\n keyShiftState = 1;\r\n } else if(keyShiftState != 0) {\r\n if(ruleBehavior) {\r\n ruleBehavior.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(ruleBehavior) {\r\n ruleBehavior.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 \"../index.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 if(keyEventSpec[key] !== undefined) {\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 mappingEvent[key] = 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 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 * @param {Array=} c0 array member being copied\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, c0?): T {\r\n var c = c0 || {};\r\n for (var i in p) {\r\n if(typeof p[i] === 'object' && p[i] != null) {\r\n c[i] = (p[i].constructor === Array ) ? [] : {};\r\n deepCopy(p[i],c[i]);\r\n }\r\n else {\r\n c[i] = p[i];\r\n }\r\n }\r\n\r\n return c;\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.8\";\n static readonly VERSION_RELEASE =\"18.0\";\n static readonly VERSION_MAJOR = \"18\";\n static readonly VERSION_MINOR = \"0\";\n static readonly VERSION_PATCH = \"8\";\n static readonly TIER =\"alpha\";\n static readonly VERSION_TAG = \"-alpha\";\n static readonly VERSION_WITH_TAG = \"18.0.8-alpha\";\n static readonly VERSION_ENVIRONMENT = \"alpha\";\n static readonly VERSION_GIT_TAG = \"release@18.0.8-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)\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) => {\r\n const promise = new TimeoutPromise(time);\r\n return promise.corePromise;\r\n}", + "/*\r\n * The definitions below are duplicated from common/web/types/util/util.ts;\r\n * we can't downcompile the originals to ES5 when bundling with esbuild.\r\n * `import type` stuff is fine, but not non-type `import` statements.\r\n *\r\n * TODO: Use those instead, once we're no longer building ES5 versions of Web.\r\n */\r\n\r\nexport const Uni_LEAD_SURROGATE_START = 0xD800;\r\nexport const Uni_LEAD_SURROGATE_END = 0xDBFF;\r\nexport const Uni_TRAIL_SURROGATE_START = 0xDC00;\r\nexport const Uni_TRAIL_SURROGATE_END = 0xDFFF;\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 KeymanWeb 10.0\r\n Copyright 2017 SIL International\r\n***/\r\n\r\nimport { Version, deepCopy } from \"@keymanapp/web-utils\";\r\nimport { TouchLayout } from \"@keymanapp/common-types\";\r\n\r\nimport LayoutFormFactorBase = 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 \"../text/codes.js\";\r\nimport type Keyboard from \"./keyboard.js\";\r\n\r\nexport type KLS = {[layerName: string]: string[]};\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};\r\n\r\nexport interface LayoutFormFactor extends LayoutFormFactorBase {\r\n // To facilitate those post-processing elements.\r\n layer: LayoutLayer[]\r\n};\r\n\r\nexport type LayoutSpec = {\r\n \"desktop\"?: LayoutFormFactor,\r\n \"phone\"?: LayoutFormFactor,\r\n \"tablet\"?: LayoutFormFactor\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 static readonly DEFAULT_RAW_SPEC = {'F':'Tahoma', 'BK': Layouts.dfltText} 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, keyboard: Keyboard, formFactor: string): LayoutFormFactor {\r\n // Build a layout using the default for the device\r\n var layoutType=formFactor;\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: LayoutFormFactor = deepCopy(Layouts.dfltLayout[layoutType]);\r\n\r\n var n,layers=layout['layer'], keyLabels: KLS=PVK['KLS'], key102=PVK['K102'];\r\n var i, j, k, m, row, 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 // Identify key labels (e.g. *Shift*) that require the special OSK font\r\n var specialLabel=/\\*\\w+\\*/;\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 = [];\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_TAB\":\r\n nextKey=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[layers[n].id] ? Layouts.modifierSpecials[layers[n].id] : \"*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 let modifierCodes = Codes.modifierCodes;\r\n\r\n var s='';\r\n if(m == 0) {\r\n return 'default';\r\n } else {\r\n if(m & modifierCodes['LCTRL']) {\r\n s = (s.length > 0 ? s + '-' : '') + 'leftctrl';\r\n }\r\n if(m & modifierCodes['RCTRL']) {\r\n s = (s.length > 0 ? s + '-' : '') + 'rightctrl';\r\n }\r\n if(m & modifierCodes['LALT']) {\r\n s = (s.length > 0 ? s + '-' : '') + 'leftalt';\r\n }\r\n if(m & modifierCodes['RALT']) {\r\n s = (s.length > 0 ? s + '-' : '') + 'rightalt';\r\n }\r\n if(m & modifierCodes['SHIFT']) {\r\n s = (s.length > 0 ? s + '-' : '') + 'shift';\r\n }\r\n if(m & modifierCodes['CTRL']) {\r\n s = (s.length > 0 ? s + '-' : '') + 'ctrl';\r\n }\r\n if(m & modifierCodes['ALT']) {\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 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 LayoutSubKey[];\r\n if(!Array.isArray(arr)) {\r\n delete rawKey[key];\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];\r\n if(typeof flickObj != 'object') {\r\n delete rawKey[key];\r\n } else {\r\n for(const flickKey of KeyTypesOfFlickList) {\r\n const sk = flickObj[flickKey];\r\n 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];\r\n if(typeof prop != value) {\r\n delete rawKey[key];\r\n }\r\n }\r\n }\r\n\r\n rawKey.text ||= ActiveKey.DEFAULT_KEY.text;\r\n }\r\n\r\n static polyfill(key: LayoutKey, keyboard: Keyboard, layout: ActiveLayout, displayLayer: string, analysisFlagObj?: AnalysisMetadata) {\r\n analysisFlagObj ||= {\r\n hasFlicks: false,\r\n hasLongpresses: false,\r\n hasMultitaps: false\r\n }\r\n\r\n // Add class functions to the existing layout object, allowing it to act as an ActiveLayout.\r\n let dummy = new ActiveKeyBase();\r\n let proto = Object.getPrototypeOf(dummy);\r\n\r\n for(let prop in dummy) {\r\n if(!key.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(key, prop, descriptor);\r\n } else {\r\n key[prop] = dummy[prop];\r\n }\r\n }\r\n }\r\n\r\n if(!key.text && typeof key.id == 'string') {\r\n key.text = ActiveKey.unicodeIDToText(key.id);\r\n }\r\n\r\n // Ensure subkeys are also properly extended.\r\n if(key.sk) {\r\n analysisFlagObj.hasLongpresses = true;\r\n for(let subkey of key.sk) {\r\n ActiveSubKey.polyfill(subkey, keyboard, layout, displayLayer, analysisFlagObj);\r\n }\r\n }\r\n\r\n // Also multitap keys.\r\n if(key.multitap) {\r\n analysisFlagObj.hasMultitaps = true;\r\n for(let mtKey of key.multitap) {\r\n ActiveSubKey.polyfill(mtKey, keyboard, layout, displayLayer, analysisFlagObj);\r\n }\r\n }\r\n\r\n\r\n if(key.flick) {\r\n analysisFlagObj.hasFlicks = true;\r\n for(let flickKey in key.flick) {\r\n ActiveSubKey.polyfill(key.flick[flickKey as keyof TouchLayoutFlick], keyboard, layout, displayLayer, analysisFlagObj);\r\n }\r\n }\r\n\r\n let aKey = key as ActiveKey;\r\n aKey.displayLayer = displayLayer;\r\n aKey.layer = aKey.layer || displayLayer;\r\n\r\n ActiveKeyBase.determineHint(aKey, layout.defaultHint);\r\n\r\n // Compute the key's base KeyEvent properties for use in future event generation\r\n aKey.constructBaseKeyEvent(keyboard, layout, displayLayer);\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);\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 @Enumerable\r\n private constructBaseKeyEvent(keyboard: Keyboard, 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 this._baseKeyEvent = Lkc;\r\n }\r\n}\r\n\r\n\r\nexport class ActiveKey extends ActiveKeyBase implements LayoutKey {\r\n public getSubkey(coreID: string): ActiveKey {\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\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 keyboard: Keyboard,\r\n layout: ActiveLayout,\r\n displayLayer: string,\r\n totalWidth: number,\r\n proportionalY: number,\r\n analysisFlagObj: AnalysisMetadata\r\n ) {\r\n // Apply defaults, setting the width and other undefined properties for each key\r\n let keys=row['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 // Add class functions to the existing layout object, allowing it to act as an ActiveLayout.\r\n let dummy = new ActiveRow();\r\n for(let key in dummy) {\r\n if(!row.hasOwnProperty(key)) {\r\n row[key] = dummy[key];\r\n }\r\n }\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, keyboard: Keyboard, layout: ActiveLayout, analysisFlagObj: AnalysisMetadata) {\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 layer: ActiveLayer[];\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\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 layerMap: {[layerId: string]: ActiveLayer};\r\n\r\n private constructor() {\r\n\r\n }\r\n\r\n @Enumerable\r\n getLayer(layerId: string): ActiveLayer {\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: LayoutFormFactor) {\r\n ActiveLayout.correctLayerEmptyRowBug(rawLayout.layer);\r\n\r\n for(const layer of rawLayout.layer) {\r\n ActiveLayer.sanitize(layer);\r\n }\r\n }\r\n\r\n /**\r\n *\r\n * @param layout\r\n * @param formFactor\r\n */\r\n static polyfill(layout: LayoutFormFactor, 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 // Create a separate OSK div for each OSK layer, only one of which will ever be visible\r\n var n: number;\r\n let layerMap: {[layerId: string]: ActiveLayer} = {};\r\n\r\n let layers=layout.layer;\r\n\r\n // Add class functions to the existing layout object, allowing it to act as an ActiveLayout.\r\n let dummy = new ActiveLayout();\r\n for(let key in dummy) {\r\n if(!layout.hasOwnProperty(key)) {\r\n layout[key] = dummy[key];\r\n }\r\n }\r\n\r\n let aLayout = layout as ActiveLayout;\r\n aLayout.keyboard = keyboard;\r\n aLayout.formFactor = formFactor;\r\n\r\n for(n=0; n entry.id == 'caps')) {\r\n const defaultLayer = layout.layer.find((entry) => entry.id == 'default') as ActiveLayer;\r\n const shiftLayer = layout.layer.find((entry) => entry.id == '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) => ActiveSubKey.polyfill(sk, keyboard, aLayout, 'default'));\r\n shiftShift .multitap.forEach((sk) => ActiveSubKey.polyfill(sk, keyboard, 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 aLayout.layerMap = layerMap;\r\n\r\n return aLayout;\r\n }\r\n}\r\n", + "import Codes from \"../text/codes.js\";\r\nimport { Layouts, type LayoutFormFactor } from \"./defaultLayouts.js\";\r\nimport { ActiveKey, ActiveLayout, ActiveSubKey } from \"./activeLayout.js\";\r\nimport KeyEvent from \"../text/keyEvent.js\";\r\nimport type OutputTarget from \"../text/outputTarget.js\";\r\n\r\nimport type { ComplexKeyboardStore } from \"../text/kbdInterface.js\";\r\n\r\nimport { Version, DeviceSpec } from \"@keymanapp/web-utils\";\r\nimport StateKeyMap from \"./stateKeyMap.js\";\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\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 = {\r\n 'gs': function(outputTarget, keystroke) { 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: any;\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 = {};\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 // TODO: Better typing.\r\n private get _legacyLayoutSpec(): any {\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(): {[formFactor: string]: LayoutFormFactor} {\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) {\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 let modifierCodes = Codes.modifierCodes;\r\n\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 = modifierCodes['LCTRL'] | modifierCodes['LALT'];\r\n var unshiftedEmulationLayer = layers[Layouts.getLayerId(emulationMask)];\r\n var shiftedEmulationLayer = layers[Layouts.getLayerId(modifierCodes['SHIFT'] | 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(modifierCodes['RALT'])]) {\r\n return false;\r\n }\r\n\r\n if(shiftedEmulationLayer != null &&\r\n shiftedEmulationLayer != layers[Layouts.getLayerId(modifierCodes['RALT'] | modifierCodes['SHIFT'])]) {\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): LayoutFormFactor {\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 = this._layouts[formFactor] = Layouts.buildDefaultLayout(rawSpecifications, this, formFactor) as ActiveLayout;\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 rawLayout = ActiveLayout.polyfill(rawLayout, this, formFactor);\r\n this.layoutStates[formFactor] = LayoutState.POLYFILLED;\r\n }\r\n\r\n return rawLayout as ActiveLayout;\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];\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'] ? Codes.modifierCodes['CAPS'] : Codes.modifierCodes['NO_CAPS'];\r\n Lkc.Lstates |= stateKeys['K_NUMLOCK'] ? Codes.modifierCodes['NUM_LOCK'] : Codes.modifierCodes['NO_NUM_LOCK'];\r\n Lkc.Lstates |= stateKeys['K_SCROLL'] ? Codes.modifierCodes['SCROLL_LOCK'] : Codes.modifierCodes['NO_SCROLL_LOCK'];\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 |= Codes.modifierCodes['RALT'];\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 if(!this.scriptObject['VKDictionary']) {\r\n const a=[];\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 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}", + "// 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 /**\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 count(): number {\r\n return this.dks.length;\r\n }\r\n}", + "import type KeyboardInterface from \"./kbdInterface.js\";\r\nimport { SystemStoreIDs } from \"./kbdInterface.js\";\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: KeyboardInterface;\r\n\r\n constructor(keyboardInterface: KeyboardInterface) {\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 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\r\nimport { extendString } from \"@keymanapp/web-utils\";\r\nimport { findCommonSubstringEndIndex } from \"./stringDivergence.js\";\r\n\r\nextendString();\r\n\r\n// Defines deadkey management in a manner attachable to each element interface.\r\nimport type KeyEvent from \"./keyEvent.js\";\r\nimport { Deadkey, DeadkeyTracker } from \"./deadkeys.js\";\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 false;\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\r\n constructor(insert: string, deleteLeft: number, deleteRight?: number) {\r\n this.insert = insert;\r\n this.deleteLeft = deleteLeft;\r\n this.deleteRight = deleteRight || 0;\r\n }\r\n\r\n public static readonly nil = new TextTransform('', 0, 0);\r\n}\r\n\r\nexport class Transcription {\r\n readonly token: number;\r\n readonly keystroke: KeyEvent;\r\n readonly transform: Transform;\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: Transform, 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 {\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): Transform {\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);\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\r\n// Due to some interesting requirements on compile ordering in TS,\r\n// this needs to be in the same file as OutputTarget now.\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 doInputEvent() {\r\n // Mock isn't backed by an element, so it won't have any event listeners.\r\n }\r\n}", + "///\r\n\r\nimport KeyboardProcessor from \"./keyboardProcessor.js\";\r\nimport OutputTarget, { Mock, type Transcription } from \"./outputTarget.js\";\r\nimport { VariableStoreDictionary } from \"../keyboards/keyboard.js\";\r\nimport type { VariableStore } from \"./kbdInterface.js\";\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}", + "/***\r\n KeymanWeb 11.0\r\n Copyright 2019 SIL International\r\n***/\r\n\r\n//#region Imports\r\n\r\nimport { type DeviceSpec } from \"@keymanapp/web-utils\";\r\n\r\nimport Codes from \"./codes.js\";\r\nimport type KeyEvent from \"./keyEvent.js\";\r\nimport type { Deadkey } from \"./deadkeys.js\";\r\nimport KeyMapping from \"./keyMapping.js\";\r\nimport { SystemStore, MutableSystemStore, PlatformSystemStore } from \"./systemStores.js\";\r\nimport type { VariableStoreSerializer } from \"./keyboardProcessor.js\";\r\nimport type OutputTarget from \"./outputTarget.js\";\r\nimport { Mock } from \"./outputTarget.js\";\r\nimport RuleBehavior from \"./ruleBehavior.js\";\r\nimport Keyboard, { VariableStoreDictionary } from \"../keyboards/keyboard.js\";\r\nimport { KeyboardHarness, KeyboardKeymanGlobal } from \"../keyboards/keyboardHarness.js\";\r\n\r\n//#endregion\r\n\r\n//#region Helper type definitions\r\n\r\nexport class KeyInformation {\r\n vk: boolean;\r\n code: number;\r\n modifiers: number;\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\ntype KeyboardStore = PlainKeyboardStore | ComplexKeyboardStore;\r\n\r\nexport type VariableStore = {[name: string]: string};\r\n\r\ntype RuleChar = string;\r\n\r\nclass RuleDeadkey {\r\n /** Discriminant field - 'd' for Deadkey.\r\n */\r\n ['t']: 'd';\r\n\r\n /**\r\n * Value: the deadkey's ID.\r\n */\r\n ['d']: number; // For 'd'eadkey; also reflects the Deadkey class's 'd' property.\r\n}\r\n\r\nclass ContextAny {\r\n /** Discriminant field - 'a' for `any()`.\r\n */\r\n ['t']: 'a';\r\n\r\n /**\r\n * Value: the store to search.\r\n */\r\n ['a']: KeyboardStore; // For 'a'ny statement.\r\n\r\n /**\r\n * If set to true, negates the 'any'.\r\n */\r\n ['n']: boolean|0|1;\r\n}\r\n\r\nclass RuleIndex {\r\n /** Discriminant field - 'i' for `index()`.\r\n */\r\n ['t']: 'i';\r\n\r\n /**\r\n * Value: the Store from which to output\r\n */\r\n ['i']: KeyboardStore;\r\n\r\n /**\r\n * Offset: the offset in context for the corresponding `any()`.\r\n */\r\n ['o']: number;\r\n}\r\n\r\nclass ContextEx {\r\n /** Discriminant field - 'c' for `context()`.\r\n */\r\n ['t']: 'c';\r\n\r\n /**\r\n * Value: The offset into the current rule's context to be matched.\r\n */\r\n ['c']: number; // For 'c'ontext statement.\r\n}\r\n\r\nclass ContextNul {\r\n /** Discriminant field - 'n' for `nul`\r\n */\r\n ['t']: 'n';\r\n}\r\n\r\nclass StoreBeep {\r\n /** Discriminant field - 'b' for `beep`\r\n */\r\n ['t']: 'b';\r\n}\r\n\r\ntype ContextNonCharEntry = RuleDeadkey | ContextAny | RuleIndex | ContextEx | ContextNul;\r\ntype ContextEntry = RuleChar | ContextNonCharEntry;\r\n\r\ntype StoreNonCharEntry = RuleDeadkey | StoreBeep;\r\n\r\n/**\r\n * Cache of context storing and retrieving return values from KC\r\n * Must be reset prior to each keystroke and after any text changes\r\n * MCD 3/1/14\r\n **/\r\nclass CachedContext {\r\n _cache: string[][];\r\n\r\n reset(): void {\r\n this._cache = [];\r\n }\r\n\r\n get(n: number, ln: number): string {\r\n // return null; // uncomment this line to disable context caching\r\n if(typeof this._cache[n] == 'undefined') {\r\n return null;\r\n } else if(typeof this._cache[n][ln] == 'undefined') {\r\n return null;\r\n }\r\n return this._cache[n][ln];\r\n }\r\n\r\n set(n: number, ln: number, val: string): void {\r\n if(typeof this._cache[n] == 'undefined') {\r\n this._cache[n] = [];\r\n }\r\n this._cache[n][ln] = val;\r\n }\r\n};\r\n\r\ntype CachedExEntry = {valContext: (string|number)[], deadContext: Deadkey[]};\r\n/**\r\n * An extended version of cached context storing designed to work with\r\n * `fullContextMatch` and its helper functions.\r\n */\r\nclass CachedContextEx {\r\n _cache: CachedExEntry[][];\r\n\r\n reset(): void {\r\n this._cache = [];\r\n }\r\n\r\n get(n: number, ln: number): CachedExEntry {\r\n // return null; // uncomment this line to disable context caching\r\n if(typeof this._cache[n] == 'undefined') {\r\n return null;\r\n } else if(typeof this._cache[n][ln] == 'undefined') {\r\n return null;\r\n }\r\n return this._cache[n][ln];\r\n }\r\n\r\n set(n: number, ln: number, val: CachedExEntry): void {\r\n if(typeof this._cache[n] == 'undefined') {\r\n this._cache[n] = [];\r\n }\r\n this._cache[n][ln] = val;\r\n }\r\n\r\n clone(): CachedContextEx {\r\n let r = new CachedContextEx();\r\n r._cache = this._cache;\r\n return r;\r\n }\r\n};\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//#endregion\r\n\r\nexport default class KeyboardInterface extends KeyboardHarness {\r\n static readonly GLOBAL_NAME = 'KeymanWeb';\r\n\r\n cachedContext: CachedContext = new CachedContext();\r\n cachedContextEx: CachedContextEx = new CachedContextEx();\r\n ruleContextEx: CachedContextEx;\r\n\r\n activeTargetOutput: OutputTarget;\r\n ruleBehavior: RuleBehavior;\r\n\r\n systemStores: {[storeID: number]: SystemStore};\r\n\r\n _AnyIndices: number[] = []; // AnyIndex - array of any/index match indices\r\n\r\n // Must be accessible to some of the keyboard API methods.\r\n activeKeyboard: Keyboard;\r\n activeDevice: DeviceSpec;\r\n\r\n variableStoreSerializer?: VariableStoreSerializer;\r\n\r\n // A 'reference point' that debug keyboards may use to access KMW's code constants.\r\n public get Codes(): typeof Codes {\r\n return Codes;\r\n }\r\n\r\n constructor(_jsGlobal: any, keymanGlobal: KeyboardKeymanGlobal, variableStoreSerializer: VariableStoreSerializer = null) {\r\n super(_jsGlobal, keymanGlobal);\r\n\r\n this.systemStores = {};\r\n\r\n this.systemStores[SystemStoreIDs.TSS_PLATFORM] = new PlatformSystemStore(this);\r\n this.systemStores[SystemStoreIDs.TSS_LAYER] = new MutableSystemStore(SystemStoreIDs.TSS_LAYER, 'default');\r\n this.systemStores[SystemStoreIDs.TSS_NEWLAYER] = new MutableSystemStore(SystemStoreIDs.TSS_NEWLAYER, '');\r\n this.systemStores[SystemStoreIDs.TSS_OLDLAYER] = new MutableSystemStore(SystemStoreIDs.TSS_OLDLAYER, '');\r\n\r\n this.variableStoreSerializer = variableStoreSerializer;\r\n }\r\n\r\n /**\r\n * Function KSF\r\n * Scope Public\r\n *\r\n * Saves the document's current focus settings on behalf of the keyboard. Often paired with insertText.\r\n */\r\n saveFocus(): void { }\r\n\r\n /**\r\n * A text-insertion method used by custom OSKs for helpHTML interaction, like with sil_euro_latin.\r\n *\r\n * This function currently bypasses web-core's standard text handling control path and all predictive text processing.\r\n * It also has DOM-dependencies that help ensure KMW's active OutputTarget retains focus during use.\r\n */\r\n insertText?: (Ptext: string, PdeadKey: number) => 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): 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[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 = Codes.modifierCodes[\"LALT\"] | Codes.modifierCodes[\"RALT\"];\r\n const CHIRAL_CTRL = Codes.modifierCodes[\"LCTRL\"] | Codes.modifierCodes[\"RCTRL\"];\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 | Codes.modifierCodes[\"ALT\"];\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 | Codes.modifierCodes[\"CTRL\"];\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 if(typeof(s[i]) == 'string') {\r\n if(s[i] == ch) {\r\n Lix = i;\r\n break;\r\n }\r\n } else if(s[i]['d'] === ch['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 var storeEntry = indexChar as StoreNonCharEntry;\r\n\r\n switch(storeEntry.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(storeEntry);\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['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 } else {\r\n return false;\r\n }\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, 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: string) {\r\n if(prototype[longName]) {\r\n prototype[miniName] = prototype[longName];\r\n }\r\n }\r\n\r\n exportKBCallback('KSF', 'saveFocus');\r\n exportKBCallback('KBR', 'beepReset');\r\n exportKBCallback('KT', 'insertText');\r\n exportKBCallback('KR', 'registerKeyboard');\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-processor module.\r\n KeyboardInterface.__publishShorthandAPI();\r\n}());", + "// #region Big ol' list of imports\r\n\r\nimport EventEmitter from 'eventemitter3';\r\n\r\nimport Codes from \"./codes.js\";\r\nimport type Keyboard from \"../keyboards/keyboard.js\";\r\nimport { MinimalKeymanGlobal } from '../keyboards/keyboardHarness.js';\r\nimport KeyEvent from \"./keyEvent.js\";\r\nimport { Layouts } from \"../keyboards/defaultLayouts.js\";\r\nimport type { MutableSystemStore } from \"./systemStores.js\";\r\n\r\nimport DefaultRules, { EmulationKeystrokes } from \"./defaultRules.js\";\r\nimport type OutputTarget from \"./outputTarget.js\";\r\nimport { Mock } from \"./outputTarget.js\";\r\n\r\nimport KeyboardInterface, { SystemStoreIDs, VariableStore } from \"./kbdInterface.js\";\r\nimport RuleBehavior from \"./ruleBehavior.js\";\r\n\r\nimport { DeviceSpec, globalObject } from \"@keymanapp/web-utils\";\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 VariableStoreSerializer {\r\n loadStore(keyboardID: string, storeName: string): VariableStore;\r\n saveStore(keyboardID: string, storeName: string, storeMap: VariableStore);\r\n}\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)) != 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 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'];\r\n const lockKeys = ['K_CAPS', 'K_NUMLOCK', 'K_SCROLL'];\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 &= ~Codes.modifierCodes['RALT'];\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 & Codes.modifierCodes[lockNames[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 ^= Codes.modifierCodes['SHIFT'];\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 var lockNames = ['CAPS', 'NUM_LOCK', 'SCROLL_LOCK'];\r\n var lockKeys = ['K_CAPS', 'K_NUMLOCK', 'K_SCROLL'];\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 const onBit = lockNames[i];\r\n const offBit = 'NO_' + lockNames[i];\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 |= Codes.modifierCodes[onBit];\r\n this.modStateFlags &= ~Codes.modifierCodes[offBit];\r\n } else {\r\n this.modStateFlags &= ~Codes.modifierCodes[onBit];\r\n this.modStateFlags |= Codes.modifierCodes[offBit];\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 false;\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 ^= Codes.modifierCodes['SHIFT'];\r\n break;\r\n case 'leftctrl':\r\n modifier ^= Codes.modifierCodes['LCTRL'];\r\n break;\r\n case 'rightctrl':\r\n modifier ^= Codes.modifierCodes['RCTRL'];\r\n break;\r\n case 'ctrl':\r\n modifier ^= Codes.modifierCodes['CTRL'];\r\n break;\r\n case 'leftalt':\r\n modifier ^= Codes.modifierCodes['LALT'];\r\n break;\r\n case 'rightalt':\r\n modifier ^= Codes.modifierCodes['RALT'];\r\n break;\r\n case 'alt':\r\n modifier ^= Codes.modifierCodes['ALT'];\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 { PathOptionSpec } from \"./optionSpec.interface.js\";\r\nimport { OSKResourcePathConfiguration } from 'keyman/engine/osk';\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 { 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}", + "import StyleConstants from './utils/styleConstants.js';\r\nimport { DeviceSpec, Version } from \"@keymanapp/web-utils\";\r\n\r\n// The Device object definition -------------------------------------------------\r\n\r\nexport class Device {\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 private detected: boolean = false;\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 this.detected = true;\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\r\nexport default Device;", + "import EventEmitter from 'eventemitter3';\r\nimport { DeviceSpec, ManagedPromise, type Keyboard, type KeyboardInterface, type OutputTarget } from '@keymanapp/keyboard-processor';\r\nimport { StubAndKeyboardCache, type KeyboardStub } from 'keyman/engine/package-cache';\r\nimport { PredictionContext } from '@keymanapp/input-processor';\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);\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 }\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 // Perform standard context-reset ops, including the processing of new-context events.\r\n this.resetContext();\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 = 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, type RuleBehavior, Codes } from \"@keymanapp/keyboard-processor\";\r\nimport { KeyEventSourceInterface } from 'keyman/engine/events';\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 const modCodes = Codes.modifierCodes;\r\n\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 & modCodes.SHIFT), !!(s.Lmodifiers & modCodes.CAPS));\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 { Keyboard, KeyboardLoaderBase as KeyboardLoader } from \"@keymanapp/keyboard-processor\";\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 '@keymanapp/keyboard-processor';\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};\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/paths';\r\n\r\nimport { default as KeyboardStub, ErrorStub, KeyboardAPISpec, mergeAndResolveStubPromises } from '../keyboardStub.js';\r\nimport { LanguageAPIPropertySpec, ManagedPromise, Version } from '@keymanapp/keyboard-processor';\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 CloudAPIFont = {\r\n family: string,\r\n source: string | string[]\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: Record>> = {};\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[queryId] = promise as any;\r\n\r\n promise.finally(() => {\r\n delete this.cloudResolutionPromises[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 = 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: ManagedPromise | ManagedPromise = this.cloudResolutionPromises[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 delete this.cloudResolutionPromises[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 this.cache.addStub(entry);\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' && entry.length > 0) {\r\n identifiers.push(entry);\r\n } else { // is some sort of object.\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 \"@keymanapp/input-processor\";\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;\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 e.style['MozUserSelect'] = \"none\";\r\n return e;\r\n}", + "import { DeviceSpec, ManagedPromise } from '@keymanapp/web-utils';\r\nimport { type InternalKeyboardFont as KeyboardFont } from '@keymanapp/keyboard-processor';\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: HTMLStyleElement[] = [];\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;\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) {\r\n if(!(sheet instanceof HTMLLinkElement) && !sheet.innerHTML) {\r\n return;\r\n }\r\n\r\n this.linkedSheets.push(sheet);\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 promises: Promise[] = [];\r\n\r\n for(const sheetElem of this.linkedSheets) {\r\n // Based on https://stackoverflow.com/a/21147238\r\n if(sheetElem.sheet?.cssRules) {\r\n promises.push(Promise.resolve());\r\n } else if(sheetElem.innerHTML) {\r\n // NOT at the StackOverflow link, but something I found experimentally.\r\n // Needed for live-constructed sheets with no corresponding file.\r\n promises.push(Promise.resolve());\r\n } else {\r\n const promise = new ManagedPromise();\r\n sheetElem.addEventListener('load', () => promise.resolve());\r\n sheetElem.addEventListener('error', () => promise.reject());\r\n promises.push(promise.corePromise);\r\n }\r\n }\r\n\r\n const allPromises = promises.concat(this.fontPromises as Promise[]);\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;\r\n }\r\n\r\n if(typeof(fd.files) == 'undefined') {\r\n return;\r\n }\r\n\r\n const fontKey = fd.family;\r\n let source: string;\r\n\r\n let i, ttf='', woff='', fList=[];\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;\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(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 const clearPromise = () => this.fontPromises = this.fontPromises.filter((entry) => entry != loadPromise);\r\n const loadPromise = fontFace.load().then(clearPromise).catch(clearPromise);\r\n this.fontPromises.push(loadPromise);\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.indexOf(stylesheet);\r\n if(index > -1) {\r\n this.linkedSheets.splice(index, 1);\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 sheet of this.linkedSheets) {\r\n if(sheet.parentNode) {\r\n sheet.parentNode.removeChild(sheet);\r\n }\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\nconst no_change = (val: string) => val as string;\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 || no_change) as Type;\r\n }\r\n\r\n save(cookie: Type, encoder?: FilteredRecordEncoder) {\r\n this.saveCookie(this.name, cookie, encoder || no_change);\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 \"@keymanapp/keyboard-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 {\r\n KeyboardInterface as KeyboardInterfaceBase,\r\n} from \"@keymanapp/keyboard-processor\";\r\nimport { KeyboardStub, RawKeyboardStub, toUnprefixedKeyboardId as unprefixed } from 'keyman/engine/package-cache';\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): 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 '@keymanapp/keyboard-processor';\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) => {\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 { ActiveKeyBase, KeyDistribution } from \"@keymanapp/keyboard-processor\";\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 const entry = 1 / (Math.pow(squaredDistMap.get(key), 2) + 1e-6); // Prevent div-by-0 errors.\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 { ActiveKey, ActiveKeyBase, ActiveLayer, ActiveLayout, ActiveRow, Codes } from \"@keymanapp/keyboard-processor\";\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.isKnownOSKModifierKey(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}", + "import { Mock } from \"@keymanapp/keyboard-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}", + "import EventEmitter from \"eventemitter3\";\r\nimport { LMLayer } from \"@keymanapp/lexical-model-layer/web\";\r\nimport { OutputTarget, Transcription, Mock } from \"@keymanapp/keyboard-processor\";\r\nimport ContextWindow from \"../contextWindow.js\";\r\nimport ModelSpec from \"./modelSpec.js\"\r\nimport { TranscriptionCache } from \"../../transcriptionCache.js\";\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 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) => 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 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\ninterface 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/* Is more like the model configuration engine */\r\nexport default 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 }\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 // 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 let 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;\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.lmEngine.revertSuggestion(reversion, new ContextWindow(original.preInput, this.configuration, null))\r\n\r\n return promise.then((suggestions: Suggestion[]) => {\r\n let result = new ReadySuggestions(suggestions, transform.id);\r\n this.emit(\"suggestionsready\", result);\r\n this.currentPromise = null;\r\n\r\n return suggestions;\r\n });\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 }\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 // If and when we do auto-correct, 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('tryaccept', source);\r\n\r\n return 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 * 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\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 PromiseStore from \"./promise-store.js\";\r\n\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 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\\\";/*! https://mths.be/codepointat v0.2.0 by @mathias */String.prototype.codePointAt||function(){\\\"use strict\\\";var L=function(){try{var P={},q=Object.defineProperty,A=q(P,P,P)&&q}catch(N){}return A}(),k=function(P){if(this==null)throw TypeError();var q=String(this),A=q.length,N=P?Number(P):0;if(N!=N&&(N=0),!(N<0||N>=A)){var F=q.charCodeAt(N),R;return F>=55296&&F<=56319&&A>N+1&&(R=q.charCodeAt(N+1),R>=56320&&R<=57343)?(F-55296)*1024+R-56320+65536:F}};L?L(String.prototype,\\\"codePointAt\\\",{value:k,configurable:!0,writable:!0}):String.prototype.codePointAt=k}(),Array.prototype.fill||Object.defineProperty(Array.prototype,\\\"fill\\\",{value:function(L){if(this==null)throw new TypeError(\\\"this is null or not defined\\\");for(var k=Object(this),P=k.length>>>0,q=arguments[1],A=q>>0,N=A<0?Math.max(P+A,0):Math.min(A,P),F=arguments[2],R=F===void 0?P:F>>0,J=R<0?Math.max(P+R,0):Math.min(R,P);N>>0;if(typeof L!=\\\"function\\\")throw new TypeError(\\\"predicate must be a function\\\");for(var q=arguments[1],A=0;A=0&&typeof g.length==\\\"number\\\"&&g.length>=0){var E=Math.floor(h.length),b=Math.floor(g.length),D=0;for(h.length=E+b;D=0&&this._nextIndex=0))return(U=new b(0)).length=0,U;for(D=Math.floor(h.length),(U=new b(D)).length=D;z0&&d[d.length-1])&&(y[0]===6||y[0]===2)){f=0;continue}if(y[0]===3&&(!d||y[1]>d[0]&&y[1]=u.length&&(u=void 0),{value:u&&u[v++],done:!u}}};throw new TypeError(c?\\\"Object is not iterable.\\\":\\\"Symbol.iterator is not defined.\\\")},l=function(u,c){var f=typeof Symbol==\\\"function\\\"&&u[Symbol.iterator];if(!f)return u;var v=f.call(u),p,d=[],m;try{for(;(c===void 0||c-- >0)&&!(p=v.next()).done;)d.push(p.value)}catch(C){m={error:C}}finally{try{p&&!p.done&&(f=v.return)&&f.call(v)}finally{if(m)throw m.error}}return d},o(\\\"__extends\\\",t),o(\\\"__assign\\\",n),o(\\\"__generator\\\",i),o(\\\"__values\\\",a),o(\\\"__read\\\",l)})}}),T={};R(T,{tslib:function(){return he}}),Oe(T,me(we(),1));var he=me(we(),1);function B(){String.kmwFromCharCode=function(r){var e=[],t;for(t=0;t1114111||Math.floor(n)!==n)throw new RangeError(\\\"Invalid code point \\\"+n);n<65536?e.push(n):(n-=65536,e.push((n>>10)+55296),e.push(n%1024+56320))}return String.fromCharCode.apply(void 0,e)},String.prototype.kmwCharCodeAt=function(r){var e=String(this),t=0;if(r<0||r>=e.length)return NaN;for(var n=0;n=55296&&i<=56319&&e.length>t+1){var a=e.charCodeAt(t+1);if(a>=56320&&a<=57343)return(i-55296<<10)+(a-56320)+65536}return i},String.prototype.kmwIndexOf=function(r,e){var t=String(this),n=t.indexOf(r,e);if(n<0)return n;for(var i=0,a=0;a!==null&&ae){var a=r;r=e,e=a}n=t.kmwCodePointToCodeUnit(r),i=t.kmwCodePointToCodeUnit(e)}return(isNaN(n)||n===null)&&(n=0),(isNaN(i)||i===null)&&(i=t.length),t.substring(n,i)},String.prototype.kmwNextChar=function(r){var e=String(this);if(r===null||r<0||r>=e.length-1)return null;var t=e.charCodeAt(r);if(t>=55296&&t<=56319&&e.length>r+1){var n=e.charCodeAt(r+1);if(n>=56320&&n<=57343)return r==e.length-2?null:r+2}return r+1},String.prototype.kmwPrevChar=function(r){var e=String(this);if(r==null||r<=0||r>e.length)return null;var t=e.charCodeAt(r-1);if(t>=56320&&t<=57343&&r>1){var n=e.charCodeAt(r-2);if(n>=55296&&n<=56319)return r-2}return r-1},String.prototype.kmwCodePointToCodeUnit=function(r){if(r===null)return null;var e=String(this),t=0;if(r<0){t=e.length;for(var n=0;n>r;n--)t=e.kmwPrevChar(t);return t}if(r==e.kmwLength())return e.length;for(var n=0;n=0?e.kmwSubstr(r,1):\\\"\\\"},String.prototype.kmwBMPNextChar=function(r){var e=String(this);return r<0||r>=e.length-1?null:r+1},String.prototype.kmwBMPPrevChar=function(r){var e=String(this);return r<=0||r>e.length?null:r-1},String.prototype.kmwBMPCodePointToCodeUnit=function(r){return r},String.prototype.kmwBMPCodeUnitToCodePoint=function(r){return r},String.prototype.kmwBMPLength=function(){var r=String(this);return r.length},String.prototype.kmwBMPSubstr=function(r,e){var t=String(this);return r>-1?t.substr(r,e):t.substr(t.length+r,-r)},String.kmwEnableSupplementaryPlane=function(r){var e=String.prototype;String._kmwFromCharCode=r?String.kmwFromCharCode:String.fromCharCode,e._kmwCharAt=r?e.kmwCharAt:e.charAt,e._kmwCharCodeAt=r?e.kmwCharCodeAt:e.charCodeAt,e._kmwIndexOf=r?e.kmwIndexOf:e.indexOf,e._kmwLastIndexOf=r?e.kmwLastIndexOf:e.lastIndexOf,e._kmwSlice=r?e.kmwSlice:e.slice,e._kmwSubstring=r?e.kmwSubstring:e.substring,e._kmwSubstr=r?e.kmwSubstr:e.kmwBMPSubstr,e._kmwLength=r?e.kmwLength:e.kmwBMPLength,e._kmwNextChar=r?e.kmwNextChar:e.kmwBMPNextChar,e._kmwPrevChar=r?e.kmwPrevChar:e.kmwBMPPrevChar,e._kmwCodePointToCodeUnit=r?e.kmwCodePointToCodeUnit:e.kmwBMPCodePointToCodeUnit,e._kmwCodeUnitToCodePoint=r?e.kmwCodeUnitToCodePoint:e.kmwBMPCodeUnitToCodePoint},String._kmwFromCharCode||String.kmwEnableSupplementaryPlane(!1)}B();var G={};R(G,{DummyModel:function(){return vt},PriorityQueue:function(){return b},QuoteBehavior:function(){return U},SENTINEL_CODE_UNIT:function(){return Q},TrieModel:function(){return lt},applyTransform:function(){return H},buildMergedTransform:function(){return le},defaultApplyCasing:function(){return g},getLastPreCaretToken:function(){return pe},isHighSurrogate:function(){return $},isLowSurrogate:function(){return M},isSentinel:function(){return X},tokenize:function(){return z},transformToSuggestion:function(){return h},wordbreak:function(){return Ae}}),B();var Q=\\\"\\\\uFDD0\\\";function H(r,e){var t,n,i=e.left||\\\"\\\",a=i.kmwLength(),l=a=55296&&r<=56319}function M(r){return typeof r==\\\"string\\\"&&(r=r.charCodeAt(0)),r>=56320&&r<=57343}function X(r){return r==Q}function h(r,e){var t={transform:r,transformId:r.id,displayAs:r.insert};return(e===0||e)&&(t.p=e),t}function g(r,e){switch(r){case\\\"lower\\\":return e.toLowerCase();case\\\"upper\\\":return e.toUpperCase();case\\\"initial\\\":var t=1;return e.length>1&&$(e.charAt(0))&&M(e.charCodeAt(1))&&(t=2),e.substring(0,t).toUpperCase().concat(e.substring(t))}}var E=function(){function r(e,t){if(typeof e!=\\\"function\\\"){this.comparator=e.comparator,this.heap=[].concat(e.heap);return}var n=e;this.comparator=n,this.heap=(t!=null?t:[]).slice(0),this.heapify()}return r.leftChildIndex=function(e){return e*2+1},r.rightChildIndex=function(e){return e*2+2},r.parentIndex=function(e){return Math.floor((e-1)/2)},r.prototype.heapify=function(e,t){if(e==null||t==null){this.heapify(0,this.count-1);return}for(var n=[],i=-1,a=t;a>=e;a--){var l=r.parentIndex(a);this.siftDown(a)&&l0;){var o=n.shift(),l=r.parentIndex(o);this.siftDown(o)&&l>=0&&i!=l&&(n.push(l),i=l)}},Object.defineProperty(r.prototype,\\\"count\\\",{get:function(){return this.heap.length},enumerable:!1,configurable:!0}),r.prototype.peek=function(){return this.heap[0]},r.prototype.enqueue=function(e){var t=this.heap.length;this.heap.push(e);for(var n=r.parentIndex,i=n(t);t!==0&&this.comparator(this.heap[t],this.heap[i])<0;){var a=this.heap[t];this.heap[t]=this.heap[i],this.heap[i]=a,t=i,i=n(t)}},r.prototype.enqueueAll=function(e){if(e.length!=0){var t=this.count;this.heap=this.heap.concat(e);var n=r.parentIndex(t);this.heapify(n>=0?n:0,r.parentIndex(this.count-1))}},r.prototype.dequeue=function(){if(this.count!=0){var e=this.heap[0],t=this.heap.pop();return this.heap.length>0&&(this.heap[0]=t,this.siftDown(0)),e}},r.prototype.siftDown=function(e){var t=r.leftChildIndex(e),n=r.rightChildIndex(e),i=e;if(t0&&(i=t[t.length-1]),t.length>1){var a=t[t.length-2];if(a.end==i.start&&i.text==\\\"'\\\"){var l={text:a.text+i.text,start:a.start,end:i.end,length:a.length+i.length};t.pop(),t.pop(),t.push(l),i=l}}var o={left:t.map(function(f){return f.text}),right:n.map(function(f){return f.text}),caretSplitsToken:!1};if(t.length>0&&n.length>0){var s=n[0],u=i.end!=e.left.length,c=s.start!=0;if(u||c)return o;r(i.text+s.text).length==1&&(o.caretSplitsToken=!0)}return o}function pe(r,e){var t=z(r,e);return t.left.length>0?t.left.pop():\\\"\\\"}function Ae(r,e){return pe(r,e)}var Pe={};R(Pe,{ascii:function(){return tt},default:function(){return ce},defaultWordbreaker:function(){return ce},placeholder:function(){return et}});function et(r){var e=0;return r.split(/\\\\s+/).map(function(t){var n={start:e,end:e+t.length,text:t,length:t.length};return e=n.end,n})}function tt(r){for(var e=/[A-Za-z0-9']+/g,t=[],n;(n=e.exec(r))!==null;)t.push(new rt(n[0],n.index));return t}var rt=function(){function r(e,t){this.text=e,this.start=t}return Object.defineProperty(r.prototype,\\\"length\\\",{get:function(){return this.text.length},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,\\\"end\\\",{get:function(){return this.start+this.text.length},enumerable:!1,configurable:!0}),r}(),nt=[\\\"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\\\"],Me=[[0,0],[10,1],[11,2],[13,3],[14,0],[32,4],[33,0],[34,5],[35,0],[39,6],[40,0],[44,7],[45,0],[46,8],[47,0],[48,9],[58,10],[59,7],[60,0],[65,11],[91,0],[95,12],[96,0],[97,11],[123,0],[133,2],[134,0],[170,11],[171,0],[173,13],[174,0],[181,11],[182,0],[183,10],[184,0],[186,11],[187,0],[192,11],[215,0],[216,11],[247,0],[248,11],[728,0],[734,11],[768,14],[880,11],[885,0],[886,11],[888,0],[890,11],[894,7],[895,11],[896,0],[902,11],[903,10],[904,11],[907,0],[908,11],[909,0],[910,11],[930,0],[931,11],[1014,0],[1015,11],[1154,0],[1155,14],[1162,11],[1328,0],[1329,11],[1367,0],[1369,11],[1373,0],[1374,11],[1375,10],[1376,11],[1417,7],[1418,11],[1419,0],[1425,14],[1470,0],[1471,14],[1472,0],[1473,14],[1475,0],[1476,14],[1478,0],[1479,14],[1480,0],[1488,15],[1515,0],[1519,15],[1523,11],[1524,10],[1525,0],[1536,13],[1542,0],[1548,7],[1550,0],[1552,14],[1563,0],[1564,13],[1565,0],[1568,11],[1611,14],[1632,9],[1642,0],[1643,9],[1644,7],[1645,0],[1646,11],[1648,14],[1649,11],[1748,0],[1749,11],[1750,14],[1757,13],[1758,0],[1759,14],[1765,11],[1767,14],[1769,0],[1770,14],[1774,11],[1776,9],[1786,11],[1789,0],[1791,11],[1792,0],[1807,13],[1808,11],[1809,14],[1810,11],[1840,14],[1867,0],[1869,11],[1958,14],[1969,11],[1970,0],[1984,9],[1994,11],[2027,14],[2036,11],[2038,0],[2040,7],[2041,0],[2042,11],[2043,0],[2045,14],[2046,0],[2048,11],[2070,14],[2074,11],[2075,14],[2084,11],[2085,14],[2088,11],[2089,14],[2094,0],[2112,11],[2137,14],[2140,0],[2144,11],[2155,0],[2208,11],[2229,0],[2230,11],[2248,0],[2259,14],[2274,13],[2275,14],[2308,11],[2362,14],[2365,11],[2366,14],[2384,11],[2385,14],[2392,11],[2402,14],[2404,0],[2406,9],[2416,0],[2417,11],[2433,14],[2436,0],[2437,11],[2445,0],[2447,11],[2449,0],[2451,11],[2473,0],[2474,11],[2481,0],[2482,11],[2483,0],[2486,11],[2490,0],[2492,14],[2493,11],[2494,14],[2501,0],[2503,14],[2505,0],[2507,14],[2510,11],[2511,0],[2519,14],[2520,0],[2524,11],[2526,0],[2527,11],[2530,14],[2532,0],[2534,9],[2544,11],[2546,0],[2556,11],[2557,0],[2558,14],[2559,0],[2561,14],[2564,0],[2565,11],[2571,0],[2575,11],[2577,0],[2579,11],[2601,0],[2602,11],[2609,0],[2610,11],[2612,0],[2613,11],[2615,0],[2616,11],[2618,0],[2620,14],[2621,0],[2622,14],[2627,0],[2631,14],[2633,0],[2635,14],[2638,0],[2641,14],[2642,0],[2649,11],[2653,0],[2654,11],[2655,0],[2662,9],[2672,14],[2674,11],[2677,14],[2678,0],[2689,14],[2692,0],[2693,11],[2702,0],[2703,11],[2706,0],[2707,11],[2729,0],[2730,11],[2737,0],[2738,11],[2740,0],[2741,11],[2746,0],[2748,14],[2749,11],[2750,14],[2758,0],[2759,14],[2762,0],[2763,14],[2766,0],[2768,11],[2769,0],[2784,11],[2786,14],[2788,0],[2790,9],[2800,0],[2809,11],[2810,14],[2816,0],[2817,14],[2820,0],[2821,11],[2829,0],[2831,11],[2833,0],[2835,11],[2857,0],[2858,11],[2865,0],[2866,11],[2868,0],[2869,11],[2874,0],[2876,14],[2877,11],[2878,14],[2885,0],[2887,14],[2889,0],[2891,14],[2894,0],[2901,14],[2904,0],[2908,11],[2910,0],[2911,11],[2914,14],[2916,0],[2918,9],[2928,0],[2929,11],[2930,0],[2946,14],[2947,11],[2948,0],[2949,11],[2955,0],[2958,11],[2961,0],[2962,11],[2966,0],[2969,11],[2971,0],[2972,11],[2973,0],[2974,11],[2976,0],[2979,11],[2981,0],[2984,11],[2987,0],[2990,11],[3002,0],[3006,14],[3011,0],[3014,14],[3017,0],[3018,14],[3022,0],[3024,11],[3025,0],[3031,14],[3032,0],[3046,9],[3056,0],[3072,14],[3077,11],[3085,0],[3086,11],[3089,0],[3090,11],[3113,0],[3114,11],[3130,0],[3133,11],[3134,14],[3141,0],[3142,14],[3145,0],[3146,14],[3150,0],[3157,14],[3159,0],[3160,11],[3163,0],[3168,11],[3170,14],[3172,0],[3174,9],[3184,0],[3200,11],[3201,14],[3204,0],[3205,11],[3213,0],[3214,11],[3217,0],[3218,11],[3241,0],[3242,11],[3252,0],[3253,11],[3258,0],[3260,14],[3261,11],[3262,14],[3269,0],[3270,14],[3273,0],[3274,14],[3278,0],[3285,14],[3287,0],[3294,11],[3295,0],[3296,11],[3298,14],[3300,0],[3302,9],[3312,0],[3313,11],[3315,0],[3328,14],[3332,11],[3341,0],[3342,11],[3345,0],[3346,11],[3387,14],[3389,11],[3390,14],[3397,0],[3398,14],[3401,0],[3402,14],[3406,11],[3407,0],[3412,11],[3415,14],[3416,0],[3423,11],[3426,14],[3428,0],[3430,9],[3440,0],[3450,11],[3456,0],[3457,14],[3460,0],[3461,11],[3479,0],[3482,11],[3506,0],[3507,11],[3516,0],[3517,11],[3518,0],[3520,11],[3527,0],[3530,14],[3531,0],[3535,14],[3541,0],[3542,14],[3543,0],[3544,14],[3552,0],[3558,9],[3568,0],[3570,14],[3572,0],[3633,14],[3634,0],[3636,14],[3643,0],[3655,14],[3663,0],[3664,9],[3674,0],[3761,14],[3762,0],[3764,14],[3773,0],[3784,14],[3790,0],[3792,9],[3802,0],[3840,11],[3841,0],[3864,14],[3866,0],[3872,9],[3882,0],[3893,14],[3894,0],[3895,14],[3896,0],[3897,14],[3898,0],[3902,14],[3904,11],[3912,0],[3913,11],[3949,0],[3953,14],[3973,0],[3974,14],[3976,11],[3981,14],[3992,0],[3993,14],[4029,0],[4038,14],[4039,0],[4139,14],[4159,0],[4160,9],[4170,0],[4182,14],[4186,0],[4190,14],[4193,0],[4194,14],[4197,0],[4199,14],[4206,0],[4209,14],[4213,0],[4226,14],[4238,0],[4239,14],[4240,9],[4250,14],[4254,0],[4256,11],[4294,0],[4295,11],[4296,0],[4301,11],[4302,0],[4304,11],[4347,0],[4348,11],[4681,0],[4682,11],[4686,0],[4688,11],[4695,0],[4696,11],[4697,0],[4698,11],[4702,0],[4704,11],[4745,0],[4746,11],[4750,0],[4752,11],[4785,0],[4786,11],[4790,0],[4792,11],[4799,0],[4800,11],[4801,0],[4802,11],[4806,0],[4808,11],[4823,0],[4824,11],[4881,0],[4882,11],[4886,0],[4888,11],[4955,0],[4957,14],[4960,0],[4992,11],[5008,0],[5024,11],[5110,0],[5112,11],[5118,0],[5121,11],[5741,0],[5743,11],[5760,4],[5761,11],[5787,0],[5792,11],[5867,0],[5870,11],[5881,0],[5888,11],[5901,0],[5902,11],[5906,14],[5909,0],[5920,11],[5938,14],[5941,0],[5952,11],[5970,14],[5972,0],[5984,11],[5997,0],[5998,11],[6001,0],[6002,14],[6004,0],[6068,14],[6100,0],[6109,14],[6110,0],[6112,9],[6122,0],[6155,14],[6158,13],[6159,0],[6160,9],[6170,0],[6176,11],[6265,0],[6272,11],[6277,14],[6279,11],[6313,14],[6314,11],[6315,0],[6320,11],[6390,0],[6400,11],[6431,0],[6432,14],[6444,0],[6448,14],[6460,0],[6470,9],[6480,0],[6608,9],[6618,0],[6656,11],[6679,14],[6684,0],[6741,14],[6751,0],[6752,14],[6781,0],[6783,14],[6784,9],[6794,0],[6800,9],[6810,0],[6832,14],[6849,0],[6912,14],[6917,11],[6964,14],[6981,11],[6988,0],[6992,9],[7002,0],[7019,14],[7028,0],[7040,14],[7043,11],[7073,14],[7086,11],[7088,9],[7098,11],[7142,14],[7156,0],[7168,11],[7204,14],[7224,0],[7232,9],[7242,0],[7245,11],[7248,9],[7258,11],[7294,0],[7296,11],[7305,0],[7312,11],[7355,0],[7357,11],[7360,0],[7376,14],[7379,0],[7380,14],[7401,11],[7405,14],[7406,11],[7412,14],[7413,11],[7415,14],[7418,11],[7419,0],[7424,11],[7616,14],[7674,0],[7675,14],[7680,11],[7958,0],[7960,11],[7966,0],[7968,11],[8006,0],[8008,11],[8014,0],[8016,11],[8024,0],[8025,11],[8026,0],[8027,11],[8028,0],[8029,11],[8030,0],[8031,11],[8062,0],[8064,11],[8117,0],[8118,11],[8125,0],[8126,11],[8127,0],[8130,11],[8133,0],[8134,11],[8141,0],[8144,11],[8148,0],[8150,11],[8156,0],[8160,11],[8173,0],[8178,11],[8181,0],[8182,11],[8189,0],[8192,4],[8199,0],[8200,4],[8203,0],[8204,14],[8205,16],[8206,13],[8208,0],[8216,8],[8218,0],[8228,8],[8229,0],[8231,10],[8232,2],[8234,13],[8239,12],[8240,0],[8255,12],[8257,0],[8260,7],[8261,0],[8276,12],[8277,0],[8287,4],[8288,13],[8293,0],[8294,13],[8304,0],[8305,11],[8306,0],[8319,11],[8320,0],[8336,11],[8349,0],[8400,14],[8433,0],[8450,11],[8451,0],[8455,11],[8456,0],[8458,11],[8468,0],[8469,11],[8470,0],[8473,11],[8478,0],[8484,11],[8485,0],[8486,11],[8487,0],[8488,11],[8489,0],[8490,11],[8494,0],[8495,11],[8506,0],[8508,11],[8512,0],[8517,11],[8522,0],[8526,11],[8527,0],[8544,11],[8585,0],[9398,11],[9450,0],[11264,11],[11311,0],[11312,11],[11359,0],[11360,11],[11493,0],[11499,11],[11503,14],[11506,11],[11508,0],[11520,11],[11558,0],[11559,11],[11560,0],[11565,11],[11566,0],[11568,11],[11624,0],[11631,11],[11632,0],[11647,14],[11648,11],[11671,0],[11680,11],[11687,0],[11688,11],[11695,0],[11696,11],[11703,0],[11704,11],[11711,0],[11712,11],[11719,0],[11720,11],[11727,0],[11728,11],[11735,0],[11736,11],[11743,0],[11744,14],[11776,0],[11823,11],[11824,0],[12288,4],[12289,0],[12293,11],[12294,0],[12330,14],[12336,0],[12337,17],[12342,0],[12347,11],[12349,0],[12441,14],[12443,17],[12445,0],[12448,17],[12539,0],[12540,17],[12544,0],[12549,11],[12592,0],[12593,11],[12687,0],[12704,11],[12736,0],[12784,17],[12800,0],[13008,17],[13055,0],[13056,17],[13144,0],[40960,11],[42125,0],[42192,11],[42238,0],[42240,11],[42509,0],[42512,11],[42528,9],[42538,11],[42540,0],[42560,11],[42607,14],[42611,0],[42612,14],[42622,0],[42623,11],[42654,14],[42656,11],[42736,14],[42738,0],[42760,11],[42944,0],[42946,11],[42955,0],[42997,11],[43010,14],[43011,11],[43014,14],[43015,11],[43019,14],[43020,11],[43043,14],[43048,0],[43052,14],[43053,0],[43072,11],[43124,0],[43136,14],[43138,11],[43188,14],[43206,0],[43216,9],[43226,0],[43232,14],[43250,11],[43256,0],[43259,11],[43260,0],[43261,11],[43263,14],[43264,9],[43274,11],[43302,14],[43310,0],[43312,11],[43335,14],[43348,0],[43360,11],[43389,0],[43392,14],[43396,11],[43443,14],[43457,0],[43471,11],[43472,9],[43482,0],[43493,14],[43494,0],[43504,9],[43514,0],[43520,11],[43561,14],[43575,0],[43584,11],[43587,14],[43588,11],[43596,14],[43598,0],[43600,9],[43610,0],[43643,14],[43646,0],[43696,14],[43697,0],[43698,14],[43701,0],[43703,14],[43705,0],[43710,14],[43712,0],[43713,14],[43714,0],[43744,11],[43755,14],[43760,0],[43762,11],[43765,14],[43767,0],[43777,11],[43783,0],[43785,11],[43791,0],[43793,11],[43799,0],[43808,11],[43815,0],[43816,11],[43823,0],[43824,11],[43882,0],[43888,11],[44003,14],[44011,0],[44012,14],[44014,0],[44016,9],[44026,0],[44032,11],[55204,0],[55216,11],[55239,0],[55243,11],[55292,0],[64256,11],[64263,0],[64275,11],[64280,0],[64285,15],[64286,14],[64287,15],[64297,0],[64298,15],[64311,0],[64312,15],[64317,0],[64318,15],[64319,0],[64320,15],[64322,0],[64323,15],[64325,0],[64326,15],[64336,11],[64434,0],[64467,11],[64830,0],[64848,11],[64912,0],[64914,11],[64968,0],[65008,11],[65020,0],[65024,14],[65040,7],[65041,0],[65043,10],[65044,7],[65045,0],[65056,14],[65072,0],[65075,12],[65077,0],[65101,12],[65104,7],[65105,0],[65106,8],[65107,0],[65108,7],[65109,10],[65110,0],[65136,11],[65141,0],[65142,11],[65277,0],[65279,13],[65280,0],[65287,8],[65288,0],[65292,7],[65293,0],[65294,8],[65295,0],[65296,9],[65306,10],[65307,7],[65308,0],[65313,11],[65339,0],[65343,12],[65344,0],[65345,11],[65371,0],[65382,17],[65438,14],[65440,11],[65471,0],[65474,11],[65480,0],[65482,11],[65488,0],[65490,11],[65496,0],[65498,11],[65501,0],[65529,13],[65532,0],[65536,11],[65548,0],[65549,11],[65575,0],[65576,11],[65595,0],[65596,11],[65598,0],[65599,11],[65614,0],[65616,11],[65630,0],[65664,11],[65787,0],[65856,11],[65909,0],[66045,14],[66046,0],[66176,11],[66205,0],[66208,11],[66257,0],[66272,14],[66273,0],[66304,11],[66336,0],[66349,11],[66379,0],[66384,11],[66422,14],[66427,0],[66432,11],[66462,0],[66464,11],[66500,0],[66504,11],[66512,0],[66513,11],[66518,0],[66560,11],[66718,0],[66720,9],[66730,0],[66736,11],[66772,0],[66776,11],[66812,0],[66816,11],[66856,0],[66864,11],[66916,0],[67072,11],[67383,0],[67392,11],[67414,0],[67424,11],[67432,0],[67584,11],[67590,0],[67592,11],[67593,0],[67594,11],[67638,0],[67639,11],[67641,0],[67644,11],[67645,0],[67647,11],[67670,0],[67680,11],[67703,0],[67712,11],[67743,0],[67808,11],[67827,0],[67828,11],[67830,0],[67840,11],[67862,0],[67872,11],[67898,0],[67968,11],[68024,0],[68030,11],[68032,0],[68096,11],[68097,14],[68100,0],[68101,14],[68103,0],[68108,14],[68112,11],[68116,0],[68117,11],[68120,0],[68121,11],[68150,0],[68152,14],[68155,0],[68159,14],[68160,0],[68192,11],[68221,0],[68224,11],[68253,0],[68288,11],[68296,0],[68297,11],[68325,14],[68327,0],[68352,11],[68406,0],[68416,11],[68438,0],[68448,11],[68467,0],[68480,11],[68498,0],[68608,11],[68681,0],[68736,11],[68787,0],[68800,11],[68851,0],[68864,11],[68900,14],[68904,0],[68912,9],[68922,0],[69248,11],[69290,0],[69291,14],[69293,0],[69296,11],[69298,0],[69376,11],[69405,0],[69415,11],[69416,0],[69424,11],[69446,14],[69457,0],[69552,11],[69573,0],[69600,11],[69623,0],[69632,14],[69635,11],[69688,14],[69703,0],[69734,9],[69744,0],[69759,14],[69763,11],[69808,14],[69819,0],[69821,13],[69822,0],[69837,13],[69838,0],[69840,11],[69865,0],[69872,9],[69882,0],[69888,14],[69891,11],[69927,14],[69941,0],[69942,9],[69952,0],[69956,11],[69957,14],[69959,11],[69960,0],[69968,11],[70003,14],[70004,0],[70006,11],[70007,0],[70016,14],[70019,11],[70067,14],[70081,11],[70085,0],[70089,14],[70093,0],[70094,14],[70096,9],[70106,11],[70107,0],[70108,11],[70109,0],[70144,11],[70162,0],[70163,11],[70188,14],[70200,0],[70206,14],[70207,0],[70272,11],[70279,0],[70280,11],[70281,0],[70282,11],[70286,0],[70287,11],[70302,0],[70303,11],[70313,0],[70320,11],[70367,14],[70379,0],[70384,9],[70394,0],[70400,14],[70404,0],[70405,11],[70413,0],[70415,11],[70417,0],[70419,11],[70441,0],[70442,11],[70449,0],[70450,11],[70452,0],[70453,11],[70458,0],[70459,14],[70461,11],[70462,14],[70469,0],[70471,14],[70473,0],[70475,14],[70478,0],[70480,11],[70481,0],[70487,14],[70488,0],[70493,11],[70498,14],[70500,0],[70502,14],[70509,0],[70512,14],[70517,0],[70656,11],[70709,14],[70727,11],[70731,0],[70736,9],[70746,0],[70750,14],[70751,11],[70754,0],[70784,11],[70832,14],[70852,11],[70854,0],[70855,11],[70856,0],[70864,9],[70874,0],[71040,11],[71087,14],[71094,0],[71096,14],[71105,0],[71128,11],[71132,14],[71134,0],[71168,11],[71216,14],[71233,0],[71236,11],[71237,0],[71248,9],[71258,0],[71296,11],[71339,14],[71352,11],[71353,0],[71360,9],[71370,0],[71453,14],[71468,0],[71472,9],[71482,0],[71680,11],[71724,14],[71739,0],[71840,11],[71904,9],[71914,0],[71935,11],[71943,0],[71945,11],[71946,0],[71948,11],[71956,0],[71957,11],[71959,0],[71960,11],[71984,14],[71990,0],[71991,14],[71993,0],[71995,14],[71999,11],[72e3,14],[72001,11],[72002,14],[72004,0],[72016,9],[72026,0],[72096,11],[72104,0],[72106,11],[72145,14],[72152,0],[72154,14],[72161,11],[72162,0],[72163,11],[72164,14],[72165,0],[72192,11],[72193,14],[72203,11],[72243,14],[72250,11],[72251,14],[72255,0],[72263,14],[72264,0],[72272,11],[72273,14],[72284,11],[72330,14],[72346,0],[72349,11],[72350,0],[72384,11],[72441,0],[72704,11],[72713,0],[72714,11],[72751,14],[72759,0],[72760,14],[72768,11],[72769,0],[72784,9],[72794,0],[72818,11],[72848,0],[72850,14],[72872,0],[72873,14],[72887,0],[72960,11],[72967,0],[72968,11],[72970,0],[72971,11],[73009,14],[73015,0],[73018,14],[73019,0],[73020,14],[73022,0],[73023,14],[73030,11],[73031,14],[73032,0],[73040,9],[73050,0],[73056,11],[73062,0],[73063,11],[73065,0],[73066,11],[73098,14],[73103,0],[73104,14],[73106,0],[73107,14],[73112,11],[73113,0],[73120,9],[73130,0],[73440,11],[73459,14],[73463,0],[73648,11],[73649,0],[73728,11],[74650,0],[74752,11],[74863,0],[74880,11],[75076,0],[77824,11],[78895,0],[78896,13],[78905,0],[82944,11],[83527,0],[92160,11],[92729,0],[92736,11],[92767,0],[92768,9],[92778,0],[92880,11],[92910,0],[92912,14],[92917,0],[92928,11],[92976,14],[92983,0],[92992,11],[92996,0],[93008,9],[93018,0],[93027,11],[93048,0],[93053,11],[93072,0],[93760,11],[93824,0],[93952,11],[94027,0],[94031,14],[94032,11],[94033,14],[94088,0],[94095,14],[94099,11],[94112,0],[94176,11],[94178,0],[94179,11],[94180,14],[94181,0],[94192,14],[94194,0],[110592,17],[110593,0],[110948,17],[110952,0],[113664,11],[113771,0],[113776,11],[113789,0],[113792,11],[113801,0],[113808,11],[113818,0],[113821,14],[113823,0],[113824,13],[113828,0],[119141,14],[119146,0],[119149,14],[119155,13],[119163,14],[119171,0],[119173,14],[119180,0],[119210,14],[119214,0],[119362,14],[119365,0],[119808,11],[119893,0],[119894,11],[119965,0],[119966,11],[119968,0],[119970,11],[119971,0],[119973,11],[119975,0],[119977,11],[119981,0],[119982,11],[119994,0],[119995,11],[119996,0],[119997,11],[120004,0],[120005,11],[120070,0],[120071,11],[120075,0],[120077,11],[120085,0],[120086,11],[120093,0],[120094,11],[120122,0],[120123,11],[120127,0],[120128,11],[120133,0],[120134,11],[120135,0],[120138,11],[120145,0],[120146,11],[120486,0],[120488,11],[120513,0],[120514,11],[120539,0],[120540,11],[120571,0],[120572,11],[120597,0],[120598,11],[120629,0],[120630,11],[120655,0],[120656,11],[120687,0],[120688,11],[120713,0],[120714,11],[120745,0],[120746,11],[120771,0],[120772,11],[120780,0],[120782,9],[120832,0],[121344,14],[121399,0],[121403,14],[121453,0],[121461,14],[121462,0],[121476,14],[121477,0],[121499,14],[121504,0],[121505,14],[121520,0],[122880,14],[122887,0],[122888,14],[122905,0],[122907,14],[122914,0],[122915,14],[122917,0],[122918,14],[122923,0],[123136,11],[123181,0],[123184,14],[123191,11],[123198,0],[123200,9],[123210,0],[123214,11],[123215,0],[123584,11],[123628,14],[123632,9],[123642,0],[124928,11],[125125,0],[125136,14],[125143,0],[125184,11],[125252,14],[125259,11],[125260,0],[125264,9],[125274,0],[126464,11],[126468,0],[126469,11],[126496,0],[126497,11],[126499,0],[126500,11],[126501,0],[126503,11],[126504,0],[126505,11],[126515,0],[126516,11],[126520,0],[126521,11],[126522,0],[126523,11],[126524,0],[126530,11],[126531,0],[126535,11],[126536,0],[126537,11],[126538,0],[126539,11],[126540,0],[126541,11],[126544,0],[126545,11],[126547,0],[126548,11],[126549,0],[126551,11],[126552,0],[126553,11],[126554,0],[126555,11],[126556,0],[126557,11],[126558,0],[126559,11],[126560,0],[126561,11],[126563,0],[126564,11],[126565,0],[126567,11],[126571,0],[126572,11],[126579,0],[126580,11],[126584,0],[126585,11],[126589,0],[126590,11],[126591,0],[126592,11],[126602,0],[126603,11],[126620,0],[126625,11],[126628,0],[126629,11],[126634,0],[126635,11],[126652,0],[127280,11],[127306,0],[127312,11],[127338,0],[127344,11],[127370,0],[127462,18],[127488,0],[127995,14],[128e3,0],[130032,9],[130042,0],[917505,13],[917506,0],[917536,14],[917632,0],[917760,14],[918e3,0]];function ce(r,e){var t=ot(r,e);if(t.length==0)return[];for(var n=[],i=0;i=this.text.length?20:We(this.text[e])?Ee(this.text[e]+this.text[e+1]):Ee(this.text[e],this.options)},r.prototype.match=function(e,t,n,i){var a,l,o,s,u=(a=e==null?void 0:e.includes(this.lookbehind))!==null&&a!==void 0?a:!0;return u=u&&((l=t==null?void 0:t.includes(this.left))!==null&&l!==void 0?l:!0),u=u&&((o=n==null?void 0:n.includes(this.right))!==null&&o!==void 0?o:!0),u&&((s=i==null?void 0:i.includes(this.lookahead))!==null&&s!==void 0?s:!0)},r.prototype.propertyMatch=function(e,t,n,i){var a=this,l=function(o){return Be(o,a.options)};return this.match(e==null?void 0:e.map(l),t==null?void 0:t.map(l),n==null?void 0:n.map(l),i==null?void 0:i.map(l))},r}();function at(r,e){return!Array.from(r).map(function(t){return Ee(t,e)}).every(function(t){return t===3||t===1||t===2||t===4})}function ot(r,e){var t,n,i;if(r.length===0)return[];e&&!e.rules&&(e.rules=[]);var a=[],l,o=0,s=new it(r,e,o),u=0;do{if(l=o,o=_(o),s=s.next(o),s.match(null,[19],null,null)){a.push(l);continue}if(s.match(null,null,[20],null)){a.push(l);break}if(!s.match(null,[3],[1],null)){var c=[2,3,1];if(s.match(null,c,null,null)){a.push(l);continue}if(s.match(null,null,c,null)){a.push(l);continue}if(!s.match(null,[4],[4],null)){for(var f=[13,14,16];s.match(null,null,f,null);)t=(0,T.__read)([o,_(o)],2),l=t[0],o=t[1],s=s.ignoringRight(o);if(s.right===20){a.push(l);break}for(;s.match(null,null,null,f);)o=_(o),s=s.ignoringLookahead(o);var v=[11,15],p=[8,6];if(e!=null&&e.rules){var d=!1;try{for(var m=(n=void 0,(0,T.__values)(e.rules)),C=m.next();!C.done;C=m.next()){var x=C.value;if(d=x.match(s),d){x.breakIfMatch&&a.push(l);break}}}catch(I){n={error:I}}finally{try{C&&!C.done&&(i=m.return)&&i.call(m)}finally{if(n)throw n.error}}if(d)continue}if(!s.match(null,v,v,null)){var y=[10].concat(p);if(!s.match(null,v,y,v)&&!s.match(v,y,v,null)&&!s.match(null,[15],[6],null)&&!s.match(null,[15],[5],[15])&&!s.match([15],[5],[15],null)&&!s.match(null,[9],[9],null)&&!s.match(null,v,[9],null)&&!s.match(null,[9],v,null)){var S=[7].concat(p);if(!s.match([9],S,[9],null)&&!s.match(null,[9],S,[9])&&!s.match(null,[17],[17],null)){var w=[17,9].concat(v);if(!s.match(null,w,[12],null)&&!s.match(null,[12],[12],null)&&!s.match(null,[12],w,null)){if(s.right===18){if(u+=1,u%2==1)continue}else u=0;a.push(l)}}}}}}}while(l=r.length?r.length:We(r[I])?I+2:I+1}}function We(r){var e=r.charCodeAt(0);return e>=55296&&e<=56319}function Ee(r,e){if(e!=null&&e.propertyMapping){var t=e.propertyMapping(r);if(t)return Be(t,e)}var n=r.codePointAt(0);return Ie(n,0,Me.length-1)}function Be(r,e){var t,n,i=function(l){return l.toLowerCase()==r.toLowerCase()},a=(n=(t=e==null?void 0:e.customProperties)===null||t===void 0?void 0:t.findIndex(i))!==null&&n!==void 0?n:-1;return a!=-1?-a-1:nt.findIndex(i)}function Ie(r,e,t){if(t=l?Ie(r,n+1,t):i[1]}B();var Qe=12,ut=function(){function r(e,t){this.root=e,this.prefix=t}return r.prototype.children=function(){var e,t,n,i,a,l,o,s,u,c,f,v,p,d,m,C,x,y;return(0,T.__generator)(this,function(S){switch(S.label){case 0:if(e=this.root,e.type!=\\\"internal\\\")return[3,9];t=function(w){var _,I,Y,ne,Z,be,ke,ie,_e,ae,K,ve;return(0,T.__generator)(this,function(W){switch(W.label){case 0:if(_=e.children[w],!$(w))return[3,12];if(_.type!=\\\"internal\\\")return[3,9];I=_,Y=function(oe){var ue;return(0,T.__generator)(this,function(se){switch(se.label){case 0:return ue=n.prefix+w+oe,[4,{char:w+oe,traversal:function(){return new r(I.children[oe],ue)}}];case 1:return se.sent(),[2]}})},W.label=1;case 1:W.trys.push([1,6,7,8]),ne=(K=void 0,(0,T.__values)(I.values)),Z=ne.next(),W.label=2;case 2:return Z.done?[3,5]:(be=Z.value,[5,Y(be)]);case 3:W.sent(),W.label=4;case 4:return Z=ne.next(),[3,2];case 5:return[3,8];case 6:return ke=W.sent(),K={error:ke},[3,8];case 7:try{Z&&!Z.done&&(ve=ne.return)&&ve.call(ne)}finally{if(K)throw K.error}return[7];case 8:return[3,11];case 9:return ie=_.entries[0].key,w=w+ie[n.prefix.length+1],_e=n.prefix+w,[4,{char:w,traversal:function(){return new r(_,_e)}}];case 10:W.sent(),W.label=11;case 11:return[3,16];case 12:return X(w)?[2,\\\"continue\\\"]:[3,13];case 13:return w?[3,14]:[2,\\\"continue\\\"];case 14:return ae=n.prefix+w,[4,{char:w,traversal:function(){return new r(_,ae)}}];case 15:W.sent(),W.label=16;case 16:return[2]}})},n=this,S.label=1;case 1:S.trys.push([1,6,7,8]),i=(0,T.__values)(e.values),a=i.next(),S.label=2;case 2:return a.done?[3,5]:(l=a.value,[5,t(l)]);case 3:S.sent(),S.label=4;case 4:return a=i.next(),[3,2];case 5:return[3,8];case 6:return o=S.sent(),m={error:o},[3,8];case 7:try{a&&!a.done&&(C=i.return)&&C.call(i)}finally{if(m)throw m.error}return[7];case 8:return[2];case 9:s=this.prefix,u=e.entries.filter(function(w){return w.key!=s&&s.length=n)return o}}}catch(C){i={error:C}}finally{try{u&&!u.done&&(a=s.return)&&a.call(s)}finally{if(i)throw i.error}}else{l.enqueue(r);for(var p=void 0,d=function(){if(ft(p))if(p.type===\\\"leaf\\\")l.enqueueAll(p.entries);else{var C=p;l.enqueueAll(p.values.map(function(x){return C.children[x]}))}else if(o.push({text:p.content,p:p.weight/t}),o.length>=n)return{value:o}};p=l.dequeue();){var m=d();if(typeof m==\\\"object\\\")return m.value}}return o}function ft(r){return\\\"type\\\"in r}function ht(r){return r.normalize(\\\"NFD\\\").replace(/[\\\\u0300-\\\\u036f]/g,\\\"\\\").toLowerCase()}var pt=function(){function r(e){e=e||{},this._futureSuggestions=e.futureSuggestions?e.futureSuggestions.slice():[],e.punctuation&&(this.punctuation=e.punctuation)}return r.prototype.configure=function(e){return this.configuration={leftContextCodePoints:e.maxLeftContextCodePoints,rightContextCodePoints:e.maxRightContextCodePoints},this.configuration},r.prototype.predict=function(e,t,n){var i=function(l){var o,s,u=[],c=l.length;try{for(var f=(0,T.__values)(l),v=f.next();!v.done;v=f.next()){var p=v.value;u.push({sample:p,p:1})}}catch(d){o={error:d}}finally{try{v&&!v.done&&(s=f.return)&&s.call(f)}finally{if(o)throw o.error}}return u};if(n)return i(n);var a=this._futureSuggestions.shift();return a?i(a):[]},r}(),vt=pt,qe={};R(qe,{ClassicalDistanceCalculation:function(){return Ne},ContextTracker:function(){return ze},QUEUE_NODE_COMPARATOR:function(){return Se},SearchNode:function(){return Ge},SearchResult:function(){return He},SearchSpace:function(){return ee},TrackedContextState:function(){return Ce},TrackedContextSuggestion:function(){return gt},TrackedContextToken:function(){return re}});var Ne=function(){function r(e){if(this.diagonalWidth=2,this.inputSequence=[],this.matchSequence=[],e){var t=e.resolvedDistances.length;this.resolvedDistances=Array(t);for(var n=0;n2*n)&&(i.sparse=!0),i},r.prototype.getCostAt=function(e,t,n){if(n===void 0&&(n=this.diagonalWidth),e<0||t<0)return e==-1&&t>=-1?t+1:t==-1&&e>=-1?e+1:Number.MAX_VALUE;var i=this.getTrueIndex(e,t,n);return i.sparse?Number.MAX_VALUE:this.resolvedDistances[i.row][i.col]},r.prototype.getFinalCost=function(){for(var e=this,t=e.getHeuristicFinalCost();t>e.diagonalWidth;)e=e.increaseMaxDistance(),t=e.getHeuristicFinalCost();return t},r.prototype.getHeuristicFinalCost=function(){return this.getCostAt(this.inputSequence.length-1,this.matchSequence.length-1)},r.prototype.hasFinalCostWithin=function(e){var t=this,n=t.getHeuristicFinalCost(),i=this.diagonalWidth;do{if(n<=e)return!0;if(i=0&&f>=0){var v=1;if(i=[\\\"transpose-start\\\"],c!=e-1){var p=e-c-1;i=i.concat(Array(p).fill(\\\"transpose-delete\\\")),v+=p}else{var p=t-f-1;i=i.concat(Array(p).fill(\\\"transpose-insert\\\")),v+=p}i.push(\\\"transpose-end\\\"),this.getCostAt(c-1,f-1)!=n-v&&(i=null),a=[c-1,f-1]}return i||(s==n-1?(i=[\\\"substitute\\\"],a=[e-1,t-1]):l==n-1?(i=[\\\"insert\\\"],a=[e,t-1]):o==n-1?(i=[\\\"delete\\\"],a=[e-1,t]):(i=[\\\"match\\\"],a=[e-1,t-1])),a[0]>=0&&a[1]>=0?this.editPath(a[0],a[1]).concat(i):a[0]>-1?Array(a[0]+1).fill(\\\"delete\\\").concat(i):a[1]>-1?Array(a[1]+1).fill(\\\"insert\\\").concat(i):i},r.getTransposeParent=function(e,t,n){if(t<0||n<0||e.inputSequence[t].key==e.matchSequence[n].key)return[-1,-1];for(var i=-1,a=t-1;a>=0;a--)if(e.inputSequence[a].key==e.matchSequence[n].key){i=a;break}for(var l=-1,a=n-1;a>=0;a--)if(e.matchSequence[a].key==e.inputSequence[t].key){l=a;break}return[i,l]},r.initialCostAt=function(e,t,n,i,a){var l=e.inputSequence[t].key==e.matchSequence[n].key?0:1,o=e.getCostAt(t-1,n-1)+l,s=i||e.getCostAt(t,n-1)+1,u=a||e.getCostAt(t-1,n)+1,c=Number.MAX_VALUE;if(t>0&&n>0){var f=(0,T.__read)(r.getTransposeParent(e,t,n),2),v=f[0],p=f[1];c=e.getCostAt(v-1,p-1)+(t-v-1)+1+(n-p-1)}return Math.min(o,u,s,c)},r.prototype.getSubset=function(e,t){var n=new r(this);if(e>this.inputSequence.length||t>this.matchSequence.length)throw\\\"Invalid dimensions specified for trim operation\\\";n.inputSequence.splice(e),n.matchSequence.splice(t),n.resolvedDistances.splice(e);for(var i=this.getTrueIndex(e-1,t-1,this.diagonalWidth),a=i.col;a<=2*this.diagonalWidth;a++){var l=i.row-(a-i.col);if(l<0)break;if(a<0)n.resolvedDistances[l]=Array(2*n.diagonalWidth+1).fill(Number.MAX_VALUE);else{var o=2*this.diagonalWidth-a,s=n.resolvedDistances[l].splice(0,a+1),u=Array(o).fill(Number.MAX_VALUE);n.resolvedDistances[l]=s.concat(u)}}return n},r.forDiagonalOfAxis=function(e,t,n,i){for(var a=n-t=0){var c=u==0?o+2:Number.MAX_VALUE;s=r.initialCostAt(e,o,u,c,void 0);var f=s;if(u0&&l){var s=i+1;this.propagateUpdateFrom(e,t+1,n,s,a-1)}if(l&&o){var s=i+(e.inputSequence[t+1].key==e.matchSequence[n+1].key?0:1);this.propagateUpdateFrom(e,t+1,n+1,s,a);for(var u=-1,c=t+2;c0&&f>0){var v=i+(u-t-2)+1+(f-n-2);this.propagateUpdateFrom(e,u,f,v,e.diagonalWidth-1+f-u)}}},Object.defineProperty(r.prototype,\\\"mapKey\\\",{get:function(){var e=this.inputSequence.map(function(n){return n.key}).join(\\\"\\\"),t=this.matchSequence.map(function(n){return n.key}).join(\\\"\\\");return e+Q+t+Q+this.diagonalWidth},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,\\\"lastInputEntry\\\",{get:function(){return this.inputSequence[this.inputSequence.length-1]},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,\\\"lastMatchEntry\\\",{get:function(){return this.matchSequence[this.matchSequence.length-1]},enumerable:!1,configurable:!0}),r.computeDistance=function(e,t,n){n===void 0&&(n=1);var i=new r;n=n||1,i.diagonalWidth=n;for(var a=0;ae?t.p:e}).reduce(function(t,n){return t-Math.log(n)},0),this._inputCost},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,\\\"currentCost\\\",{get:function(){return ee.EDIT_DISTANCE_COST_SCALE*this.knownCost+this.inputSamplingCost},enumerable:!1,configurable:!0}),r.prototype.buildInsertionEdges=function(){var e,t,n=[];try{for(var i=(0,T.__values)(this.currentTraversal.children()),a=i.next();!a.done;a=i.next()){var l=a.value,o=l.traversal(),s={key:l.char,traversal:o},u=this.calculation.addMatchChar(s),c=new r(this);c.calculation=u,c.priorInput=this.priorInput,c.currentTraversal=o,n.push(c)}}catch(f){e={error:f}}finally{try{a&&!a.done&&(t=i.return)&&t.call(i)}finally{if(e)throw e.error}}return n},r.prototype.buildDeletionEdges=function(e){var t,n,i=[];try{for(var a=(0,T.__values)(e),l=a.next();!l.done;l=a.next()){var o=l.value;if(o.p0:!1},r.prototype.handleNextNode=function(){if(!this.hasNextMatchEntry())return{type:\\\"none\\\"};var e=this.selectionQueue.dequeue(),t=e.correctionQueue.dequeue(),n={type:\\\"intermediate\\\",cost:t.currentCost};if(this.processedEdgeSet[t.mapKey])return this.selectionQueue.enqueue(e),n;this.processedEdgeSet[t.mapKey]=!0;var i=!1;if(t.knownCost>2)return n;t.knownCost==2&&(i=!0);for(var a=0,l=0;l<=e.index;l++)a+=this.minInputCost[l];if(t.currentCost>a+2.5*r.EDIT_DISTANCE_COST_SCALE)return n;if(!i){var o=t.buildInsertionEdges();e.correctionQueue.enqueueAll(o)}if(e.index==this.tierOrdering.length-1)return this.completedPaths.push(t),this.selectionQueue.enqueue(e),{type:\\\"complete\\\",cost:t.currentCost,finalNode:t};var s=this.tierOrdering[e.index+1],u=s.index,c=[];i||(c=t.buildDeletionEdges(this.inputSequence[u-1]));var f=t.buildSubstitutionEdges(this.inputSequence[u-1]);return s.correctionQueue.enqueueAll(c.concat(f)),this.selectionQueue=new b(this.QUEUE_SPACE_COMPARATOR,this.tierOrdering),n},r.prototype.getBestMatches=function(e){var t,n,i,a,l,o,s,u,c,f,v,p,d,m,C,x;return(0,T.__generator)(this,function(y){switch(y.label){case 0:if(t=this,n={},e==0?i=1/0:e==null||Number.isNaN(e)?i=r.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL:i=e,a=function(){function S(w,_){this.largestIntervals=[0],this.loopStart=this.start=Date.now(),this.maxExecutionTime=w,this.maxTrueTime=_}return S.prototype.startLoop=function(){this.loopStart=Date.now()},S.prototype.markIteration=function(){var w=Date.now(),_=w-this.loopStart;this.executionTime+=_,_&&_>this.largestIntervals[0]&&(this.largestIntervals.length>2?this.largestIntervals[0]=_:this.largestIntervals.push(_),this.largestIntervals.sort(),this.updateOutliers())},S.prototype.updateOutliers=function(){this.largestIntervals.length>2&&this.largestIntervals[2]>=2*(this.largestIntervals[0]+this.largestIntervals[1])&&(this.executionTime-=this.largestIntervals[2],this.largestIntervals.pop())},S.prototype.shouldTimeout=function(){var w=Date.now();return w-this.start>this.maxTrueTime?!0:this.executionTime>this.maxExecutionTime},S.prototype.resetOutlierCheck=function(){this.largestIntervals=[]},S}(),l=function(){function S(){this.currentCost=Number.MIN_SAFE_INTEGER,this.entries=[]}return S.prototype.checkAndAdd=function(w){var _=null;w.currentCost>this.currentCost&&(_=this.tryFinalize(),this.currentCost=w.currentCost);var I=w.calculation.matchSequence.map(function(Y){return Y.key}).join(\\\"\\\");return t.returnedValues[I]||(t.returnedValues[I]=w),n[I]||(this.entries.push(new He(w)),n[I]=w),_},S.prototype.tryFinalize=function(){var w=null;return this.entries.length>0&&(w=this.entries,this.entries=[]),w},S}(),o=new l,s=new a(i*1.5,i),u=Object.values(this.returnedValues),!(u.length>0))return[3,6];c=new b(Se,u),s.startLoop(),y.label=1;case 1:return c.count>0?(f=c.dequeue(),f.isFullReplacement?[3,1]:(v=o.checkAndAdd(f),s.markIteration(),v?[4,v]:[3,3])):[3,4];case 2:y.sent(),y.label=3;case 3:return[3,1];case 4:return p=o.tryFinalize(),p?[4,p]:[3,6];case 5:y.sent(),y.label=6;case 6:s.resetOutlierCheck(),s.startLoop(),d=!1,y.label=7;case 7:m=void 0;do m=this.handleNextNode(),s.markIteration(),s.shouldTimeout()&&(d=!0);while(!d&&m.type==\\\"intermediate\\\");if(C=void 0,m.type==\\\"none\\\")return[3,10];if(m.type==\\\"complete\\\"){if(m.finalNode.isFullReplacement)return[3,10];C=o.checkAndAdd(m.finalNode)}return C?[4,C]:[3,9];case 8:y.sent(),y.label=9;case 9:if(!d&&this.hasNextMatchEntry())return[3,7];y.label=10;case 10:return x=o.tryFinalize(),x?[4,x]:[3,12];case 11:y.sent(),y.label=12;case 12:return[2,null]}})},r.EDIT_DISTANCE_COST_SCALE=5,r.MIN_KEYSTROKE_PROBABILITY=1e-4,r.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL=33,r}(),dt=function(){function r(){}return r.isWhitespace=function(e){var 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},r.isBackspace=function(e){return e.insert==\\\"\\\"&&e.deleteLeft>0&&!e.deleteRight},r.isEmpty=function(e){return e.insert==\\\"\\\"&&e.deleteLeft==0&&!e.deleteRight},r}(),te=dt;function Xe(r,e){for(var t=[],n=0;n0&&e.transformDistributions.forEach(function(n){return t.searchSpace[0].addInput(n)})},r.prototype.pushWhitespaceToTail=function(e){e===void 0&&(e=null);var t=new re;t.transformDistributions=e?[e]:[],t.raw=null,this.tokens.push(t)},r.prototype.replaceTailForBackspace=function(e,t){this.tokens.pop();var n=Xe(e,t).map(function(a){return[{sample:a,p:1}]}),i=new re;i.raw=e,i.transformDistributions=n,this.pushTail(i)},r.prototype.updateTail=function(e,t){var n=this.tail;t=t||(t===\\\"\\\"?\\\"\\\":n.raw),e&&e.length>0&&(n.transformDistributions.push(e),this.searchSpace&&this.searchSpace.forEach(function(i){return i.addInput(e)})),n.raw=t},r.prototype.toRawTokenization=function(){var e,t,n=[];try{for(var i=(0,T.__values)(this.tokens),a=i.next();!a.done;a=i.next()){var l=a.value;l.currentText!==null&&n.push(l.currentText)}}catch(o){e={error:o}}finally{try{a&&!a.done&&(t=i.return)&&t.call(i)}finally{if(e)throw e.error}}return n},r}(),yt=function(){function r(e){e===void 0&&(e=r.DEFAULT_ARRAY_SIZE),this.currentHead=0,this.currentTail=0,this.circle=Array(e)}return Object.defineProperty(r.prototype,\\\"count\\\",{get:function(){var e=this.currentHead-this.currentTail;return e<0&&(e=e+this.circle.length),e},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,\\\"maxCount\\\",{get:function(){return this.circle.length},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,\\\"oldest\\\",{get:function(){if(this.count!=0)return this.item(0)},enumerable:!1,configurable:!0}),Object.defineProperty(r.prototype,\\\"newest\\\",{get:function(){if(this.count!=0)return this.item(this.count-1)},enumerable:!1,configurable:!0}),r.prototype.enqueue=function(e){var t=null,n=(this.currentHead+1)%this.maxCount;return n==this.currentTail&&(t=this.circle[this.currentTail],this.currentTail=(this.currentTail+1)%this.maxCount),this.circle[this.currentHead]=e,this.currentHead=n,t},r.prototype.dequeue=function(){if(this.currentTail==this.currentHead)return null;var e=this.circle[this.currentTail];return this.currentTail=(this.currentTail+1)%this.maxCount,e},r.prototype.popNewest=function(){if(this.currentTail==this.currentHead)return null;var e=this.circle[this.currentHead];return this.currentHead=(this.currentHead-1+this.maxCount)%this.maxCount,e},r.prototype.item=function(e){if(e>=this.count)throw\\\"Invalid array index\\\";var t=(this.currentTail+e)%this.maxCount;return this.circle[t]},r.DEFAULT_ARRAY_SIZE=5,r}(),ze=function(r){(0,T.__extends)(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.attemptMatchContext=function(t,n,i){var a=n.toRawTokenization(),l=Ne.computeDistance(a.map(function(I){return{key:I}}),t.map(function(I){return{key:I}}),1),o=l.editPath(),s=!1,u=!1;if(o.length>1){if(o[0]==\\\"insert\\\"&&!(o[1]==\\\"substitute\\\"&&o.length==2)||o[0].indexOf(\\\"transpose\\\")>=0)return null;o[0]==\\\"delete\\\"&&(s=!0)}var c=o.length-1,f=!1;if(o[c]==\\\"delete\\\"||o[0].indexOf(\\\"transpose\\\")>=0||(o[c]==\\\"insert\\\"?u=!0:c>0&&o[c-1]==\\\"insert\\\"&&o[c]==\\\"substitute\\\"&&(u=!0,f=!0),c>0&&o[c-1]==\\\"delete\\\"&&o[c]==\\\"substitute\\\"))return null;for(var v=1;v1)if(s&&p.popHead(),u){var S=t[t.length-1],w=new re;w.raw=S,C||!m?(p.pushWhitespaceToTail(i!=null?i:[]),w.transformDistributions=[]):(p.pushWhitespaceToTail(),w.transformDistributions=i?[i]:[]),p.pushTail(w)}else x?p.replaceTailForBackspace(y,m.id):p.updateTail(m?i:null,y);else if(o[c]==\\\"insert\\\"){var _=new re;_.raw=t[0],_.transformDistributions=[i],p.pushTail(_)}else x?p.replaceTailForBackspace(y,m.id):p.updateTail(m?i:null,y);return p},e.modelContextState=function(t,n,i){var a=t.map(function(s){var u=new re;return u.raw=s,u.raw?u.transformDistributions=Xe(u.raw).map(function(c){return[{sample:c,p:1}]}):u.transformDistributions=[],u}),l=new Ce(i);for(a.length>0&&l.pushTail(a.splice(0,1)[0]);a.length>0;)l.pushWhitespaceToTail(),l.pushTail(a.splice(0,1)[0]);if(l.tokens.length==0){var o=new re;o.raw=\\\"\\\",l.pushTail(o)}return l},e.prototype.analyzeState=function(t,n,i){if(!t.traverseFromRoot)throw\\\"This lexical model does not provide adequate data for correction algorithms and context reuse\\\";var a=z(t.wordbreaker||ce,n);if(a.left.length>0)for(var l=this.count-1;l>=0;l--){var o=this.item(l),s=o.taggedContext;if(s&&i&&i.length>0){var u=H(i[0].sample,s);if(u.left!=n.left)continue}else if((s==null?void 0:s.left)!=n.left)continue;var c=e.attemptMatchContext(a.left,this.item(l),i);if(c)return this.newest!=c&&this.newest!=o&&this.enqueue(o),c.taggedContext=n,c!=this.item(l)&&this.enqueue(c),c}var f=e.modelContextState(a.left,i,t);return f.taggedContext=n,this.enqueue(f),f},e.prototype.clearCache=function(){for(;this.count>0;)this.dequeue()},e}(yt),mt=function(){function r(e,t){this.SUGGESTION_ID_SEED=0,this.testMode=!1,this.lexicalModel=e,e.traverseFromRoot&&(this.contextTracker=new ze),this.punctuation=r.determinePunctuationFromModel(e),this.testMode=!!t}return r.prototype.predictFromCorrections=function(e,t){var n,i,a=[],l=function(f){var v=o.lexicalModel.predict(f.sample,t),p=v.map(function(d){var m=f.sample,C=f.p;m.id!==void 0&&(d.sample.transformId=m.id);var x={sample:d.sample,p:d.p*C};return x},o);a=a.concat(p)},o=this;try{for(var s=(0,T.__values)(e),u=s.next();!u.done;u=s.next()){var c=u.value;l(c)}}catch(f){n={error:f}}finally{try{u&&!u.done&&(i=s.return)&&i.call(s)}finally{if(n)throw n.error}}return a},r.prototype.predict=function(e,t){var n,i,a,l,o=[],s=this.lexicalModel,u=this.punctuation;e instanceof Array?e.length==0&&e.push({sample:{insert:\\\"\\\",deleteLeft:0},p:1}):e=[{sample:e,p:1}];var c=e.sort(function(O,j){return j.p-O.p})[0].sample,f=te.isWhitespace(c),v=te.isBackspace(c),p=H(c,t),d=this.wordbreak(p),m=null,C=[],x,y=null;if(this.contextTracker){var w=this.contextTracker.analyzeState(this.lexicalModel,t,null);y=this.contextTracker.analyzeState(this.lexicalModel,p,te.isEmpty(c)?null:e);var _=y.searchSpace[0],I=0,Y=y.tokens,ne=Y.length,Z=Y.length-w.tokens.length;ne==0||Z>0?(I=0,te.isWhitespace(c)&&(x=c,t=p,w=y)):Z<0?I=this.wordbreak(p).kmwLength()+c.deleteLeft:I=this.wordbreak(t).kmwLength();var be=Y[Y.length-1],ke=be.transformDistributions.length<=1,ie=void 0,_e=this.testMode?0:ee.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL;try{for(var ae=(0,T.__values)(_.getBestMatches(_e)),K=ae.next();!K.done;K=ae.next()){var ve=K.value,S=ve.map(function(j){var fe=j.matchString,Te;j.inputSequence.length>0?Te=j.inputSequence[j.inputSequence.length-1].sample:Te=c;var Tt={insert:fe,deleteLeft:I,id:c.id},$e=j.totalCost;return ke&&($e*=r.SINGLE_CHAR_KEY_PROB_EXPONENT),{sample:Tt,p:Math.exp(-$e)}},this),W=this.predictFromCorrections(S,t);W.length>0&&ie===void 0&&(ie=-Math.log(S[0].p)),C=C.concat(W);var oe=ve[0].totalCost;if(oe>=ie+8)break;if(C.length>=r.MAX_SUGGESTIONS){if(oe>=ie+4)break;if(C.sort(function(j,fe){return fe.p-j.p}),C[r.MAX_SUGGESTIONS-1].p>Math.exp(-oe))break}}}catch(O){n={error:O}}finally{try{K&&!K.done&&(i=ae.return)&&i.call(ae)}finally{if(n)throw n.error}}}else{var S=void 0;f?(S=[{sample:c,p:1}],x=c):S=e.map(function(O){var j=O.sample;return te.isWhitespace(j)&&!f||te.isBackspace(j)&&!v?null:O}),S=S.filter(function(O){return!!O}),C=this.predictFromCorrections(S,t)}var ue={},se=null;s.languageUsesCasing&&(se=this.detectCurrentCasing(p));var bt=this.wordbreak(t);try{for(var xe=(0,T.__values)(C),de=xe.next();!de.done;de=xe.next()){var V=de.value,ge=V.sample.displayAs,Re=ge==d;if(this.lexicalModel.languageUsesCasing&&(Re=Re||ge==this.lexicalModel.applyCasing(\\\"lower\\\",d)),Re)if(m)m.p&&V.p&&(m.p+=V.p);else{var Fe=V.sample.transform,De={insert:d,deleteLeft:Fe.deleteLeft,deleteRight:Fe.deleteRight,id:Fe.id},kt=h(De,V.p);m=this.toAnnotatedSuggestion(kt,\\\"keep\\\",U.noQuotes),m.matchesModel=!0,m.transformId=V.sample.transformId}else{se&&se!=\\\"lower\\\"&&(this.applySuggestionCasing(V.sample,bt,se),ge=V.sample.displayAs);var Ze=ue[ge];Ze?Ze.p+=V.p:ue[ge]=V}}}catch(O){a={error:O}}finally{try{de&&!de.done&&(l=xe.return)&&l.call(xe)}finally{if(a)throw a.error}}if(!m&&d!=\\\"\\\"){var De=(0,T.__assign)({},c),Je=h(De,1);Je.displayAs=d,m=this.toAnnotatedSuggestion(Je,\\\"keep\\\"),m.matchesModel=!1}for(var _t in ue){var xt=ue[_t];o.push(xt)}o=o.sort(function(O,j){return j.p-O.p});var ye=o.splice(0,r.MAX_SUGGESTIONS).map(function(O){return O.sample.p&&(O.sample[\\\"lexical-p\\\"]=O.sample.p,O.sample[\\\"correction-p\\\"]=O.p/O.sample.p,O.sample.p=O.p),O.sample});m&&(ye=[m].concat(ye));var je=this;return ye.forEach(function(O){if(!t.right)O.transform.insert+=u.insertAfterWord;else{var j=je.tokenize(t);j&&j.caretSplitsToken&&(O.transform.insert+=u.insertAfterWord)}if(x){var fe=le(x,O.transform);fe.id=O.transformId;var Te=O;Te.transform=fe}O.id=je.SUGGESTION_ID_SEED,je.SUGGESTION_ID_SEED++}),y&&(y.tail.replacements=ye.map(function(O){return{suggestion:O,tokenWidth:1}})),ye},r.prototype.applySuggestionCasing=function(e,t,n){var i=t.kmwLength()-e.transform.deleteLeft;i>0&&(e.transform.deleteLeft+=i,e.transform.insert=t.kmwSubstr(0,i)+e.transform.insert),e.transform.insert=this.lexicalModel.applyCasing(n,e.transform.insert),e.displayAs=this.lexicalModel.applyCasing(n,e.displayAs)},r.prototype.toAnnotatedSuggestion=function(e,t,n){n===void 0&&(n=U.default);var i=U,a=i.noQuotes;return(t==\\\"keep\\\"||t==\\\"revert\\\")&&(a=i.useQuotes),{transform:e.transform,transformId:e.transformId,displayAs:i.apply(n,e.displayAs,this.punctuation,a),tag:t,p:e.p}},r.determinePunctuationFromModel=function(e){var t=St;if(!e.punctuation)return t;var n=e.punctuation,i=n.insertAfterWord;i!==\\\"\\\"&&!i&&(i=t.insertAfterWord);var a=n.quotesForKeepSuggestion;a||(a=t.quotesForKeepSuggestion);var l=n.isRTL;return{insertAfterWord:i,quotesForKeepSuggestion:a,isRTL:l}},r.prototype.acceptSuggestion=function(e,t,n){var i=e.transform,a=t.left.kmwSubstr(-i.deleteLeft,i.deleteLeft),l=i.insert.kmwLength(),o={insert:a,deleteLeft:l},s=t;n&&(o=le(o,n),s=H(n,s));var u,c=this.tokenize(s);c?(c.left.length>0?u=c.left[c.left.length-1]:u=\\\"\\\",u+=c.caretSplitsToken?c.right[0]:\\\"\\\"):u=this.wordbreak(s);var f=h(o);f.displayAs=u;var v=this.toAnnotatedSuggestion(f,\\\"revert\\\");if(e.transformId!=null&&(v.transformId=-e.transformId),e.id!=null?v.id=-e.id:(v.id=-this.SUGGESTION_ID_SEED,this.SUGGESTION_ID_SEED++),this.contextTracker){var p=this.contextTracker.newest;p||(p=this.contextTracker.analyzeState(this.lexicalModel,t)),p.tail.activeReplacementId=e.id;var d=H(e.transform,t);this.contextTracker.analyzeState(this.lexicalModel,d)}return v},r.prototype.applyReversion=function(e,t){var n=this,i=function(){var u=H(e.transform,t),c=n.predict({insert:\\\"\\\",deleteLeft:0},u);return c.forEach(function(f){f.transformId=-e.transformId}),c};if(!this.contextTracker)return i();for(var a=!1,l=this.contextTracker.count-1;l>=0;l--){var o=this.contextTracker.item(l);if(o.tail.activeReplacementId==-e.id){a=!0;break}}if(!a)return i();for(;this.contextTracker.newest.tail.activeReplacementId!=-e.id;)this.contextTracker.popNewest();this.contextTracker.newest.tail.revert();var s=this.contextTracker.newest.tail.replacements.map(function(u){return u.suggestion});return s.forEach(function(u){u.transformId=-e.transformId}),s},r.prototype.wordbreak=function(e){var t=this.lexicalModel;if(t.wordbreaker||!t.wordbreak){var n=t.wordbreaker||ce;return Ae(n,e)}else return t.wordbreak(e)},r.prototype.tokenize=function(e){var t=this.lexicalModel;return t.wordbreaker?z(t.wordbreaker,e):null},r.prototype.resetContext=function(e){this.contextTracker&&(this.contextTracker.clearCache(),this.contextTracker.analyzeState(this.lexicalModel,e,null))},r.prototype.detectCurrentCasing=function(e){var t,n=this.lexicalModel,i=this.wordbreak(e);if(!n.languageUsesCasing)throw\\\"Invalid attempt to detect casing: languageUsesCasing is set to false\\\";if(!n.applyCasing)throw\\\"Invalid LMLayer state: languageUsesCasing is set to true, but no applyCasing function exists\\\";return e.casingForm==\\\"upper\\\"||e.casingForm==\\\"initial\\\"?e.casingForm:n.applyCasing(\\\"lower\\\",i)==i?\\\"lower\\\":n.applyCasing(\\\"upper\\\",i)==i?i.kmwLength()>1?\\\"upper\\\":\\\"initial\\\":n.applyCasing(\\\"initial\\\",i)==i?\\\"initial\\\":(t=e.casingForm)!==null&&t!==void 0?t:null},r.MAX_SUGGESTIONS=12,r.SINGLE_CHAR_KEY_PROB_EXPONENT=16,r}(),wt=mt,St={quotesForKeepSuggestion:{open:\\\"\\\\u201C\\\",close:\\\"\\\\u201D\\\"},insertAfterWord:\\\" \\\"};B();var Ct=function(){function r(e){e===void 0&&(e={importScripts:null,postMessage:null}),this._testMode=!1,this._postMessage=e.postMessage||postMessage,this._importScripts=e.importScripts||importScripts,this.setupConfigState()}return r.prototype.error=function(e,t){this.cast(\\\"error\\\",{log:e,error:t&&t.stack?t.stack:void 0})},r.prototype.onMessage=function(e){var t=e.data.message;if(!t)throw new Error(\\\"Missing required 'message' property: \\\".concat(e.data));var n=e.data;if(n.message==\\\"load\\\"){var i=n,a=!1;if(this._currentModelSource&&i.source.type==this._currentModelSource.type&&(i.source.type==\\\"file\\\"&&i.source.file==this._currentModelSource.file||i.source.type==\\\"raw\\\"&&i.source.code==this._currentModelSource.code)&&(a=!0),a){typeof console!=\\\"undefined\\\"&&console.warn(\\\"Duplicate model load message detected - squashing!\\\");return}else this._currentModelSource=i.source}else n.message==\\\"unload\\\"&&(this._currentModelSource=null);this.state.handleMessage(n)},r.prototype.cast=function(e,t){var n=this._postMessage;n((0,T.__assign)({message:e},t))},r.prototype.loadModel=function(e){try{var 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=g);var n=this.transitionToReadyState(e);t.wordbreaksAfterSuggestions===void 0&&(t.wordbreaksAfterSuggestions=n.punctuation.insertAfterWord!=\\\"\\\"),this.cast(\\\"ready\\\",{configuration:t})}catch(i){this.error(\\\"loadModel failed!\\\",i)}},r.prototype.loadModelFile=function(e){try{this._importScripts(e)}catch(t){this.error(\\\"Error occurred when attempting to load dictionary\\\",t)}},r.prototype.unloadModel=function(){this.transitionToLoadingState()},r.prototype.setupConfigState=function(){var e=this;this.state={name:\\\"unconfigured\\\",handleMessage:function(t){if(t.message!==\\\"config\\\")throw new Error(\\\"invalid message; expected 'config' but got \\\".concat(t.message));e._platformCapabilities=t.capabilities,e._testMode=!!t.testMode,e.transitionToLoadingState()}}},r.prototype.transitionToLoadingState=function(){var e=this;this.state={name:\\\"modelless\\\",handleMessage:function(t){if(t.message!==\\\"load\\\")throw new Error(\\\"invalid message; expected 'load' but got \\\".concat(t.message));if(t.source.type==\\\"file\\\")e.loadModelFile(t.source.file);else{var n=t.source.code,i=new Function(\\\"LMLayerWorker\\\",\\\"models\\\",\\\"correction\\\",\\\"wordBreakers\\\",n);i(e,G,qe,Pe)}}}},r.prototype.transitionToReadyState=function(e){var t=this,n=new wt(e,this._testMode);return this.state={name:\\\"ready\\\",handleMessage:function(i){switch(i.message){case\\\"predict\\\":var a=i.transform,f=i.context,c=n.predict(a,f);t.cast(\\\"suggestions\\\",{token:i.token,suggestions:c});break;case\\\"wordbreak\\\":var l=Ae(e.wordbreaker||ce,i.context);t.cast(\\\"currentword\\\",{token:i.token,word:l});break;case\\\"unload\\\":t.unloadModel();break;case\\\"accept\\\":var o=i.suggestion,f=i.context,s=i.postTransform,u=n.acceptSuggestion(o,f,s);t.cast(\\\"postaccept\\\",{token:i.token,reversion:u});break;case\\\"revert\\\":var u=i.reversion,f=i.context,c=n.applyReversion(u,f);t.cast(\\\"postrevert\\\",{token:i.token,suggestions:c});break;case\\\"reset-context\\\":var f=i.context;n.resetContext(f);break;default:throw new Error(\\\"invalid message; expected one of {'predict', 'wordbreak', 'accept', 'revert', 'reset-context', 'unload'} but got \\\".concat(i.message))}},compositor:n},n},r.install=function(e){var t=new r({postMessage:e.postMessage,importScripts:e.importScripts.bind(e)});return e.onmessage=t.onMessage.bind(t),t.self=e,e.LMLayerWorker=t,e.models=G,e.correction=qe,e.wordBreakers=Pe,t},r}(),Ye=Ct;typeof self!=\\\"undefined\\\"&&\\\"postMessage\\\"in self&&\\\"importScripts\\\"in self?Ye.install(self):window.LMLayerWorker=Ye}();\\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 { Transcription } from \"@keymanapp/keyboard-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\n/// \r\n\r\nimport ContextWindow from \"./contextWindow.js\";\r\nimport LanguageProcessor from \"./prediction/languageProcessor.js\";\r\nimport type ModelSpec from \"./prediction/modelSpec.js\";\r\nimport { globalObject, DeviceSpec } from \"@keymanapp/web-utils\";\r\n\r\nimport {\r\n type Alternate,\r\n Codes,\r\n isEmptyTransform,\r\n type Keyboard,\r\n KeyboardInterface,\r\n KeyboardProcessor,\r\n type KeyEvent,\r\n Mock,\r\n type OutputTarget,\r\n type ProcessorInitOptions,\r\n RuleBehavior,\r\n SystemStoreIDs,\r\n type TextTransform\r\n} from \"@keymanapp/keyboard-processor\";\r\nimport { TranscriptionCache } from \"../transcriptionCache.js\";\r\n\r\nexport default 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 // Restores full context, including deadkeys in their exact pre-keystroke state.\r\n outputTarget.restoreTo(transcription.preInput);\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.isKnownOSKModifierKey(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}", + "import EventEmitter from \"eventemitter3\";\r\nimport type LanguageProcessor from \"./languageProcessor.js\";\r\nimport { type ReadySuggestions, type InvalidateSourceEnum, StateChangeEnum, StateChangeHandler } from './languageProcessor.js';\r\nimport { type KeyboardProcessor, type OutputTarget } from \"@keymanapp/keyboard-processor\";\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 private 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 private recentAccept: boolean = false;\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: LanguageProcessor;\r\n private kbdProcessor: KeyboardProcessor;\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 /**\r\n * Handler for post-processing once a suggestion has been applied: calls\r\n * into the active keyboard's `begin postKeystroke` entry point.\r\n *\r\n * Called after the suggestion is applied but _before_ new predictions are\r\n * requested based on the resulting context.\r\n */\r\n private readonly postApplicationHandler: () => void;\r\n\r\n public constructor(langProcessor: LanguageProcessor, kbdProcessor: KeyboardProcessor) {\r\n super();\r\n\r\n this.langProcessor = langProcessor;\r\n this.kbdProcessor = kbdProcessor;\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, () => kbdProcessor.layerId);\r\n }\r\n }\r\n\r\n this.suggestionReverter = (reversion) => {\r\n if(validSuggestionState()) {\r\n langProcessor.applyReversion(reversion, this.currentTarget);\r\n }\r\n }\r\n\r\n // As it's called synchronously via event-callback during `this.suggestionApplier`,\r\n // `this.currentTarget` is guaranteed to remain unchanged.\r\n this.postApplicationHandler = () => {\r\n // Tell the keyboard that the current layer has not changed\r\n kbdProcessor.newLayerStore.set('');\r\n kbdProcessor.oldLayerStore.set('');\r\n // Call the keyboard's entry point.\r\n kbdProcessor.processPostKeystroke(kbdProcessor.contextDevice, this.currentTarget)\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(kbdProcessor, this.currentTarget, true);\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 this.langProcessor.addListener('suggestionapplied', this.postApplicationHandler);\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\r\n this.langProcessor.removeListener('suggestionapplied', this.postApplicationHandler);\r\n this.clearSuggestions();\r\n }\r\n\r\n public get currentSuggestions(): Suggestion[] {\r\n let suggestions = [];\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\r\n if(this.activateKeep() && this.keepSuggestion && 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.recentAccept = false;\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 this.recentAccept = true;\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 //let keyman = com.keyman.singleton;\r\n\r\n if(!this.recentAccept && this.selected) {\r\n this.accept(this.selected);\r\n // returnObj.shouldSwallow = true;\r\n } else if(this.recentAccept && source == 'space') {\r\n this.recentAccept = false;\r\n // // If the model doesn't insert wordbreaks, don't swallow the space. If it does,\r\n // // we consider that insertion to be the results of the first post-accept space.\r\n // returnObj.shouldSwallow = !!keyman.core.languageProcessor.wordbreaksAfterSuggestions; // 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}*/): boolean => {\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.recentAccept = false;\r\n // Otherwise, did we just accept something before the revert signal was received?\r\n } else if(this.recentAccept) {\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\r\n if(!this.swallowPrediction || source == 'context') {\r\n this.recentAccept = false;\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.recentAccept && !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\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\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.recentAccept = false;\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.kbdProcessor.layerId);\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}", + "class DomEventTracking {\r\n Pelem: EventTarget;\r\n Peventname: string;\r\n Phandler: (Object) => boolean;\r\n PuseCapture?: boolean\r\n\r\n constructor(Pelem: EventTarget, Peventname: string, Phandler: (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: (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: (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, { EventNames, EventListener } from \"eventemitter3\";\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);\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);\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) => boolean\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]: ((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 this.events[event].push(func);\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 '@keymanapp/keyboard-processor';\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 Keyboard, KeyboardKeymanGlobal, ProcessorInitOptions } from \"@keymanapp/keyboard-processor\";\r\nimport { DOMKeyboardLoader as KeyboardLoader } from \"@keymanapp/keyboard-processor/dom-keyboard-loader\";\r\nimport { InputProcessor, PredictionContext } from \"@keymanapp/input-processor\";\r\nimport { OSKView } from \"keyman/engine/osk\";\r\nimport { KeyboardRequisitioner, ModelCache, ModelSpec, toUnprefixedKeyboardId as unprefixed } from \"keyman/engine/package-cache\";\r\n\r\nimport { EngineConfiguration, InitOptionSpec } from \"./engineConfiguration.js\";\r\nimport KeyboardInterface from \"./keyboardInterface.js\";\r\nimport { ContextManagerBase } from \"./contextManagerBase.js\";\r\nimport { KeyEventHandler } from 'keyman/engine/events';\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/package-cache/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 if(typeof(window['KeymanWeb_BaseLayout']) !== 'undefined') {\r\n return window['KeymanWeb_BaseLayout'];\r\n } else {\r\n return 'us';\r\n }\r\n}\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: KeyEventHandler = (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 }\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 this.refreshModel();\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 // 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 if(this.osk) {\r\n this.osk.setNeedsLayout();\r\n this.osk.activeKeyboard = kbd;\r\n this.osk.present();\r\n }\r\n\r\n // Needed to ensure the correct layer is displayed.\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 this.core.resetContext(this.contextManager.activeTarget);\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 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-processor.\r\n this.core.resetContext(target);\r\n },\r\n predictionContext: new PredictionContext(this.core.languageProcessor, this.core.keyboardProcessor),\r\n keyboardCache: this.keyboardRequisitioner.cache\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 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 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 KeyEvent,\r\n KeyboardProperties,\r\n ManagedPromise,\r\n type MinimalCodesInterface,\r\n type MutableSystemStore,\r\n type SystemStoreMutationHandler\r\n} from '@keymanapp/keyboard-processor';\r\nimport { createUnselectableElement, getAbsoluteX, getAbsoluteY, StylesheetManager } from 'keyman/engine/dom-utils';\r\nimport { EventListener, EventNames, KeyEventHandler, KeyEventSourceInterface, LegacyEventEmitter } from 'keyman/engine/events';\r\n\r\nimport Configuration from '../config/viewConfiguration.js';\r\nimport Activator, { StaticActivator } from './activator.js';\r\nimport TouchEventPromiseMap from './touchEventPromiseMap.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: {});\r\n 'helpclick'(obj: {});\r\n 'resizemove'(obj: {});\r\n 'show'(obj: {});\r\n 'hide'(obj: {\r\n HiddenByUser: boolean\r\n });\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\r\n private _boxBaseMouseDown: (e: MouseEvent) => boolean;\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 * 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\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\r\n this._bannerController = new BannerController(this.bannerView, this.hostDevice, this.config.predictionContextManager);\r\n\r\n this.keyboardView = null;\r\n\r\n this.setBaseMouseEventListeners();\r\n if(this.hostDevice.touchable) {\r\n this.setBaseTouchEventListeners();\r\n }\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 /**\r\n * Function _VKbdMouseEnter\r\n * Scope Private\r\n * @param {Object} e event\r\n * Description Activate the KMW UI when mouse enters the OSK element hierarchy\r\n */\r\n private _VKbdMouseEnter: (e: MouseEvent) => void;\r\n\r\n /**\r\n * Function _VKbdMouseLeave\r\n * Scope Private\r\n * @param {Object} e event\r\n * Description Cancel activation of KMW UI when mouse leaves the OSK element hierarchy\r\n */\r\n private _VKbdMouseLeave: (e: MouseEvent) => void;\r\n\r\n private setBaseMouseEventListeners() {\r\n this._Box.onmouseenter = this._VKbdMouseEnter = (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 = this._VKbdMouseLeave = (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 refreshLayout(pending?: boolean): void {\r\n if(!this.keyboardView) {\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 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\r\n // Ensure that the layer's spacebar is properly captioned.\r\n this.vkbd.showLanguage();\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\r\n // Do not erase / 'shutdown' the banner-controller; we simply re-use its elements.\r\n if(this.vkbd) {\r\n this.vkbd.shutdown();\r\n }\r\n this.keyboardView = null;\r\n this.needsLayout = true;\r\n\r\n // Instantly resets the OSK container, erasing / delinking the previously-loaded keyboard.\r\n this._Box.innerHTML = '';\r\n\r\n // Since we cleared all inner HTML, that means we cleared the stylesheets, too.\r\n this.uiStyleSheetManager.unlinkAll();\r\n this.kbdStyleSheetManager.unlinkAll();\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 // Any event-cancelers would go here, after the innerHTML reset.\r\n\r\n // Add header element to OSK only for desktop browsers\r\n if(this.headerView) {\r\n this._Box.appendChild(this.headerView.element);\r\n }\r\n\r\n // Add suggestion banner bar to OSK\r\n this._Box.appendChild(this.banner.element);\r\n\r\n this.bannerController?.configureForKeyboard(this.keyboardData?.keyboard, this.keyboardData?.metadata);\r\n\r\n let kbdView: KeyboardView = this.keyboardView = this._GenerateKeyboardView(this.keyboardData?.keyboard, this.keyboardData?.metadata);\r\n this._Box.appendChild(kbdView.element);\r\n kbdView.postInsert();\r\n\r\n // Add footer element to OSK only for desktop browsers\r\n if(this.footerView) {\r\n this._Box.appendChild(this.footerView.element);\r\n }\r\n // END: construction of the actual internal layout for the overall OSK\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 if(this.vkbd) {\r\n this.vkbd.shutdown();\r\n }\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 });\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.layers[newValue] && !this.vkbd?.layerLocked) {\r\n this.vkbd.layerId = newValue;\r\n // Ensure that the layer's spacebar is properly captioned.\r\n this.vkbd.showLanguage();\r\n }\r\n\r\n // Ensure the keyboard view is modeling the correct state. (Correct layer, etc.)\r\n this.keyboardView.updateState(); // will also need the stateKeys.\r\n // We need to recalc the font size here because the layer did not have\r\n // calculated dimensions available before it was visible\r\n this.refreshLayout();\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 if(this.keyboardView instanceof VisualKeyboard) {\r\n this.keyboardView.showLanguage();\r\n }\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();\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 // 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 { 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 }\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 { Keyboard, KeyboardProperties } from '@keymanapp/keyboard-processor';\r\nimport { type PredictionContext } from '@keymanapp/input-processor';\r\n\r\nimport {\r\n GestureRecognizer,\r\n GestureRecognizerConfiguration,\r\n GestureSource,\r\n InputSample,\r\n PaddedZoneSource\r\n} from '@keymanapp/gesture-recognizer';\r\n\r\nimport { BANNER_GESTURE_SET } from './bannerGestureSet.js';\r\n\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();\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 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 null;\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", + "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 { 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}", + "/**\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 const tDelta = sample.t - this.lastSample.t;\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 const tDelta = subsetStats.followingSample.t - subsetStats.lastSample.t;\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 get summaryObject() {\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 { 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\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\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\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 /**\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 maintainTouchpointsWithIds(identifiers: number[]) {\r\n const identifiersToMaintain = identifiers.map((internal_id) => this.identifierMap[internal_id]);\r\n this._activeTouchpoints\r\n .filter((source) => !identifiersToMaintain.includes(source.rawIdentifier))\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 if(this.identifierMap[key] == id) {\r\n delete this.identifierMap[key];\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 EventEmitter from \"eventemitter3\";\r\nimport { InputSample } from \"./inputSample.js\";\r\nimport { CumulativePathStats } from \"./cumulativePathStats.js\";\r\nimport { Mutable } from \"../mutable.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}", + "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}\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 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 }\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\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 * 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\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 { InputSample } from \"./inputSample.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, GestureDebugPath } from \"./gestureDebugPath.js\";\r\nimport { GestureSource } from \"./gestureSource.js\";\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 // identifier is not included b/c it's only needed during live processing.\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 GestureDebugSource extends GestureSource> {\r\n // Assertion: must always contain an index 0 - the base recognizer config.\r\n protected recognizerConfigStack: Nonoptional>[];\r\n\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\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 const path = this.path as GestureDebugPath;\r\n let jsonClone: SerializedGestureSource = {\r\n isFromTouch: this.isFromTouch,\r\n path: path.toJSON()\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}", + "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\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 this.emit('pointstart', touchpoint);\r\n }\r\n\r\n protected onInputMove(identifier: number, sample: InputSample, target: EventTarget) {\r\n const activePoint = this.getTouchpointWithId(identifier);\r\n if(!activePoint) {\r\n return;\r\n }\r\n\r\n activePoint.update(sample);\r\n }\r\n\r\n protected onInputMoveCancel(identifier: number, sample: InputSample, target: EventTarget) {\r\n const touchpoint = this.getTouchpointWithId(identifier);\r\n if(!touchpoint) {\r\n return;\r\n }\r\n\r\n touchpoint.update(sample);\r\n touchpoint.path.terminate(true);\r\n }\r\n\r\n protected onInputEnd(identifier: number, target: EventTarget) {\r\n const touchpoint = this.getTouchpointWithId(identifier);\r\n if(!touchpoint) {\r\n return;\r\n }\r\n\r\n const lastEntry = touchpoint.path.stats.lastSample;\r\n const sample = this.buildSampleFor(lastEntry.clientX, lastEntry.clientY, target, lastEntry.t, touchpoint);\r\n\r\n /* While an 'end' event immediately follows a 'move' if it occurred simultaneously,\r\n * this is decidedly _not_ the case if the touchpoint was held for a while without\r\n * moving, even at the point of its release.\r\n *\r\n * We'll never need to worry about the touchpoint moving here, and thus we don't\r\n * need to worry about `currentHoveredItem` changing. We're only concerned with\r\n * recording the _timing_ of the touchpoint's release.\r\n */\r\n if(sample.t != lastEntry.t) {\r\n touchpoint.update(sample);\r\n }\r\n\r\n this.getTouchpointWithId(identifier)?.path.terminate(false);\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 { InputSample } from \"./headless/inputSample.js\";\r\nimport { Nonoptional } from \"./nonoptional.js\";\r\nimport { ZoneBoundaryChecker } from \"./configuration/zoneBoundaryChecker.js\";\r\n\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 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 private get activeIdentifier(): number {\r\n return 0;\r\n }\r\n\r\n // public static forPredictiveBanner(banner: SuggestionBanner, handlerRoot: SuggestionManager) {\r\n // const config: GestureRecognizerConfiguration = {\r\n // targetRoot: banner.getDiv(),\r\n // // document.body is the event root b/c we need to track the mouse if it leaves\r\n // // the VisualKeyboard's hierarchy.\r\n // eventRoot: document.body,\r\n // };\r\n\r\n // return new MouseEventEngine(config);\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, identifier: number) {\r\n // WILL be null for newly-starting `GestureSource`s / contact points.\r\n const source = this.getTouchpointWithId(identifier);\r\n return this.buildSampleFor(event.clientX, event.clientY, event.target, performance.now(), source);\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 identifier = this.activeIdentifier;\r\n const sample = this.buildSampleFromEvent(event, identifier);\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 this.onInputStart(identifier, sample, event.target, false);\r\n }\r\n\r\n onMouseMove(event: MouseEvent) {\r\n if(!this.hasActiveTouchpoint(this.activeIdentifier)) {\r\n return;\r\n }\r\n\r\n const sample = this.buildSampleFromEvent(event, this.activeIdentifier);\r\n\r\n if(!event.buttons) {\r\n if(this.hasActiveClick) {\r\n this.hasActiveClick = false;\r\n this.onInputMoveCancel(this.activeIdentifier, sample, event.target);\r\n }\r\n return;\r\n }\r\n\r\n this.preventPropagation(event);\r\n const config = this.getConfigForId(this.activeIdentifier);\r\n\r\n if(!ZoneBoundaryChecker.inputMoveCancellationCheck(sample, config, this.disabledSafeBounds)) {\r\n this.onInputMove(this.activeIdentifier, sample, event.target);\r\n } else {\r\n this.onInputMoveCancel(this.activeIdentifier, sample, event.target);\r\n }\r\n }\r\n\r\n onMouseEnd(event: MouseEvent) {\r\n if(!this.hasActiveTouchpoint(this.activeIdentifier)) {\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(this.activeIdentifier, event.target);\r\n }\r\n}", + "import { GestureRecognizerConfiguration } from \"./configuration/gestureRecognizerConfiguration.js\";\r\nimport { InputEventEngine } from \"./inputEventEngine.js\";\r\nimport { InputSample } from \"./headless/inputSample.js\";\r\nimport { Nonoptional } from \"./nonoptional.js\";\r\nimport { ZoneBoundaryChecker } from \"./configuration/zoneBoundaryChecker.js\";\r\nimport { GestureSource } from \"./headless/gestureSource.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 private safeBoundMaskMap: {[id: number]: number} = {};\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 // public static forPredictiveBanner(banner: SuggestionBanner, handlerRoot: SuggestionManager) {\r\n // const config: GestureRecognizerConfiguration = {\r\n // targetRoot: banner.getDiv(),\r\n // // document.body is the event root b/c we need to track the mouse if it leaves\r\n // // the VisualKeyboard's hierarchy.\r\n // eventRoot: banner.getDiv(),\r\n // };\r\n\r\n // return new TouchEventEngine(config);\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 if(this.getTouchpointWithId(Number.parseInt(key, 10)) == source) {\r\n delete this.safeBoundMaskMap[key];\r\n }\r\n }\r\n }\r\n\r\n private buildSampleFromTouch(touch: Touch, timestamp: number) {\r\n // WILL be null for newly-starting `GestureSource`s / contact points.\r\n const source = this.getTouchpointWithId(touch.identifier);\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 // Maintain all touches in the `.touches` array that are NOT marked as `.changedTouches` (and therefore, new)\r\n this.maintainTouchpointsWithIds(allTouches\r\n .filter((touch1) => newTouches.findIndex(touch2 => touch1.identifier == touch2.identifier) == -1)\r\n .map((touch) => touch.identifier)\r\n );\r\n\r\n // Ensure the same timestamp is used for all touches being updated.\r\n const timestamp = performance.now();\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 sample = this.buildSampleFromTouch(touch, timestamp);\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[touch.identifier] = 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 continue;\r\n }\r\n\r\n this.onInputStart(touch.identifier, sample, event.target, true);\r\n }\r\n }\r\n\r\n onTouchMove(event: TouchEvent) {\r\n let propagationActive = true;\r\n // Ensure the same timestamp is used for all touches being updated.\r\n const timestamp = performance.now();\r\n\r\n this.maintainTouchpointsWithIds(touchListToArray(event.touches)\r\n .map((touch) => touch.identifier)\r\n );\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\r\n if(!this.hasActiveTouchpoint(touch.identifier)) {\r\n continue;\r\n }\r\n\r\n if(propagationActive) {\r\n this.preventPropagation(event);\r\n propagationActive = false;\r\n }\r\n\r\n const config = this.getConfigForId(touch.identifier);\r\n\r\n const sample = this.buildSampleFromTouch(touch, timestamp);\r\n\r\n if(!ZoneBoundaryChecker.inputMoveCancellationCheck(sample, config, this.safeBoundMaskMap[touch.identifier])) {\r\n this.onInputMove(touch.identifier, sample, touch.target);\r\n } else {\r\n this.onInputMoveCancel(touch.identifier, sample, touch.target);\r\n }\r\n }\r\n }\r\n\r\n onTouchEnd(event: TouchEvent) {\r\n let propagationActive = true;\r\n\r\n // Only lists touch contact points that have been lifted; touchmove is raised separately if any movement occurred.\r\n for(let i=0; i < event.changedTouches.length; i++) {\r\n const touch = event.changedTouches.item(i);\r\n\r\n if(!this.hasActiveTouchpoint(touch.identifier)) {\r\n continue;\r\n }\r\n\r\n if(propagationActive) {\r\n this.preventPropagation(event);\r\n propagationActive = false;\r\n }\r\n\r\n this.onInputEnd(touch.identifier, event.target);\r\n }\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 { timedPromise } from \"@keymanapp/web-utils\";\r\nimport { InputSample } from \"./inputSample.js\";\r\nimport { GestureDebugPath } from \"./gestureDebugPath.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 public constructor(gestureModelDefinitions: GestureModelDefs, inputEngines?: InputEngineBase[]) {\r\n super();\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 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 this.emit('inputstart', touchpoint);\r\n\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 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 /**\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}", + "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\nimport { GestureSequence } from \"./index.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\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) => {\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 if(this.pendingMatchSetup) {\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 await this.pendingMatchSetup;\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 };\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 pendingMatchGesture = new ManagedPromise();\r\n this.pendingMatchSetup = pendingMatchGesture.corePromise;\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 this.pendingMatchSetup = null;\r\n pendingMatchGesture.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 // 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, that don't combine with\r\n * already-active ones. This could be the first stage in a sequence or a followup to a prior stage.\r\n */\r\n let newMatchers = gestureModelSet.map((model) => new GestureMatcher(model, unmatchedSource || priorMatcher));\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) {\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 { 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\nimport { GesturePath } from \"../../gesturePath.js\";\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 { SampleCoordReplacement } from \"../specs/contactModel.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].resetOnResolve) {\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 // (`resetOnResolve` 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 let baseContact = srcContact;\r\n\r\n if(srcContact instanceof GestureSourceSubview) {\r\n srcContact.disconnect(); // prevent further updates from mangling tracked path info.\r\n baseContact = srcContact.baseSource;\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);\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);\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) {\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 // 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 if(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 }\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) {\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 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 this.finalize(false, 'path');\r\n }\r\n }\r\n\r\n contactModel.update();\r\n // Now that we've done the initial-state check, we can check for instantly-matching path models.\r\n\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) => matcher.update());\r\n }\r\n}", + "import EventEmitter from \"eventemitter3\";\r\n\r\nimport { GestureModelDefs, getGestureModel, getGestureModelSet } from \"../specs/gestureModelDefs.js\";\r\nimport { GestureSource, GestureSourceSubview } from \"../../gestureSource.js\";\r\nimport { GestureMatcher, MatchResult, PredecessorMatch } 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 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) {\r\n const { matcher, result } = selection;\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 matchReport = new GestureStageReport(selection);\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\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 * 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 { 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);\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}", + "import { type KeyElement } from '../../../keyElement.js';\r\nimport VisualKeyboard from '../../../visualKeyboard.js';\r\n\r\nimport { ActiveKey, ActiveKeyBase, ActiveSubKey, KeyDistribution, KeyEvent } from '@keymanapp/keyboard-processor';\r\nimport { ConfigChangeClosure, CumulativePathStats, GestureRecognizerConfiguration, GestureSequence, GestureSource, GestureSourceSubview, InputSample, RecognitionZoneSource } from '@keymanapp/gesture-recognizer';\r\nimport { GestureHandler } from '../gestureHandler.js';\r\nimport { distributionFromDistanceMaps } from '@keymanapp/input-processor';\r\nimport { GestureParams } from '../specsForLayout.js';\r\nimport { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js';\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] == 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: (typeof OrderedFlickDirections[number])) => {\r\n return {\r\n spec: flickSet[dir] as ActiveSubKey,\r\n coord: FlickNameCoordMap.get(dir)\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 InputSample,\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 '@keymanapp/keyboard-processor';\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 longpress: {\r\n /**\r\n * Allows enabling or disabling the longpress up-flick shortcut for keyboards that do not\r\n * include any defined flick gestures.\r\n *\r\n * Will be ignored (in favor of `false`) for keyboards that do have defined flicks.\r\n */\r\n permitsFlick: (item?: Item) => boolean,\r\n\r\n /**\r\n * The minimum _net_ distance traveled before a longpress flick-shortcut will trigger.\r\n */\r\n flickDist: 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 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 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 * Indicates whether roaming-touch oriented behaviors should be enabled.\r\n *\r\n * Note that run-time adjustments to this property after initialization will\r\n * not take affect, unlike the other properties of the overall parameter object.\r\n */\r\n roamingEnabled?: boolean;\r\n}\r\n\r\nexport const DEFAULT_GESTURE_PARAMS: GestureParams = {\r\n longpress: {\r\n permitsFlick: () => true,\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 flickDist: 5,\r\n waitLength: 500,\r\n noiseTolerance: 10\r\n },\r\n multitap: {\r\n waitLength: 500,\r\n holdLength: 500\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 const modifierKeyIds = ['K_SHIFT', 'K_ALT', 'K_CTRL', 'K_NUMERALS', 'K_SYMBOLS', 'K_CURRENCIES'];\r\n for(const modKeyId of modifierKeyIds) {\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\nlet dummy2: LayoutGestureSupportFlags = dummy;\r\n\r\n/**\r\n * Defines the set of gestures appropriate for use with the specified Keyman keyboard.\r\n * @param layerGroup The active keyboard's layer group\r\n * @param params A set of tweakable gesture parameters. It will be closure-captured\r\n * and referred to by reference; changes to its values will take\r\n * immediate effect during gesture processing.\r\n *\r\n * If params.roamingEnabled is unset, it will be initialized by this\r\n * method based upon layout properties.\r\n * @returns\r\n */\r\nexport function gestureSetForLayout(flags: LayoutGestureSupportFlags, params: 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 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 const _longpressModel: GestureModel = deepCopy(longpressModel(params, true, doRoaming));\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 withKeySpecFiltering(_longpressModel, 0),\r\n withKeySpecFiltering(multitapStartModel(params), 0),\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 withKeySpecFiltering(modipressMultitapStartModel(params), 0),\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: GestureParams): ContactModel {\r\n return {\r\n itemPriority: 1,\r\n pathModel: {\r\n evaluate: (path) => path.stats.netDistance > params.flick.startDist ? 'resolve' : null\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: GestureParams): 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 },\r\n pathResolutionAction: 'resolve',\r\n pathInheritance: 'full'\r\n }\r\n}\r\n\r\n\r\nexport function flickEndContactModel(params: GestureParams): 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 }\r\n },\r\n pathResolutionAction: 'resolve',\r\n pathInheritance: 'full'\r\n }\r\n}\r\n\r\nexport function longpressContactModel(params: GestureParams, 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 duration: 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 if(stats.netDistance > spec.flickDist) {\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 }\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 }\r\n }\r\n };\r\n}\r\n\r\nexport function simpleTapContactModel(params: GestureParams, 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 }\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 }\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: GestureParams): 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: GestureParams, allowShortcut: boolean, allowRoaming: boolean): GestureModel {\r\n const base: GestureModel = {\r\n id: 'longpress',\r\n resolutionPriority: 0,\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 }\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: GestureParams): 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: GestureParams): 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: GestureParams): 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: GestureParams): 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 resetOnResolve: 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: GestureParams): 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: GestureParams): 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 },\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: GestureParams): 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 resetOnResolve: 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: GestureParams): 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: GestureParams): 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 resetOnResolve: 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: GestureParams): 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 resetOnResolve: 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: GestureParams): 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 resetOnResolve: 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: GestureParams): 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: GestureParams): 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: GestureParams): 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 resetOnResolve: 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: GestureParams): 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: GestureParams): 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 resetOnResolve: 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 InputSample\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 '@keymanapp/input-processor';\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 } from '@keymanapp/keyboard-processor';\r\nimport { Banner } from './banner.js';\r\nimport EventEmitter from 'eventemitter3';\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\n\r\nconst TOUCHED_CLASS: string = 'kmw-suggest-touched';\r\nconst BANNER_CLASS: string = 'kmw-suggest-banner';\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 private fontFamily?: string;\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'), ds=div.style;\r\n div.className = \"kmw-suggest-option\";\r\n div.id = BannerSuggestion.BASE_ID + this.index;\r\n\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 = this.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.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 private hostDevice: DeviceSpec;\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 this.hostDevice = hostDevice;\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 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, target: HTMLElement) => {\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 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 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 if(suggestion && !this.scrollState.hasScrolled) {\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 '@keymanapp/input-processor';\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 '@keymanapp/keyboard-processor';\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 let banner: Banner;\r\n\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 = banner = 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 '@keymanapp/keyboard-processor';\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 EventEmitter from 'eventemitter3';\r\n\r\nimport {\r\n ActiveKey,\r\n ActiveLayout,\r\n ButtonClass,\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 LayoutKey,\r\n ActiveSubKey,\r\n timedPromise,\r\n ActiveKeyBase,\r\n isEmptyTransform,\r\n SystemStoreIDs\r\n} from '@keymanapp/keyboard-processor';\r\n\r\nimport { buildCorrectiveLayout, distributionFromDistanceMaps, keyTouchDistances } from '@keymanapp/input-processor';\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, getAbsoluteX, getAbsoluteY, StylesheetManager } from 'keyman/engine/dom-utils';\r\n\r\nimport { KeyEventHandler, KeyEventResultCallback } from 'keyman/engine/events';\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 from './keyboard-layout/oskLayer.js';\r\nimport OSKLayerGroup from './keyboard-layout/oskLayerGroup.js';\r\nimport OSKView from './views/oskView.js';\r\nimport { LengthStyle, ParsedLengthStyle } from './lengthStyle.js';\r\nimport { defaultFontSize, getFontSizeStyle } 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, { buildFlickScroller } 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 './index.js';\r\n\r\ninterface KeyRuleEffects {\r\n contextToken?: number,\r\n alteredText?: boolean\r\n};\r\n\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\r\ninterface BoundingRect {\r\n left: number,\r\n right: number,\r\n top: number,\r\n bottom: number\r\n};\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\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 readonly gestureParams: GestureParams = {\r\n ...DEFAULT_GESTURE_PARAMS,\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 private _layerId: string = \"default\";\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 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 // Used by embedded-mode's globe key\r\n menuEvent: KeyElement; // Used by embedded-mode.\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\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._layerId;\r\n }\r\n\r\n set layerId(value: string) {\r\n const changedLayer = value != this._layerId;\r\n if(!this.layerGroup.layers[value]) {\r\n throw new Error(`Keyboard ${this.layoutKeyboard.id} does not have a layer with id ${value}`);\r\n } else {\r\n this._layerId = value;\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) {\r\n this.updateState();\r\n this.refreshLayout();\r\n }\r\n }\r\n\r\n get currentLayer(): OSKLayer {\r\n return this.layerId ? this.layerGroup?.layers[this.layerId] : null;\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 OSK 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._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 rowCount = this.kbdLayout.layerMap['default'].row.length;\r\n\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 };\r\n\r\n this.gestureParams.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\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 const trackingEntry = 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\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\r\n const previewHost = this.highlightKey(key, true);\r\n if(previewHost) {\r\n this.gesturePreviewHost = previewHost;\r\n trackingEntry.previewHost = previewHost;\r\n sourceTrackingMap[source.identifier].key = key;\r\n }\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 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);\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 existingPreviewHost?.cancel();\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', () => this.emit('globekey', gestureKey, false));\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 } else if(gestureStage.matchedId.indexOf('longpress') > -1) {\r\n existingPreviewHost?.cancel();\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 existingPreviewHost?.cancel();\r\n this.gesturePreviewHost = null;\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 existingPreviewHost?.cancel();\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 existingPreviewHost?.cancel();\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 let cs = getComputedStyle(this.element);\r\n if (cs.border) {\r\n let borderWidth = new ParsedLengthStyle(cs.borderWidth).val;\r\n baseWidth -= borderWidth * 2;\r\n }\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 let cs = getComputedStyle(this.element);\r\n if (cs.border) {\r\n let borderHeight = new ParsedLengthStyle(cs.borderWidth).val;\r\n baseHeight -= borderHeight * 2;\r\n }\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 return ParsedLengthStyle.inPixels(this.layoutHeight.val - this.getVerticalLayerGroupPadding());\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\r\n /**\r\n * Returns the default properties for a key object, used to construct\r\n * both a base keyboard key and popup keys\r\n *\r\n * @return {Object} An object that contains default key properties\r\n */\r\n getDefaultKeyObject(): ActiveKey {\r\n const baseKeyObject: LayoutKey = {...ActiveKey.DEFAULT_KEY};\r\n ActiveKey.polyfill(baseKeyObject, this.layoutKeyboard, this.kbdLayout, this.layerId);\r\n return baseKeyObject as ActiveKey;\r\n };\r\n //#endregion\r\n\r\n //#region 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 // TODO: It'd be nice to optimize by keeping these off when unused, but the wiring\r\n // necessary would get in the way of modularization at the moment.\r\n // let keyman = com.keyman.singleton;\r\n // if (!keyman.core.languageProcessor.mayCorrect) {\r\n // return null;\r\n // }\r\n\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.layers[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'];\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 in this.stateKeys) {\r\n this.stateKeys[key] = stateKeys[key];\r\n }\r\n\r\n this._UpdateVKShiftStyle();\r\n }\r\n\r\n //#endregion\r\n\r\n /**\r\n * Indicate the current language and keyboard on the space bar\r\n **/\r\n showLanguage() {\r\n let activeStub = this.layoutKeyboardProperties;\r\n let displayName: string = activeStub?.displayName ?? '(System keyboard)';\r\n\r\n try {\r\n var t = this.spaceBar.key.label;\r\n let tParent = t.parentNode;\r\n if (typeof (tParent.className) == 'undefined' || tParent.className == '') {\r\n tParent.className = 'kmw-spacebar';\r\n } else if (tParent.className.indexOf('kmw-spacebar') == -1) {\r\n tParent.className += ' kmw-spacebar';\r\n }\r\n\r\n if (t.className != 'kmw-spacebar-caption') {\r\n t.className = 'kmw-spacebar-caption';\r\n }\r\n\r\n // It sounds redundant, but this dramatically cuts down on browser DOM processing;\r\n // but sometimes innerText is reported empty when it actually isn't, so set it\r\n // anyway in that case (Safari, iOS 14.4)\r\n if (t.innerText != displayName || displayName == '') {\r\n t.innerText = displayName;\r\n }\r\n\r\n this.spaceBar.key.refreshLayout(this);\r\n }\r\n catch (ex) { }\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)) return;\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(): number {\r\n if (!this.fontSize) {\r\n return 0;\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).val;\r\n } else {\r\n let emSizeStr = getComputedStyle(document.body).fontSize;\r\n let emSize = getFontSizeStyle(emSizeStr).val;\r\n\r\n var 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.val;\r\n } else {\r\n emScale = this.fontSize.val;\r\n }\r\n }\r\n return emSize * 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 var n, b = this.kbdDiv.childNodes[0].childNodes;\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 //let keyman = com.keyman.singleton;\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 let paddedHeight: number;\r\n if (this.height) {\r\n paddedHeight = this.computedAdjustedOskHeight(this.height);\r\n }\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 // 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 isInDOM = computedStyle.height != '' && computedStyle.height != 'auto';\r\n\r\n // Step 2: determine basic layout geometry, refresh things that might update.\r\n this.showLanguage(); // In case the spacebar-text mode setting has changed.\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 if (!this._computedWidth) {\r\n // For touch keyboards, the width _was_ specified on the layer group,\r\n // not the root element (`kbdDiv`).\r\n const groupStyle = getComputedStyle(this.kbdDiv.firstElementChild);\r\n this._computedWidth = parseInt(groupStyle.width, 10);\r\n }\r\n this._computedHeight = parseInt(computedStyle.height, 10);\r\n } else {\r\n // Cannot perform layout operations!\r\n return;\r\n }\r\n\r\n // Step 3: recalculate gesture parameter values\r\n // 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 this.gestureParams.longpress.flickDist = 0.25 * this.currentLayer.rowHeight;\r\n this.gestureParams.flick.startDist = 0.15 * 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 }\r\n\r\n // Step 4: perform layout operations.\r\n // Needs the refreshed layout info to work correctly.\r\n if(this.currentLayer) {\r\n this.currentLayer.refreshLayout(this, this._computedHeight - this.getVerticalLayerGroupPadding());\r\n }\r\n }\r\n\r\n private getVerticalLayerGroupPadding(): number {\r\n // For touch-based OSK layouts, kmwosk.css may include top & bottom padding on the layer-group element.\r\n const computedGroupStyle = getComputedStyle(this.layerGroup.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 return pt + pb;\r\n }\r\n\r\n /*private*/ computedAdjustedOskHeight(allottedHeight: number): number {\r\n if (!this.layerGroup) {\r\n return allottedHeight;\r\n }\r\n\r\n const layers = this.layerGroup.layers;\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.rows.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 // Do not do anything if a null stub\r\n if (activeStub == null) {\r\n return;\r\n }\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 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(() => this.refreshLayout());\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 });\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 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 keyCS = getComputedStyle(key);\r\n const parsedHeight = Number.parseInt(keyCS.height, 10);\r\n const parsedWidth = Number.parseInt(keyCS.width, 10);\r\n const previewHost = new GesturePreviewHost(key, this.device.formFactor == 'phone', parsedWidth, parsedHeight);\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}\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, DeviceSpec, LayoutKey } from '@keymanapp/keyboard-processor';\r\nimport { TouchLayout } from '@keymanapp/common-types';\r\nimport TouchLayoutFlick = TouchLayout.TouchLayoutFlick;\r\n\r\n// At present, we don't use @keymanapp/keyman. Just `keyman`. (Refer to /web/package.json.)\r\nimport { getAbsoluteX, getAbsoluteY } from 'keyman/engine/dom-utils';\r\n\r\nimport { getFontSizeStyle } from '../fontSizeUtils.js';\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\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 let specialCodePUA = 0XE000 + specialChars[oldText];\r\n\r\n return specialChars[oldText] ?\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 /**\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 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 vkbd\r\n * @param text\r\n * @param style specification for the desired base font size\r\n * @param override if true, don't use the font spec from the button, just use the passed in spec\r\n * @returns font size as a style string\r\n */\r\n getIdealFontSize(vkbd: VisualKeyboard, text: string, style: {height?: string, fontFamily?: string, fontSize: string}, override?: boolean): string {\r\n let buttonStyle: typeof style & {width?: string} = getComputedStyle(this.btn);\r\n let keyWidth = parseFloat(buttonStyle.width);\r\n let emScale = vkbd.getKeyEmFontSize();\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 const capFont = !this.label ? undefined: getComputedStyle(this.label).fontFamily;\r\n if(capFont) {\r\n buttonStyle = {\r\n fontFamily: capFont,\r\n fontSize: buttonStyle.fontSize,\r\n height: buttonStyle.height\r\n }\r\n }\r\n\r\n const originalSize = getFontSizeStyle(style.fontSize || '1em');\r\n\r\n // Not yet available; it'll be handled in a later layout pass.\r\n if(!override) {\r\n // When available, just use computedStyle instead.\r\n style = buttonStyle;\r\n }\r\n\r\n let fontSpec = getFontSizeStyle(style.fontSize || '1em');\r\n let metrics = getTextMetrics(text, emScale, 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, keyHeight: 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 if(style.height && style.height.indexOf('px') != -1) {\r\n keyHeight = Number.parseFloat(style.height.substring(0, style.height.indexOf('px')));\r\n }\r\n\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 - 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 //\r\n if(proportion < 1) {\r\n if(originalSize.absolute) {\r\n return proportion * fontSpec.val + 'px';\r\n } else {\r\n return proportion * originalSize.val + 'em';\r\n }\r\n } else {\r\n if(originalSize.absolute) {\r\n return fontSpec.val + 'px';\r\n } else {\r\n return originalSize.val + 'em';\r\n }\r\n }\r\n }\r\n\r\n getKeyWidth(vkbd: VisualKeyboard): number {\r\n let key = this.spec as ActiveKey;\r\n return key.proportionalWidth * vkbd.width;\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 let emScale = vkbd.getKeyEmFontSize();\r\n var width: number = getTextMetrics(keyText, emScale, styleSpec).width;\r\n if(width == 0 && keyText != '' && keyText != '\\xa0') {\r\n // Add the Unicode 'empty circle' as a base support for needy diacritics.\r\n\r\n // Disabled by mcdurdin 2020-10-19; dotted circle display is inconsistent on iOS/Safari\r\n // at least and doesn't combine with diacritic marks. For consistent display, it may be\r\n // necessary to build a custom font that does not depend on renderer choices for base\r\n // mark display -- e.g. create marks with custom base included, potentially even on PUA\r\n // code points and use those in rendering the OSK. See #3039 for more details.\r\n // keyText = '\\u25cc' + keyText;\r\n\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\r\n ts.fontSize = this.getIdealFontSize(vkbd, keyText, styleSpec);\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 refreshLayout(vkbd: VisualKeyboard) {\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 this.label.style.fontSize = this.getIdealFontSize(vkbd, keyCapText, this.btn.style);\r\n } else {\r\n // Remove any custom setting placed on it before recomputing its inherited style info.\r\n this.label.style.fontSize = '';\r\n const fontSize = this.getIdealFontSize(vkbd, this.label.textContent, this.btn.style);\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, \"important\");\r\n }\r\n }\r\n }\r\n}\r\n", + "import { ActiveSubKey } from '@keymanapp/keyboard-processor';\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, DeviceSpec } from '@keymanapp/keyboard-processor';\r\nimport { landscapeView } from 'keyman/engine/dom-utils';\r\n\r\nimport OSKKey, { 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\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(vkbd: VisualKeyboard) {\r\n let key = this.spec as ActiveKey;\r\n this.square.style.width = vkbd.layoutWidth.scaledBy(key.proportionalWidth).styleString;\r\n this.square.style.marginLeft = vkbd.layoutWidth.scaledBy(key.proportionalPad).styleString;\r\n this.btn.style.width = vkbd.usesFixedWidthScaling ? this.square.style.width : '100%';\r\n\r\n if(vkbd.usesFixedHeightScaling) {\r\n // Matches its row's height.\r\n this.square.style.height = vkbd.internalHeight.scaledBy(this.row.heightFraction).styleString;\r\n } else {\r\n this.square.style.height = '100%'; // use the full row height\r\n }\r\n\r\n super.refreshLayout(vkbd);\r\n\r\n const device = vkbd.device;\r\n const resizeLabels = (device.OS == DeviceSpec.OperatingSystem.iOS &&\r\n device.formFactor == DeviceSpec.FormFactor.Phone\r\n && landscapeView());\r\n\r\n // Rescale keycap labels on iPhone (iOS 7)\r\n if(resizeLabels && this.capLabel) {\r\n this.capLabel.style.fontSize = '6px';\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 '@keymanapp/keyboard-processor';\r\n\r\nimport OSKBaseKey from './oskBaseKey.js';\r\nimport { ParsedLengthStyle } from '../lengthStyle.js';\r\nimport VisualKeyboard from '../visualKeyboard.js';\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\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.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 public refreshLayout(vkbd: VisualKeyboard) {\r\n const rs = this.element.style;\r\n\r\n const rowHeight = vkbd.internalHeight.scaledBy(this.heightFraction);\r\n rs.maxHeight=rs.lineHeight=rs.height=rowHeight.styleString;\r\n\r\n // Only used for fixed-height scales at present.\r\n const padRatio = 0.15;\r\n\r\n const keyHeightBase = vkbd.usesFixedHeightScaling ? rowHeight : ParsedLengthStyle.forScalar(1);\r\n const padTop = keyHeightBase.scaledBy(padRatio / 2);\r\n const keyHeight = keyHeightBase.scaledBy(1 - padRatio);\r\n\r\n for(const key of this.keys) {\r\n const keySquare = key.btn.parentElement;\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 if(keyElement.key) {\r\n keyElement.key.refreshLayout(vkbd);\r\n }\r\n }\r\n }\r\n}", + "import { ActiveLayer, ActiveLayout } from '@keymanapp/keyboard-processor';\r\n\r\nimport OSKRow from './oskRow.js';\r\nimport OSKBaseKey from './oskBaseKey.js';\r\nimport VisualKeyboard from '../visualKeyboard.js';\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 = gDiv['layer'] = layer['id'];\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 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, '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 this.blinkLayer(layer);\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.layers[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;\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 const baseRect = this.element.getBoundingClientRect();\r\n\r\n let row: OSKRow = null;\r\n let bestMatchDistance = Number.MAX_VALUE;\r\n\r\n // Find the row that the touch-coordinate lies within.\r\n for(const r of layer.rows) {\r\n const rowRect = r.element.getBoundingClientRect();\r\n if(rowRect.top <= coord.clientY && coord.clientY < rowRect.bottom) {\r\n row = r;\r\n break;\r\n } else {\r\n const distance = rowRect.top > coord.clientY ? rowRect.top - coord.clientY : coord.clientY - rowRect.bottom;\r\n\r\n if(distance < bestMatchDistance) {\r\n bestMatchDistance = distance;\r\n row = r;\r\n }\r\n }\r\n }\r\n\r\n // Assertion: row no longer `null`.\r\n\r\n // Warning: am not 100% sure that what follows is actually fully correct.\r\n\r\n // Find minimum distance from any key\r\n let closestKeyIndex = 0;\r\n let dx: number;\r\n let dxMax = 24;\r\n let dxMin = 100000;\r\n\r\n const x = coord.clientX;\r\n\r\n for (let k = 0; k < row.keys.length; k++) {\r\n // Second-biggest, though documentation suggests this is probably right.\r\n const keySquare = row.keys[k].square as HTMLElement; // gets the .kmw-key-square containing a key\r\n const squareRect = keySquare.getBoundingClientRect();\r\n\r\n // Find the actual key element.\r\n let childNode = keySquare.firstChild ? keySquare.firstChild as HTMLElement : keySquare;\r\n\r\n if (childNode.className !== undefined\r\n && (childNode.className.indexOf('key-hidden') >= 0\r\n || childNode.className.indexOf('key-blank') >= 0)) {\r\n continue;\r\n }\r\n const x1 = squareRect.left;\r\n const x2 = squareRect.right;\r\n if (x >= x1 && x <= x2) {\r\n // Within the key square\r\n return childNode;\r\n }\r\n dx = x1 - x;\r\n if (dx >= 0 && dx < dxMin) {\r\n // To right of key\r\n closestKeyIndex = k; dxMin = dx;\r\n }\r\n dx = x - x2;\r\n if (dx >= 0 && dx < dxMin) {\r\n // To left of key\r\n closestKeyIndex = k; dxMin = dx;\r\n }\r\n }\r\n\r\n if (dxMin < 100000) {\r\n const t = row.keys[closestKeyIndex].square;\r\n const squareRect = t.getBoundingClientRect();\r\n\r\n const x1 = squareRect.left;\r\n const x2 = squareRect.right;\r\n\r\n // Limit extended touch area to the larger of 0.6 of key width and 24 px\r\n if (squareRect.width > 40) {\r\n dxMax = 0.6 * squareRect.width;\r\n }\r\n\r\n if (((x1 - x) >= 0 && (x1 - x) < dxMax) || ((x - x2) >= 0 && (x - x2) < dxMax)) {\r\n return t.firstChild;\r\n }\r\n }\r\n return null;\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\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 kc = key.key.label,\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 kts.top = 'auto';\r\n\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 if(orientation == 'bottom') {\r\n y += canvasHeight - xHeight;\r\n }\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 popupFS = previewFontScale * px;\r\n let scaleStyle = {\r\n fontFamily: kts.fontFamily,\r\n fontSize: popupFS + 'px',\r\n height: 1.6 * xHeight + 'px' // as opposed to the canvas height of 2.3 * xHeight.\r\n };\r\n\r\n kts.fontSize = key.key.getIdealFontSize(vkbd, key.key.keyText, scaleStyle, true);\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 \"@keymanapp/keyboard-processor\";\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 '@keymanapp/keyboard-processor';\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 // Create (temporarily) unique ID by prefixing 'popup-' to actual key ID\r\n return 'popup-'+this.layer+'-'+this.spec['id'];\r\n }\r\n\r\n construct(osk: VisualKeyboard, baseKey: KeyElement, topMargin: boolean): HTMLDivElement {\r\n let spec = this.spec;\r\n\r\n let kDiv=document.createElement('div');\r\n let tKey = osk.getDefaultKeyObject();\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 if(typeof spec['width'] != 'undefined') {\r\n ks.width=(spec['width']*baseKey.offsetWidth/100)+'px';\r\n } else {\r\n ks.width=baseKey.offsetWidth+'px';\r\n }\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, KeyEvent, ActiveSubKey, KeyDistribution, ActiveKeyBase } from '@keymanapp/keyboard-processor';\r\nimport { ConfigChangeClosure, GestureRecognizerConfiguration, GestureSequence, PaddedZoneSource, RecognitionZoneSource } from '@keymanapp/gesture-recognizer';\r\nimport { GestureHandler } from '../gestureHandler.js';\r\nimport { CorrectionLayout, CorrectionLayoutEntry, distributionFromDistanceMaps, keyTouchDistances } from '@keymanapp/input-processor';\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 var i;\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 var nKeys=subKeySpec.length,nRows,nCols;\r\n nRows=Math.min(Math.ceil(nKeys/9),2);\r\n nCols=Math.ceil(nKeys/nRows);\r\n\r\n this.menuWidth = (nCols*e.offsetWidth + nCols*SUBKEY_DEFAULT_MARGIN_LEFT);\r\n ss.width = this.menuWidth+'px';\r\n\r\n // Add nested button elements for each sub-key\r\n this.subkeys = [];\r\n for(i=0; i 1 && nRow > 0) {\r\n needsTopMargin = true;\r\n }\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 let keyGenerator = new OSKSubKey(subKeySpec[i], layer);\r\n let kDiv = keyGenerator.construct(vkbd, e, needsTopMargin);\r\n this.subkeys.push(kDiv.firstChild as KeyElement);\r\n\r\n elements.appendChild(kDiv);\r\n }\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, ManagedPromise } from '@keymanapp/keyboard-processor';\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 { DeviceSpec, KeyEvent, ActiveSubKey, ActiveKey, KeyDistribution, ActiveKeyBase } from '@keymanapp/keyboard-processor';\r\nimport { GestureSequence, GestureStageReport } from '@keymanapp/gesture-recognizer';\r\nimport { GestureHandler } from '../gestureHandler.js';\r\nimport { distributionFromDistanceMaps } from '@keymanapp/input-processor';\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) => (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 { ActiveKey, ActiveKeyBase } from \"@keymanapp/keyboard-processor\";\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: typeof OrderedFlickDirections[number]) => {\r\n const flickPreview = document.createElement('div');\r\n flickPreview.className = 'kmw-flick-preview kmw-key-text';\r\n flickPreview.textContent = flickSpec[dir].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);\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\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: Record> = {};\r\n\r\n // Used to\r\n public promiseForTouchpoint(id: number): ManagedPromise {\r\n if(!this.map[id]) {\r\n this.map[id] = new ManagedPromise();\r\n }\r\n\r\n return this.map[id]; // touchpoint identifiers are unique during a page's lifetime.\r\n }\r\n\r\n public maintainTouches(list: TouchList) {\r\n let keys = Object.keys(this.map);\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[endedKey] as ManagedPromise).resolve();\r\n delete this.map[endedKey];\r\n }\r\n }\r\n}", + "import EventEmitter from 'eventemitter3';\r\n\r\nimport { Keyboard } from '@keymanapp/keyboard-processor';\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 private readonly source: MouseEvent | TouchEvent;\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 if(source) {\r\n this.source = source;\r\n }\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['changedTouches']) {\r\n coordSource = e['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();\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);\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();\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 { Codes, DeviceSpec, ManagedPromise, Version } from '@keymanapp/keyboard-processor';\r\nimport { getAbsoluteX, getAbsoluteY, landscapeView } from 'keyman/engine/dom-utils';\r\nimport { EmitterListenerSpy, LegacyEventMap } 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 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\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 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 // 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?) {\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 var Lpos={};\r\n Lpos['x']=this._Box.offsetLeft;\r\n Lpos['y']=this._Box.offsetTop;\r\n Lpos['userLocated']=this.userPositioned;\r\n this.doShow(Lpos);\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 { Codes, DeviceSpec } from '@keymanapp/keyboard-processor';\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 { Codes, DeviceSpec } from '@keymanapp/keyboard-processor';\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 \"@keymanapp/keyboard-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 c = this.getCaret();\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 c = this.getCaret();\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, offset) {\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, end) {\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 let Lsel = this.doc.getSelection();\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, offset) {\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, end) {\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 let Lsel = this.root.ownerDocument.getSelection();\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 {Element|Event} 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: Event|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 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 } else if (Pelem['defaultView']) { // Covers Document.\r\n scopedClass = Pelem['defaultView'][className];\r\n } else if(Pelem['ownerDocument']) {\r\n scopedClass = (Pelem as Node).ownerDocument.defaultView[className];\r\n } else if(Pelem['target']) {\r\n var event = Pelem as Event;\r\n\r\n if(this.instanceof(event.target, 'Window')) {\r\n scopedClass = event.target[className];\r\n } else if(this.instanceof(event.target, 'Document')) {\r\n scopedClass = (event.target as Document).defaultView[className];\r\n } else if(this.instanceof(event.target, 'HTMLElement')) {\r\n scopedClass = (event.target as HTMLElement).ownerDocument.defaultView[className];\r\n }\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