mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-05 16:35:33 +00:00
531 lines
19 KiB
TypeScript
531 lines
19 KiB
TypeScript
/// <reference path="kmwstring.ts" /> // Includes KMW string extension declarations.
|
|
|
|
// KeymanWeb 10.0
|
|
// Copyright 2017 SIL International
|
|
|
|
/*****************************************/
|
|
/* */
|
|
/* Embedded application-specific code */
|
|
/* */
|
|
/*****************************************/
|
|
|
|
(function() {
|
|
// Declare KeymanWeb and related objects
|
|
var keymanweb=window['keyman'],osk=keymanweb['osk'],util=keymanweb['util'],device=util.device;
|
|
var kbdInterface=keymanweb['interface'];
|
|
|
|
// Allow definition of application name
|
|
keymanweb.options['app']='';
|
|
|
|
// Flag to control refreshing of a keyboard that is already loaded
|
|
keymanweb.mustReloadKeyboard = true;
|
|
|
|
// Skip full page initialization - skips native-mode only code
|
|
keymanweb.isEmbedded = true;
|
|
|
|
osk.popupDelay = 400; // Delay must be less than native touch-hold delay
|
|
|
|
// Set default device options
|
|
keymanweb.setDefaultDeviceOptions = function(opt) {
|
|
opt['attachType'] = 'manual';
|
|
device.app=opt['app'];
|
|
device.touchable=true;
|
|
device.formFactor='phone';
|
|
if(navigator && navigator.userAgent && navigator.userAgent.indexOf('iPad') >= 0) device.formFactor='tablet';
|
|
if(device.app.indexOf('Mobile') >= 0) device.formFactor='phone';
|
|
if(device.app.indexOf('Tablet') >= 0) device.formFactor='tablet';
|
|
device.browser='native';
|
|
};
|
|
|
|
// Get default style sheet path
|
|
keymanweb.getStyleSheetPath = function(ssName) {
|
|
return keymanweb.rootPath+ssName;
|
|
};
|
|
|
|
// Get KMEI, KMEA keyboard path (overrides default function, allows direct app control of paths)
|
|
keymanweb.getKeyboardPath = function(Lfilename, packageID) {
|
|
return Lfilename + "?v=" + (new Date()).getTime(); /*cache buster*/
|
|
};
|
|
|
|
/**
|
|
* Force reload of resource
|
|
*
|
|
* @param {string} s unmodified URL
|
|
* @return {string} modified URL
|
|
*/
|
|
util.unCached = function(s) {
|
|
var t=(new Date().getTime());
|
|
s = s + '?v=' + t;
|
|
return s;
|
|
};
|
|
|
|
util.wait = function() {
|
|
// Empty stub - this function should not be implemented or used within embedded code routes.
|
|
console.warn("util.wait() call attempted in embedded mode!"); // Sends log message to embedding app.
|
|
};
|
|
|
|
util.alert = function() {
|
|
// Empty stub - this function should not be implemented or used within embedded code routes.
|
|
console.warn("util.alert() call attempted in embedded mode!"); // Sends log message to embedding app.
|
|
};
|
|
|
|
// TODO: This needs to be discussed with Serkan - can possibly get context without any reference to Pelem??
|
|
|
|
/**
|
|
* Get (uncached) keyboard context for a specified range, relative to caret
|
|
*
|
|
* @param {number} n Number of characters to move back from caret
|
|
* @param {number} ln Number of characters to return
|
|
* @param {Object} Pelem Element to work with (must be currently focused element)
|
|
* @return {string} Context string
|
|
*
|
|
* Example [abcdef|ghi] as INPUT, with the caret position marked by |:
|
|
* KC(2,1,Pelem) == "e"
|
|
* KC(3,3,Pelem) == "def"
|
|
* KC(10,10,Pelem) == "abcdef" i.e. return as much as possible of the requested string
|
|
*/
|
|
keymanweb.KC_ = function(n, ln, Pelem)
|
|
{
|
|
var Ldv, Ldoc;
|
|
if(Pelem.body) Ldoc=Pelem; else Ldoc=Pelem.ownerDocument; // I1481 - use Ldoc to get the ownerDocument when no selection is found
|
|
|
|
if(Ldoc && (Ldv=Ldoc.defaultView) && Ldv.getSelection &&
|
|
(Ldoc.designMode.toLowerCase() == 'on' || Pelem.contentEditable == 'true' || Pelem.contentEditable == 'plaintext-only' || Pelem.contentEditable === '')) // && Pelem.tagName == 'HTML') && Pelem.tagName == 'HTML')
|
|
// I2457 - support contentEditable elements in mozilla, webkit
|
|
{
|
|
/* Mozilla midas html editor and editable elements */
|
|
var Lsel = Ldv.getSelection();
|
|
if(Lsel.focusNode.nodeType == 3)
|
|
{
|
|
if(Lsel.focusOffset > 2*n) // I3319 SMP extension
|
|
return Lsel.focusNode.substringData(Lsel.focusOffset - 2*n, 2*n)._kmwSubstr(-n)._kmwSubstr(0,ln); // I3319
|
|
else
|
|
return Lsel.focusNode.substringData(0, Lsel.focusOffset)._kmwSubstr(-n)._kmwSubstr(0,ln); // I3319
|
|
}
|
|
else
|
|
return "";
|
|
}
|
|
else if (Pelem.setSelectionRange)
|
|
{
|
|
/* Mozilla other controls */
|
|
var LselectionStart, LselectionEnd;
|
|
if(Pelem._KeymanWebSelectionStart)
|
|
{
|
|
LselectionStart = Pelem._KeymanWebSelectionStart;
|
|
LselectionEnd = Pelem._KeymanWebSelectionEnd;
|
|
//KeymanWeb._Debug('KeymanWeb.KC: _KeymanWebSelectionStart=TRUE LselectionStart='+LselectionStart+'; LselectionEnd='+LselectionEnd);
|
|
}
|
|
else
|
|
{
|
|
if(keymanweb._CachedSelectionStart === null || Pelem.selectionStart !== keymanweb._LastCachedSelection) // I3319, KMW-1
|
|
{
|
|
keymanweb._LastCachedSelection = Pelem.selectionStart; // KMW-1
|
|
keymanweb._CachedSelectionStart = Pelem.value._kmwCodeUnitToCodePoint(Pelem.selectionStart); // I3319
|
|
keymanweb._CachedSelectionEnd = Pelem.value._kmwCodeUnitToCodePoint(Pelem.selectionEnd); // I3319
|
|
}
|
|
LselectionStart = keymanweb._CachedSelectionStart; // I3319
|
|
LselectionEnd = keymanweb._CachedSelectionEnd; // I3319
|
|
}
|
|
if(LselectionStart < n)
|
|
{
|
|
// Looking for context before start of text buffer so return non-characters to pad result
|
|
var tempContext = Array(n-LselectionStart+1).join("\uFFFE") + Pelem.value._kmwSubstr(0,LselectionStart);
|
|
return tempContext._kmwSubstr(0,ln);
|
|
}
|
|
//dbg(n+' '+ln+' '+Pelem.value._kmwSubstring(LselectionStart-n,LselectionStart-n+ln));
|
|
return Pelem.value._kmwSubstring(LselectionStart-n,LselectionStart-n+ln); //I3319, KMW-1
|
|
}
|
|
|
|
else if(Ldoc && (Ldv=Ldoc.selection)) // build 77 - use elem.ownerDocument instead of document
|
|
// I1481 - use Ldoc to get the ownerDocument when no selection is found
|
|
{
|
|
/* IE */
|
|
var Lrange = Ldv.createRange();
|
|
//if (Lrange.parentElement() == Pelem) { // build 77 - ignore parent of selection
|
|
Lrange.moveStart('character',-2*n); //I3319
|
|
|
|
return Lrange.text._kmwSubstr(-n)._kmwSubstring(0,ln); //I3319
|
|
//}
|
|
}
|
|
|
|
return "";
|
|
};
|
|
|
|
/**
|
|
* Refresh element content after change of text (if required)
|
|
*
|
|
* @param {Object} Pelem input element
|
|
*/
|
|
keymanweb.refreshElementContent = function(Pelem)
|
|
{
|
|
if('ontextchange' in keymanweb) keymanweb['ontextchange'](Pelem);
|
|
};
|
|
|
|
/**
|
|
* Set target element text direction (LTR or RTL): not functional for KMEI, KMEA
|
|
*
|
|
* @param {Object} Ptarg Target element
|
|
*/
|
|
keymanweb._SetTargDir = function(Ptarg){};
|
|
|
|
/**
|
|
* Align input fields (should not be needed with KMEI, KMEA)
|
|
*
|
|
* @param {boolean} align align and make visible, else hide
|
|
*
|
|
**/
|
|
keymanweb.alignInputs = function(align) {};
|
|
|
|
/**
|
|
* Use rotation events to adjust OSK element positions and scaling if necessary
|
|
*/
|
|
keymanweb.handleRotationEvents = function() {};
|
|
|
|
/**
|
|
* Adjust the absolute height of each keyboard element after a rotation - modified for KMEI, 13/11/14
|
|
*
|
|
**/
|
|
osk.adjustHeights=function()
|
|
{
|
|
if(!osk._Box || !osk._Box.firstChild || !osk._Box.firstChild.firstChild || !osk._Box.firstChild.firstChild.childNodes)
|
|
return false;
|
|
|
|
var layers=osk._Box.firstChild.firstChild.childNodes,
|
|
nRows=layers[0].childNodes.length,
|
|
oskHeight=osk.getHeight(),
|
|
rowHeight=Math.floor(oskHeight/nRows),
|
|
nLayer,nRow,rs,keys,nKeys,nKey,key,ks,j,pad=4,fs=1.0;
|
|
|
|
if(device.OS == 'Android' && 'devicePixelRatio' in window)
|
|
rowHeight = rowHeight/window.devicePixelRatio;
|
|
|
|
oskHeight=nRows*rowHeight;
|
|
|
|
var b=osk._Box,bs=b.style;
|
|
bs.height=bs.maxHeight=(oskHeight+3)+'px';
|
|
b=b.firstChild.firstChild; bs=b.style;
|
|
bs.height=bs.maxHeight=(oskHeight+3)+'px';
|
|
if(device.formFactor == 'phone') fs = 0.65;
|
|
pad = Math.round(0.15*rowHeight);
|
|
|
|
bs.fontSize=fs+'em';
|
|
var resizeLabels=(device.OS == 'iOS' && device.formFactor == 'phone' && util.landscapeView());
|
|
|
|
for(nLayer=0;nLayer<layers.length; nLayer++)
|
|
{
|
|
layers[nLayer].style.height=(oskHeight+3)+'px';
|
|
for(nRow=0; nRow<nRows; nRow++)
|
|
{
|
|
rs=layers[nLayer].childNodes[nRow].style;
|
|
rs.bottom=(nRows-nRow-1)*rowHeight+'px';
|
|
rs.maxHeight=rs.height=rowHeight+'px';
|
|
keys=layers[nLayer].childNodes[nRow].childNodes;
|
|
nKeys=keys.length;
|
|
for(nKey=0;nKey<nKeys;nKey++)
|
|
{
|
|
key=keys[nKey];
|
|
// Must set the height of the text DIV, not the label (if any)
|
|
for(j=0;j<key.childNodes.length;j++)
|
|
if(osk.hasClass(key.childNodes[j],'kmw-key')) break;
|
|
ks=key.childNodes[j].style;
|
|
ks.bottom=rs.bottom;
|
|
ks.height=ks.minHeight=(rowHeight-pad)+'px';
|
|
|
|
// Rescale keycap labels on iPhone (iOS 7)
|
|
if(resizeLabels && (j > 0)) key.childNodes[0].style.fontSize='6px';
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Caret position always determined from the active (but hidden) element
|
|
*
|
|
* @return {boolean}
|
|
**/
|
|
keymanweb.isPositionSynthesized = function()
|
|
{
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* correctOSKTextSize handles rotation event -- currently rebuilds keyboard and adjusts font sizes
|
|
*/
|
|
keymanweb['correctOSKTextSize']=function() {
|
|
if(osk.adjustHeights()) {
|
|
osk._Load();
|
|
}
|
|
};
|
|
|
|
// Send the subkey array to iOS, with centre,top of base key position
|
|
/**
|
|
* Create a popup key array natively
|
|
*
|
|
* @param {Object} key base key element
|
|
*/
|
|
osk.touchHold = function(key)
|
|
{
|
|
if(key.subKeys && (typeof(window['oskCreatePopup']) == 'function'))
|
|
{
|
|
var xBase=util._GetAbsoluteX(key)-util._GetAbsoluteX(osk._DivVKbd)+key.offsetWidth/2,
|
|
yBase=util._GetAbsoluteY(key)-util._GetAbsoluteY(osk._DivVKbd);
|
|
|
|
if(device.formFactor == 'phone') osk.prependBaseKey(key);
|
|
osk.popupBaseKey = key; osk.popupPending=true;
|
|
window['oskCreatePopup'](key.subKeys,xBase,yBase,key.offsetWidth,key.offsetHeight);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function called by iOS when a device-implemented keyboard popup is displayed or hidden
|
|
*
|
|
* @param {boolean} isVisible
|
|
*
|
|
**/
|
|
keymanweb['popupVisible'] = function(isVisible)
|
|
{
|
|
osk.popupVisible = isVisible;
|
|
};
|
|
|
|
// Popup key highlighting (managed by device, dummy call)
|
|
osk.highlightSubKeys = function(k,x,y){};
|
|
|
|
// Create a keytip (dummy call - actual keytip handled by native code)
|
|
osk.createKeyTip = function()
|
|
{
|
|
if(device.formFactor == 'phone') osk.keytip = {key:null,state:false};
|
|
};
|
|
|
|
// Send the key details to KMEI or KMEA for showing or hiding the native-code keytip
|
|
osk.showKeyTip = function(key,on)
|
|
{
|
|
var tip = osk.keytip,
|
|
showPreview = window['oskCreateKeyPreview'],
|
|
clearPreview = window['oskClearKeyPreview'];
|
|
|
|
if(tip == null || (key == tip.key && on == tip.state)) return;
|
|
|
|
if(on && (typeof showPreview == 'function'))
|
|
{
|
|
var xBase=util._GetAbsoluteX(key)-util._GetAbsoluteX(osk._DivVKbd)+key.offsetWidth/2,
|
|
yBase=util._GetAbsoluteY(key)-util._GetAbsoluteY(osk._DivVKbd), kc;
|
|
|
|
// Find key text element
|
|
for(var i=0; i<key.childNodes.length; i++)
|
|
{
|
|
kc = key.childNodes[i];
|
|
if(osk.hasClass(kc,'kmw-key-text')) break;
|
|
}
|
|
|
|
if(key.className.indexOf('kmw-key-default') >= 0 && key.id.indexOf('K_SPACE') < 0)
|
|
showPreview(xBase,yBase,key.offsetWidth,key.offsetHeight,kc.innerHTML);
|
|
}
|
|
else if(!on && (typeof clearPreview == 'function'))
|
|
{
|
|
if(osk.touchCount == 0 || key == null) clearPreview();
|
|
}
|
|
tip.key = key; tip.state = on;
|
|
};
|
|
|
|
osk.menuEvent = null;
|
|
osk.optionKey = function(e,keyName,keyDown)
|
|
{
|
|
if(keyName.indexOf('K_LOPT') >= 0)
|
|
{
|
|
if(keyDown)
|
|
{
|
|
osk.menuEvent = e;
|
|
if('showKeyboardList' in keymanweb) keymanweb['showKeyboardList']();
|
|
}
|
|
else
|
|
{
|
|
if(osk.menuEvent) osk.highlightKey(osk.menuEvent,false);
|
|
if(typeof(window['menuKeyUp']) == 'function') window['menuKeyUp']();
|
|
osk.menuEvent = null;
|
|
}
|
|
}
|
|
else if(keyName.indexOf('K_ROPT') >= 0)
|
|
{
|
|
if(keyDown)
|
|
{
|
|
osk.highlightKey(e,false);
|
|
if('hideKeyboard' in keymanweb) keymanweb['hideKeyboard']();
|
|
}
|
|
}
|
|
};
|
|
|
|
// For KMEI and KMEA will always assume fonts are already installed
|
|
osk.waitForFonts = function(kfd,ofd){return true;};
|
|
|
|
|
|
/**
|
|
* Return position of language menu key to KeymanTouch
|
|
*
|
|
* @return {string} comma-separated x,y,w,h of language menu key
|
|
*
|
|
**/
|
|
keymanweb['touchMenuPos'] = function()
|
|
{
|
|
if(osk.lgKey == null) return '';
|
|
var key=osk.lgKey;
|
|
// A CSS change of kmd-key-square from position:fixed to position:static was needed
|
|
// for Android 4.3 to display the OSK correctly, but resulted in the position of
|
|
// the menu key not being returned correctly. The following line gets the
|
|
// key element, instead of the key-square element, fixes this. It should be
|
|
// removed again when the key-square elements are all removed as planned.
|
|
if(typeof key.firstChild != 'undefined' && key.firstChild != null && osk.hasClass(key.firstChild,'kmw-key')) key = key.firstChild;
|
|
var w=key.offsetWidth,
|
|
h=key.offsetHeight,
|
|
x=util._GetAbsoluteX(key) - util._GetAbsoluteX(osk._DivVKbd) + w/2,
|
|
y=util._GetAbsoluteY(key) - util._GetAbsoluteY(osk._DivVKbd);
|
|
return x+','+y+','+w+','+h;
|
|
};
|
|
|
|
/**
|
|
* Accept an external key ID (from KeymanTouch) and pass to the keyboard mapping
|
|
*
|
|
* @param {string} keyName key identifier
|
|
**/
|
|
keymanweb['executePopupKey'] = function(keyName)
|
|
{
|
|
if(!keymanweb.keyboardManager.activeKeyboard) return false;
|
|
|
|
/* Clear any pending (non-popup) key */
|
|
osk.keyPending = null;
|
|
|
|
// Changes for Build 353 to resolve KMEI popup key issues
|
|
keyName=keyName.replace('popup-',''); //remove popup prefix if present (unlikely)
|
|
|
|
var t=keyName.split('-'),layer=(t.length>1?t[0]:osk.layerId);
|
|
keyName=t[t.length-1];
|
|
if(layer == 'undefined') layer=osk.layerId;
|
|
|
|
var Lelem=keymanweb._LastActiveElement,Lkc,keyShiftState=osk.getModifierState(layer);
|
|
|
|
if(keymanweb._ActiveElement == null) keymanweb._ActiveElement=Lelem;
|
|
|
|
// Process modifier key action
|
|
if(osk.selectLayer(keyName,null)) return true;
|
|
|
|
// Check the virtual key
|
|
Lkc = {Ltarg:Lelem,Lmodifiers:0,Lstates:0,Lcode:osk.keyCodes[keyName],LisVirtualKey:true};
|
|
|
|
// Set the flags for the state keys.
|
|
Lkc.Lstates |= osk._stateKeys['K_CAPS'] ? osk.modifierCodes['CAPS'] : osk.modifierCodes['NO_CAPS'];
|
|
Lkc.Lstates |= osk._stateKeys['K_NUMLOCK'] ? osk.modifierCodes['NUM_LOCK'] : osk.modifierCodes['NO_NUM_LOCK'];
|
|
Lkc.Lstates |= osk._stateKeys['K_SCROLL'] ? osk.modifierCodes['SCROLL_LOCK'] : osk.modifierCodes['NO_SCROLL_LOCK'];
|
|
|
|
// Set LisVirtualKey to false to ensure that nomatch rule does fire for U_xxxx keys
|
|
if(keyName.substr(0,2) == 'U_') Lkc.isVirtualKey=false;
|
|
|
|
// Get code for non-physical keys
|
|
if(typeof Lkc.Lcode == 'undefined') {
|
|
Lkc.Lcode = osk.getVKDictionaryCode(keyName);
|
|
if (!Lkc.Lcode) {
|
|
// Special case for U_xxxx keys
|
|
Lkc.Lcode = 1;
|
|
}
|
|
}
|
|
|
|
//if(!Lkc.Lcode) return false; // Value is now zero if not known (Build 347)
|
|
//Build 353: revert to prior test to try to fix lack of KMEI output, May 1, 2014
|
|
if(isNaN(Lkc.Lcode) || !Lkc.Lcode) return false;
|
|
|
|
// Define modifiers value for sending to keyboard mapping function
|
|
Lkc.Lmodifiers = keyShiftState;
|
|
|
|
// Handles modifier states when the OSK is emulating rightalt through the leftctrl-leftalt layer.
|
|
if((Lkc.Lmodifiers & osk.modifierBitmasks['ALT_GR_SIM']) == osk.modifierBitmasks['ALT_GR_SIM'] && osk.emulatesAltGr()) {
|
|
Lkc.Lmodifiers &= ~osk.modifierBitmasks['ALT_GR_SIM'];
|
|
Lkc.Lmodifiers |= osk.modifierCodes['RALT'];
|
|
}
|
|
|
|
Lkc.vkCode=Lkc.Lcode;
|
|
|
|
// Pass this key code and state to the keyboard program
|
|
if(!keymanweb.keyboardManager.activeKeyboard || Lkc.Lcode == 0) return false;
|
|
|
|
// If key is mapped, return true
|
|
if(kbdInterface.processKeystroke(util.device, Lelem, Lkc)) return true;
|
|
|
|
keymanweb.processDefaultMapping(Lkc.Lcode, keyShiftState, Lelem, keyName);
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* API endpoint for hardware keystroke events from Android external keyboards
|
|
*
|
|
* @param {number} code key identifier
|
|
* @param {number} shift shift state (0x01=left ctrl 0x02=right ctrl 0x04=left alt 0x08=right alt
|
|
* 0x10=shift 0x20=ctrl 0x40=alt)
|
|
* @param {number} lstates lock state (0x0200=no caps 0x0400=num 0x0800=no num 0x1000=scroll 0x2000=no scroll locks)
|
|
**/
|
|
keymanweb['executeHardwareKeystroke'] = function(code, shift, lstates = 0) {
|
|
if(!keymanweb.keyboards.activeKeyboard || code == 0) {
|
|
return false;
|
|
}
|
|
|
|
// Clear any pending (non-popup) key
|
|
osk.keyPending = null;
|
|
|
|
var Lelem = keymanweb._LastActiveElement;
|
|
|
|
if(keymanweb._ActiveElement == null) {
|
|
keymanweb._ActiveElement = Lelem;
|
|
}
|
|
|
|
// Check the virtual key
|
|
var Lkc = {
|
|
Ltarg: keymanweb._ActiveElement,
|
|
Lmodifiers: shift,
|
|
vkCode: code,
|
|
Lcode: code,
|
|
Lstates: lstates,
|
|
LisVirtualKey: true,
|
|
LisVirtualKeyCode: false
|
|
};
|
|
|
|
try {
|
|
// Pass this key code and state to the keyboard program
|
|
// If key is mapped, return true
|
|
if(kbdInterface.processKeystroke(util.physicalDevice, Lelem, Lkc)) {
|
|
return true;
|
|
}
|
|
|
|
return keymanweb.processDefaultMapping(Lkc.Lcode, shift, Lelem, '');
|
|
} catch (err) {
|
|
console.error(err.message, err);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Process default mapping only if necessary (last resort)
|
|
* @param {number} code key identifier
|
|
* @param {number} shift shift state (0x01=left ctrl 0x02=right ctrl 0x04=left alt 0x08=right alt
|
|
* 0x10=shift 0x20=ctrl 0x40=alt)
|
|
* @param {Object} Lelem element to output to
|
|
* @param {string} keyName
|
|
* @return {boolean} true if key code successfully processed
|
|
*/
|
|
keymanweb.processDefaultMapping = function(code, shift, Lelem, keyName) {
|
|
if (code == osk.keyCodes.K_SPACE) {
|
|
kbdInterface.output(0, Lelem, ' ');
|
|
return true;
|
|
}
|
|
else if (code == osk.keyCodes.K_ENTER) {
|
|
kbdInterface.output(0, Lelem, '\n');
|
|
return true;
|
|
}
|
|
var ch = osk.defaultKeyOutput(keyName, code, shift, false);
|
|
if(ch) {
|
|
kbdInterface.output(0, Lelem, ch);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
})();
|