mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-07 17:36:00 +00:00
May not handle the BUILD variable properly and doesn't yet have constants for the relative path directories.
510 lines
No EOL
18 KiB
JavaScript
510 lines
No EOL
18 KiB
JavaScript
// KeymanWeb 2.0
|
|
// Copyright 2014 Tavultesoft Pty Ltd
|
|
|
|
/*****************************************/
|
|
/* */
|
|
/* Embedded application-specific code */
|
|
/* */
|
|
/*****************************************/
|
|
|
|
(function()
|
|
{
|
|
// Declare KeymanWeb and related objects
|
|
var keymanweb=window['tavultesoft']['keymanweb'],osk=keymanweb['osk'],util=keymanweb['util'],device=util.device;
|
|
|
|
// 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 (code not needed for embedded applications)
|
|
keymanweb.fullInitialization = false;
|
|
|
|
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)
|
|
keymanweb.getKeyboardPath = function(Lfilename)
|
|
{
|
|
return keymanweb.rootPath+'languages/' + Lfilename + "?v=" + (new Date()).getTime(); /*cache buster*/
|
|
}
|
|
|
|
/**
|
|
* Force reload of resource
|
|
*
|
|
* @param {string} s unmodified URL
|
|
* @return modified URL
|
|
*/
|
|
util.unCached = function(s)
|
|
{
|
|
var t=(new Date().getTime());
|
|
s = s + '?v=' + t;
|
|
return s;
|
|
}
|
|
|
|
// 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;
|
|
if(Pelem.body) var Ldoc=Pelem; else var Ldoc=Pelem.ownerDocument; // I1481 - use Ldoc to get the ownerDocument when no selection is found
|
|
|
|
if(keymanweb.legacy)
|
|
{
|
|
return Pelem.value._kmwSubstr(Pelem.length-n, ln); //I3319
|
|
}
|
|
else 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._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,Lcode:osk.keyCodes[keyName],LisVirtualKey:true};
|
|
|
|
if(typeof Lkc.Lcode == 'undefined')
|
|
Lkc.Lcode = osk.getVKDictionaryCode(keyName);
|
|
|
|
if(!Lkc.Lcode)
|
|
{
|
|
// Key code will be Unicode value for U_xxxx keys
|
|
if(keyName.substr(0,2) == 'U_')
|
|
{
|
|
var tUnicode=parseInt(keyName.substr(2),16);
|
|
if(!isNaN(tUnicode)) Lkc.Lcode=tUnicode;
|
|
}
|
|
}
|
|
|
|
//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;
|
|
|
|
Lkc.vkCode=Lkc.Lcode;
|
|
|
|
// Ensure that KIK returns true for U_xxxx keys to avoid firing nomatch
|
|
if(keyName.substr(0,2) == 'U_') Lkc.isVirtualKey=false;
|
|
|
|
// Define modifiers value for sending to keyboard mapping function
|
|
Lkc.Lmodifiers = keyShiftState*0x10;
|
|
|
|
// Pass this key code and state to the keyboard program
|
|
if(!keymanweb._ActiveKeyboard || Lkc.Lcode == 0) return false;
|
|
|
|
// If key is mapped, return true
|
|
if(keymanweb._ActiveKeyboard['gs'](Lelem, Lkc)) return true;
|
|
|
|
// Use default mapping only if necessary (last resort)
|
|
var ch = osk.defaultKeyOutput(keyName,Lkc.Lcode,keyShiftState);
|
|
if(ch) keymanweb.KO(0, Lelem, ch);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* API endpoint for hardware keystroke events from Android external keyboards
|
|
*
|
|
* @param {number} code key identifier
|
|
* @param {number} shift shift state (0x10=shift 0x20=ctrl 0x40=alt)
|
|
**/
|
|
keymanweb['executeHardwareKeystroke'] = function(code, shift)
|
|
{
|
|
if(!keymanweb._ActiveKeyboard || code == 0) {
|
|
return false;
|
|
}
|
|
var cachedTouchable = device.touchable, cachedFormFactor = device.formFactor;
|
|
device.touchable = false;
|
|
device.formFactor = 'desktop';
|
|
var result = keymanweb.executeHardwareKeystrokeInternal(code, shift);
|
|
device.touchable = cachedTouchable;
|
|
device.formFactor = cachedFormFactor;
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Process the hardware key to the keyboard mapping
|
|
*
|
|
* @param {number} code key identifier
|
|
* @param {number} shift shift state (0x10=shift 0x20=ctrl 0x40=alt)
|
|
**/
|
|
keymanweb.executeHardwareKeystrokeInternal = function(code, shift) {
|
|
|
|
// 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,
|
|
LisVirtualKey: true,
|
|
LisVirtualKeyCode: false
|
|
};
|
|
|
|
// Pass this key code and state to the keyboard program
|
|
// If key is mapped, return true
|
|
if(keymanweb._ActiveKeyboard['gs'](Lelem, Lkc)) {
|
|
return true;
|
|
}
|
|
|
|
// Use default mapping only if necessary (last resort)
|
|
var ch = osk.defaultKeyOutput('', Lkc.Lcode, shift);
|
|
if(ch) {
|
|
keymanweb.KO(0, Lelem, ch);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
})(); |