mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-05 00:15:32 +00:00
1311 lines
45 KiB
JavaScript
1311 lines
45 KiB
JavaScript
/***
|
|
KeymanWeb 2.0
|
|
Copyright 2017 SIL International
|
|
***/
|
|
|
|
|
|
/**
|
|
* Base code: Declare tavultesoft, major component namespaces and instances, utility functions
|
|
*/
|
|
|
|
/** @define {number} */
|
|
var __BUILD__ = 300;
|
|
|
|
(function()
|
|
{
|
|
// Define exposed base object
|
|
var tavultesoft = window['tavultesoft'] = {};
|
|
|
|
var keymanweb = tavultesoft['keymanweb'] = {
|
|
_TitleElement: null, // I1972 - KeymanWeb Titlebar should not be a link
|
|
_Enabled: 1, // Is KeymanWeb running?
|
|
_IE: 0, // browser version identification
|
|
legacy: 0, // set true for IE 3,4,5 (I2186 - multiple IE tweaks needed)
|
|
_IsActivatingKeymanWebUI: 0, // ActivatingKeymanWebUI - is the KeymanWeb DIV in process of being clicked on?
|
|
_JustActivatedKeymanWebUI: 0, // JustActivatedKeymanWebUI - focussing back to control after KeymanWeb UI interaction
|
|
_IgnoreNextSelChange: 0, // when a visual keyboard key is mouse-down, ignore the next sel change because this stuffs up our history
|
|
_Selection: null,
|
|
_SelectionControl: null,
|
|
_KeyboardStubs: [], // KeyboardStubs - array of available keyboards
|
|
dfltStub: null, // First keyboard stub loaded - default for touch-screen devices, ignored on desktops
|
|
_Keyboards: [], // Keyboards - array of loaded keyboards
|
|
_ActiveKeyboard: null, // ActiveKeyboard - points to active keyboard in Keyboards array
|
|
_ActiveStub: null, // ActiveStub - points to active stub in KeyboardStubs
|
|
_LoadingInternalName: null,
|
|
_AnyIndices: [], // AnyIndex - array of any/index match indices
|
|
_DeadKeys: [], // DeadKeys - array of matched deadkeys
|
|
_ActiveControl: null, // Currently active control in _Controls array
|
|
_Controls: [], // Array of controls with specific states: .Control, .Enabled, .DefaultInternalName
|
|
_AttachedElements: [], // I1596 - attach to controls dynamically
|
|
_ActiveElement: null, // Currently active (focused) element I3363 (Build 301)
|
|
_LastActiveElement: null, // LastElem - Last active element
|
|
_DfltStyle: '', // Default styles
|
|
_MasterDocument: null, // Document with controller (to allow iframes to distinguish local/master control)
|
|
_HotKeys: [], // Array of document-level hotkey objects
|
|
focusTimer: null, // Timer to manage loss of focus to unmapped input
|
|
focusing: false, // Flag to manage movement of focus
|
|
resizing: false, // Flag to control resize events when resetting viewport parameters
|
|
sortedInputs: [], // List of all INPUT and TEXTAREA elements ordered top to bottom, left to right
|
|
inputList: [], // List of simulated input divisions for touch-devices I3363 (Build 301)
|
|
languageList: null, // List of keyboard languages available for KeymanCloud
|
|
languagesPending: [], // Array of languages waiting to be registered
|
|
deferredStubs: [], // Array of pending keyboard stubs from addKeyboard(), to register after initialization
|
|
deferredKRS: [], // Array of pending keyboard stubs from KRS, to register afterf initialization
|
|
deferredKR: [], // Array of pending keyboards, to be installed at end of initialization
|
|
loadTimer:null, // Keyboard loading timer, to manage load failure
|
|
waiting:null, // Element displayed during keyboard load time
|
|
warned: false, // Warning flag (to prevent multiple warnings)
|
|
baseFont: 'sans-serif', // Default font for mapped input elements
|
|
appliedFont: '', // Chain of fonts to be applied to mapped input elements
|
|
embeddedFonts: [], // Array of currently embedded fonts
|
|
fontCheckTimer: null, // Timer for testing loading of embedded fonts
|
|
srcPath: '', // Path to folder containing executing keymanweb script
|
|
rootPath: '', // Path to server root
|
|
mustReloadKeyboard: false,// Force keyboard refreshing even if already loaded
|
|
fullInitialization: true // Force full page initialization unless overridden
|
|
};
|
|
|
|
keymanweb['initialized'] = 0;
|
|
keymanweb['build'] = __BUILD__;
|
|
keymanweb['version'] = '2.0';
|
|
keymanweb['helpURL'] = 'http://help.keyman.com/go';
|
|
|
|
// Expose (old) KeymanWeb object for use by compiled keyboards (replaces legacy object in earlier versions)
|
|
window['KeymanWeb'] = keymanweb;
|
|
|
|
// Define public OSK, user interface and utility function objects
|
|
var util = keymanweb['util'] = {};
|
|
var osk = keymanweb['osk'] = {ready:false};
|
|
var ui = keymanweb['ui'] = {};
|
|
|
|
// Stub functions (defined later in code only if required)
|
|
keymanweb.setDefaultDeviceOptions = function(opt){}
|
|
keymanweb.getStyleSheetPath = function(s){return s;}
|
|
keymanweb.getKeyboardPath=function(f){return f;}
|
|
keymanweb.KC_ = function(n, ln, Pelem){return '';}
|
|
keymanweb.handleRotationEvents = function(){}
|
|
keymanweb.alignInputs = function(b){}
|
|
keymanweb.refreshElementContent = null;
|
|
|
|
osk.highlightSubKeys = function(k,x,y){}
|
|
osk.createKeyTip = function(){}
|
|
osk.optionKey = function(e,keyName,keyDown){}
|
|
osk.showKeyTip = function(key,on){}
|
|
osk.waitForFonts = function(kfd,ofd){return true;}
|
|
|
|
// Define private options object with empty members
|
|
keymanweb.options = {
|
|
'root':'',
|
|
'resources':'',
|
|
'keyboards':'',
|
|
'fonts':'',
|
|
'attachType':'',
|
|
'ui':null
|
|
};
|
|
|
|
// Generalized component event registration
|
|
util.events = {}; // An object mapping event names to individual event lists. Maps strings to arrays.
|
|
util.currentEvents = []; // The event messaging call stack.
|
|
|
|
/**
|
|
* Determine path and protocol of executing script
|
|
*
|
|
* This can only be done during load when the active script will be the
|
|
* last script loaded. Otherwise the script must be identified by name.
|
|
*/
|
|
var scripts = document.getElementsByTagName('script');
|
|
var ss = scripts[scripts.length-1].src,
|
|
sPath = ss.substr(0,ss.lastIndexOf('/')+1);
|
|
keymanweb.srcPath = sPath;
|
|
keymanweb.rootPath = sPath.replace(/(https?:\/\/)([^\/]*)(.*)/,'$1$2/');
|
|
keymanweb.protocol = sPath.replace(/(.{3,5}:)(.*)/,'$1');
|
|
/*
|
|
|
|
osk.positionChanged = function(newPosition)
|
|
{
|
|
return util.callEvent('osk.positionChanged', newPosition);
|
|
}
|
|
|
|
osk['setPosition'] = function(newPosition)
|
|
{
|
|
divOsk.left = newPosition.left;
|
|
...
|
|
osk.positionChanged({left: divOsk.left, top: divOsk.top, ...});
|
|
}
|
|
|
|
ui.oskPositionChanged = function(newPosition)
|
|
{
|
|
// do something with the osk
|
|
}
|
|
|
|
ui.init = function()
|
|
{
|
|
osk.addEventListener('positionChanged', ui.oskPositionChanged);
|
|
}
|
|
|
|
|
|
*/
|
|
|
|
/**
|
|
* Function addEventListener
|
|
* Scope Private
|
|
* @param {string} event name of event prefixed by module, e.g. osk.touchmove
|
|
* @param {function(Object)} func event handler
|
|
* @return {boolean}
|
|
* Description Add (or replace) an event listener for this component
|
|
*/
|
|
util.addEventListener = function(event, func)
|
|
{
|
|
util.removeEventListener(event, func);
|
|
util.events[event].push(func);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Function removeEventListener
|
|
* Scope Private
|
|
* @param {string} event name of event prefixed by module, e.g. osk.touchmove
|
|
* @param {function(Object)} func event handler
|
|
* @return {boolean}
|
|
* Description Remove the specified function from the listeners for this event
|
|
*/
|
|
util.removeEventListener = function(event, func)
|
|
{
|
|
if(typeof util.events[event] == 'undefined') util.events[event] = [];
|
|
for(var i=0; i<util.events[event].length; i++)
|
|
{
|
|
if(util.events[event][i] == func)
|
|
{
|
|
util.events[event].splice(i, 1);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Function callEvent
|
|
* Scope Private
|
|
* @param {string} event name of event prefixed by module, e.g. osk.touchmove
|
|
* @param {Array} params parameter array for function
|
|
* @return {boolean}
|
|
* Description Invoke an event using any function with up to four arguments
|
|
*/
|
|
util.callEvent = function(event,params)
|
|
{
|
|
if(typeof util.events[event] == 'undefined') return true;
|
|
if(util.currentEvents.indexOf(event) != -1) return false; // Avoid event messaging recursion!
|
|
util.currentEvents.push(event);
|
|
for(var i=0; i<util.events[event].length; i++)
|
|
{
|
|
var func=util.events[event][i],result=false;
|
|
try { result=func(params); }
|
|
catch(strExcept) { result=false; }//don't know whether to use true or false here
|
|
if(result === false) {util.currentEvents.pop(); return false;}
|
|
}
|
|
util.currentEvents.pop();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Function attachDOMEvent: Note for most browsers, adds an event to a chain, doesn't stop existing events
|
|
* Scope Public
|
|
* @param {Object} Pelem Element to which event is being attached
|
|
* @param {string} Peventname Name of event without 'on' prefix
|
|
* @param {function(Object)} Phandler Event handler for event
|
|
* @param {boolean=} PuseCapture True only if event to be handled on way to target element
|
|
* Description Attaches event handler to element DOM event
|
|
*/
|
|
util['attachDOMEvent'] = util.attachDOMEvent = function(Pelem, Peventname, Phandler, PuseCapture) {
|
|
util.detachDOMEvent(Pelem, Peventname, Phandler, PuseCapture);
|
|
if(Pelem.attachEvent) {
|
|
// IE
|
|
Pelem.attachEvent('on'+Peventname, Phandler);
|
|
} else if(Pelem.addEventListener) {
|
|
// Firefox + standards
|
|
Pelem.addEventListener(Peventname, Phandler, PuseCapture?true:false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function detachDOMEvent
|
|
* Scope Public
|
|
* @param {Object} Pelem Element from which event is being detached
|
|
* @param {string} Peventname Name of event without 'on' prefix
|
|
* @param {function(Object)} Phandler Event handler for event
|
|
* @param {boolean=} PuseCapture True if event was being handled on way to target element
|
|
* Description Detaches event handler from element [to prevent memory leaks]
|
|
*/
|
|
util['detachDOMEvent'] = util.detachDOMEvent = function(Pelem, Peventname, Phandler, PuseCapture) {
|
|
if(Pelem.detachEvent) {
|
|
Pelem.detachEvent('on'+Peventname, Phandler);
|
|
} else if(Pelem.removeEventListener) {
|
|
Pelem.removeEventListener(Peventname, Phandler, PuseCapture);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function getOption
|
|
* Scope Public
|
|
* @param {string} optionName Name of option
|
|
* @param {*=} dflt Default value of option
|
|
* @return {*}
|
|
* Description Returns value of named option
|
|
*/
|
|
util['getOption'] = util.getOption = function(optionName,dflt)
|
|
{
|
|
if(optionName in keymanweb.options) return keymanweb.options[optionName];
|
|
else if(arguments.length > 1) return dflt;
|
|
else return '';
|
|
}
|
|
|
|
/**
|
|
* Function setOption
|
|
* Scope Public
|
|
* @param {string} optionName Name of option
|
|
* @param {*=} value Value of option
|
|
* Description Sets value of named option
|
|
*/
|
|
util['setOption'] = util.setOption = function(optionName,value)
|
|
{
|
|
keymanweb.options[optionName] = value;
|
|
}
|
|
|
|
/**
|
|
* Function getAbsoluteX
|
|
* Scope Public
|
|
* @param {Object} Pobj HTML element
|
|
* @return {number}
|
|
* Description Returns x-coordinate of Pobj element absolute position with respect to page
|
|
*/
|
|
util['getAbsoluteX'] = util._GetAbsoluteX = function(Pobj) // I1476 - Handle SELECT overlapping END
|
|
|
|
{
|
|
if(!Pobj) return 0;
|
|
|
|
var Lcurleft = Pobj.offsetLeft ? Pobj.offsetLeft : 0, Lobj = Pobj; // I2404 - Support for IFRAMEs
|
|
if (Lobj.offsetParent)
|
|
{
|
|
while (Lobj.offsetParent)
|
|
{
|
|
Lobj = Lobj.offsetParent;
|
|
Lcurleft += Lobj.offsetLeft;
|
|
}
|
|
}
|
|
// Correct position if element is within a frame (but not if the controller is in document within that frame)
|
|
if(Lobj && Lobj.ownerDocument && (Pobj.ownerDocument != keymanweb._MasterDocument)) Lobj=Lobj.ownerDocument; // I2404 - Support for IFRAMEs
|
|
|
|
// The following two lines are old code and may or may not still be needed - possibly should be conditioned similalry to above
|
|
if(Lobj && Lobj.parentWindow && Lobj.parentWindow.frameElement) return Lcurleft + util._GetAbsoluteX(Lobj.parentWindow.frameElement) - Lobj.documentElement.scrollLeft;
|
|
if(Lobj && Lobj.defaultView && Lobj.defaultView.frameElement) return Lcurleft + util._GetAbsoluteX(Lobj.defaultView.frameElement) - Lobj.documentElement.scrollLeft;
|
|
return Lcurleft;
|
|
}
|
|
|
|
/**
|
|
* Function getAbsoluteY
|
|
* Scope Public
|
|
* @param {Object} Pobj HTML element
|
|
* @return {number}
|
|
* Description Returns y-coordinate of Pobj element absolute position with respect to page
|
|
*/
|
|
util['getAbsoluteY'] = util._GetAbsoluteY = function(Pobj)
|
|
{
|
|
if(!Pobj) return 0;
|
|
var Lcurtop = Pobj.offsetTop ? Pobj.offsetTop : 0, Lobj = Pobj; // I2404 - Support for IFRAMEs
|
|
if (Lobj.offsetParent)
|
|
{
|
|
while (Lobj.offsetParent)
|
|
{
|
|
Lobj = Lobj.offsetParent;
|
|
Lcurtop += Lobj.offsetTop;
|
|
}
|
|
}
|
|
// Correct position if element is within a frame (but not if the controller is in document within that frame)
|
|
if(Lobj && Lobj.ownerDocument && (Pobj.ownerDocument != keymanweb._MasterDocument)) Lobj=Lobj.ownerDocument; // I2404 - Support for IFRAMEs
|
|
|
|
// The following two lines are old code and may or may not still be needed - possibly should be conditioned similalry to above
|
|
if(Lobj && Lobj.parentWindow && Lobj.parentWindow.frameElement) return Lcurtop + util._GetAbsoluteY(Lobj.parentWindow.frameElement) - Lobj.documentElement.scrollTop;
|
|
if(Lobj && Lobj.defaultView && Lobj.defaultView.frameElement) return Lcurtop + util._GetAbsoluteY(Lobj.defaultView.frameElement) - Lobj.documentElement.scrollTop;
|
|
return Lcurtop;
|
|
}
|
|
|
|
/**
|
|
* Function getAbsolute
|
|
* Scope Public
|
|
* @param {Object} Pobj HTML element
|
|
* @return {Object.<string,number>}
|
|
* Description Returns absolute position of Pobj element with respect to page
|
|
*/
|
|
util['getAbsolute'] = util._GetAbsolute = function(Pobj)
|
|
{
|
|
var p={};
|
|
p['x'] = util._GetAbsoluteX(Pobj);
|
|
p['y'] = util._GetAbsoluteY(Pobj);
|
|
return p;
|
|
}
|
|
|
|
/**
|
|
* Select start handler (to replace multiple inline handlers) (Build 360)
|
|
*/
|
|
util.selectStartHandler = function()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Default mouse down event handler (to replace multiple inline handlers) (Build 360)
|
|
*/
|
|
util.mouseDownPreventDefaultHandler = function(e)
|
|
{
|
|
if(e) e.preventDefault();
|
|
}
|
|
|
|
util.mouseDownPreventDefaultHandler = function(e) {
|
|
if(e) e.preventDefault();
|
|
}
|
|
|
|
util['createElement'] = util._CreateElement = function(nodeName)
|
|
{
|
|
var e = document.createElement(nodeName);
|
|
// Make element unselectable (Internet Explorer)
|
|
if (typeof e.onselectstart != 'undefined') //IE route
|
|
{
|
|
e.unSelectable='on'; e.onselectstart=util.selectStartHandler; // Build 360
|
|
}
|
|
// And for well-behaved browsers (may also work for IE9+, but not necessary)
|
|
else
|
|
{
|
|
e.style.MozUserSelect="none"; e.style.KhtmlUserSelect="none";
|
|
e.style.UserSelect="none"; e.style.WebkitUserSelect="none";
|
|
}
|
|
return e;
|
|
}
|
|
|
|
/**
|
|
* Function getIEVersion
|
|
* Scope Public
|
|
* @return {number}
|
|
* Description Return IE version number (or 999 if browser not IE)
|
|
*/
|
|
util['getIEVersion'] = util._GetIEVersion = function()
|
|
{
|
|
var n,agent='';
|
|
|
|
if('userAgent' in navigator) agent=navigator.userAgent;
|
|
|
|
// Test first for old versions
|
|
if('selection' in document) // only defined for IE and not for IE 11!!!
|
|
{
|
|
var appVer=navigator.appVersion;
|
|
n=appVer.indexOf('MSIE ');
|
|
if(n >= 0)
|
|
{
|
|
// Check for quirks mode page, always return 6 if so
|
|
if(document.compatMode == 'BackCompat') return 6;
|
|
|
|
appVer=appVer.substr(n+5);
|
|
n=appVer.indexOf('.');
|
|
if(n > 0) return parseInt(appVer.substr(0,n),10);
|
|
}
|
|
}
|
|
|
|
// Finally test for IE 11 (and later?)
|
|
n=agent.indexOf('Trident/');
|
|
if(n < 0) return 999;
|
|
|
|
agent=agent.substr(n+8);
|
|
n=agent.indexOf('.');
|
|
if(n > 0) return parseInt(agent.substr(0,n),10)+4;
|
|
|
|
return 999;
|
|
}
|
|
|
|
/**
|
|
* Get device horizontal DPI for touch devices, to set actual size of active regions
|
|
* Note that the actual physical DPI may be somewhat different.
|
|
*
|
|
* @return {number}
|
|
*/
|
|
util.getDPI = function()
|
|
{
|
|
var t=document.createElement('DIV'),s=t.style,dpi=96;
|
|
if(document.readyState !== 'complete') return dpi;
|
|
|
|
t.id='calculateDPI';
|
|
s.position='absolute'; s.display='block';s.visibility='hidden';
|
|
s.left='10px'; s.top='10px'; s.width='1in'; s.height='10px';
|
|
document.body.appendChild(t);
|
|
dpi=(typeof window.devicePixelRatio == 'undefined') ? t.offsetWidth : t.offsetWidth * window.devicePixelRatio;
|
|
document.body.removeChild(t);
|
|
return dpi;
|
|
}
|
|
|
|
/**
|
|
* Get browser-independent computed style value for element
|
|
*
|
|
* @param {Element} e HTML element
|
|
* @param {string} s CSS style name
|
|
* @return {*}
|
|
*/
|
|
util.getStyleValue = function(e,s)
|
|
{
|
|
// Build 349: error trap added, since on iOS, getPropertyValue may fail
|
|
// and crash in some cases, possibly if passed a text node
|
|
try
|
|
{
|
|
if(e && (typeof(window.getComputedStyle) != 'undefined'))
|
|
return window.getComputedStyle(e,'').getPropertyValue(s);
|
|
else if(e && (typeof(e.currentStyle) != 'undefined')) //IE 8 and earlier
|
|
return e.currentStyle[s];
|
|
}
|
|
catch(ex){}
|
|
|
|
// Return empty string if unable to get style value
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get browser-independent computed style integer value for element (Build 349)
|
|
*
|
|
* @param {Element} e HTML element
|
|
* @param {string} s CSS style name
|
|
* @param {number=} d default value if NaN
|
|
* @return {number} integer value of style
|
|
*/
|
|
util.getStyleInt = function(e,s,d)
|
|
{
|
|
var x=parseInt(util.getStyleValue(e,s),10);
|
|
if(!isNaN(x)) return x;
|
|
|
|
// Return the default value if numeric, else 0
|
|
if(typeof(d) == 'number') return d; else return 0;
|
|
}
|
|
|
|
/**
|
|
* Set device parameters according to platform
|
|
* TODO: put into separate function (?)
|
|
*/
|
|
var device=util.device={
|
|
touchable:!!('ontouchstart' in window),OS:'',
|
|
formFactor:'desktop',
|
|
dyPortrait:0,
|
|
dyLandscape:0,
|
|
version:'0',
|
|
orientation:window.orientation
|
|
};
|
|
|
|
if(navigator && navigator.userAgent)
|
|
{
|
|
var agent=navigator.userAgent;
|
|
if(agent.indexOf('iPad') >= 0)
|
|
{
|
|
device.OS='iOS';device.formFactor='tablet';
|
|
device.dyPortrait=device.dyLandscape=0;
|
|
}
|
|
if(agent.indexOf('iPhone') >= 0)
|
|
{
|
|
device.OS='iOS';device.formFactor='phone';
|
|
device.dyPortrait=device.dyLandscape=25;
|
|
}
|
|
if(agent.indexOf('Android') >= 0)
|
|
{
|
|
device.OS='Android';device.formFactor='phone'; // form factor may be redefined on initialization
|
|
device.dyPortrait=75; device.dyLandscape=25;
|
|
try
|
|
{
|
|
var rx=new RegExp("(?:Android\\s+)(\\d+\\.\\d+\\.\\d+)");
|
|
device.version=agent.match(rx)[1];
|
|
} catch(ex){}
|
|
|
|
}
|
|
if(agent.indexOf('Windows NT') >= 0)
|
|
{
|
|
device.OS='Windows';
|
|
if(agent.indexOf('Touch') >= 0) device.formFactor='phone'; // will be redefined as tablet if resolution high enough
|
|
|
|
// Windows Phone and Tablet PC
|
|
if(typeof navigator.msMaxTouchPoints == 'number' && navigator.msMaxTouchPoints > 0) device.touchable=true;
|
|
}
|
|
}
|
|
// var sxx=device.formFactor;
|
|
// Check and possibly revise form factor according to actual screen size (adjusted for Galaxy S, maybe OK generally?)
|
|
if(device.formFactor == 'tablet' && Math.min(screen.width,screen.height) < 400) device.formFactor='phone';
|
|
if(device.formFactor == 'phone' && Math.max(screen.width,screen.height) > 720) device.formFactor='tablet';
|
|
// alert(sxx+'->'+device.formFactor);
|
|
// Check for phony iOS devices (Win32 test excludes Chrome touch emulation on Windows)!
|
|
if(device.OS == 'iOS' && !('ongesturestart' in window) && navigator.platform != 'Win32') device.OS='Android';
|
|
|
|
// Determine application or browser
|
|
device.browser='web';
|
|
if(util._GetIEVersion() < 999) device.browser='ie';
|
|
else
|
|
{
|
|
if(device.OS == 'iOS' || device.OS.toLowerCase() == 'macosx') device.browser='safari';
|
|
var bMatch=/Firefox|Chrome|OPR/;
|
|
if(bMatch.test(navigator.userAgent))
|
|
{
|
|
if((navigator.userAgent.indexOf('Firefox') >= 0) && ('onmozorientationchange' in screen))
|
|
device.browser='firefox';
|
|
else if(navigator.userAgent.indexOf('OPR') >= 0)
|
|
device.browser='opera';
|
|
else if(navigator.userAgent.indexOf('Chrome') >= 0)
|
|
device.browser='chrome';
|
|
}
|
|
}
|
|
|
|
/* DEBUG: Force touch device (Build 360)
|
|
|
|
device.touchable = true;
|
|
device.browser = 'safari';
|
|
device.formFactor = 'tablet';
|
|
device.OS = 'iOS';
|
|
|
|
END DEBUG */
|
|
|
|
|
|
|
|
/**
|
|
* Expose the touchable state for UIs - will disable external UIs entirely
|
|
**/
|
|
util['isTouchDevice'] = function()
|
|
{
|
|
return device.touchable;
|
|
}
|
|
|
|
/**
|
|
* Get orientation of tablet or phone display
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
util.portraitView = function() // new for I3363 (Build 301)
|
|
{
|
|
return !util.landscapeView();
|
|
}
|
|
|
|
/**
|
|
* Get orientation of tablet or phone display
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
util.landscapeView = function() // new for I3363 (Build 301)
|
|
{
|
|
// Assume portrait mode if orientation undefined
|
|
if(typeof(window.orientation) == 'undefined') return false;
|
|
|
|
// Else landscape for +/-90, portrait for 0, +/-180
|
|
return (Math.abs(window.orientation/90) == 1);
|
|
}
|
|
|
|
/**
|
|
* Get viewport scale factor for this document
|
|
*
|
|
* @return {number}
|
|
*/
|
|
util.getViewportScale = function()
|
|
{
|
|
// This can sometimes fail with some browsers if called before document defined,
|
|
// so catch the exception
|
|
try {
|
|
// Get viewport width
|
|
var viewportWidth = document.documentElement.clientWidth;
|
|
|
|
// Return a default value if screen width is greater than the viewport width (not fullscreen).
|
|
if(screen.width > viewportWidth) return 1;
|
|
|
|
// Get the orientation corrected screen width
|
|
var screenWidth = screen.width;
|
|
if(util.landscapeView())
|
|
{
|
|
// Take larger of the two dimensions
|
|
if(screen.width < screen.height) screenWidth = screen.height;
|
|
}
|
|
else
|
|
{
|
|
// Take smaller of the two dimensions
|
|
if(screen.width > screen.height) screenWidth = screen.height;
|
|
}
|
|
// Calculate viewport scale
|
|
return Math.round(100*screenWidth / window.innerWidth)/100;
|
|
}
|
|
catch(ex) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return height of URL bar on mobile devices, if visible
|
|
* TODO: This does not seem to be right, so is not currently used
|
|
*
|
|
* @return {number}
|
|
*/
|
|
util.barHeight = function()
|
|
{
|
|
var dy=0;
|
|
if(device.formFactor == 'phone')
|
|
{
|
|
dy=screen.height/2-window.innerHeight-(util.landscapeView()?device.dyLandscape:device.dyPortrait);
|
|
}
|
|
return dy;
|
|
}
|
|
|
|
/**
|
|
* Function _EncodeEntities
|
|
* Scope Private
|
|
* @param {string} P_txt string to be encoded
|
|
* @return {string} encoded (html-safe) string
|
|
* Description Encode angle brackets and ampersand in text string
|
|
*/
|
|
util._EncodeEntities = function(P_txt)
|
|
{
|
|
return P_txt.replace('&','&').replace('<','<').replace('>','>'); // I1452 part 2
|
|
}
|
|
|
|
|
|
/**
|
|
* Function createShim
|
|
* Scope Public
|
|
* @return {Object} IFRAME element
|
|
* Description Create an IFRAME element to go between KMW and drop down (to fix IE6 bug)
|
|
*/
|
|
util['createShim'] = util.createShim = function() // I1476 - Handle SELECT overlapping BEGIN
|
|
{
|
|
var e = util._CreateElement('IFRAME');
|
|
e.src = '';
|
|
e.style.display = 'none';
|
|
e.style.position = 'absolute';
|
|
e.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)';
|
|
e.frameBorder = '0';
|
|
e.scrolling = 'no';
|
|
return e;
|
|
}
|
|
|
|
// I1476 - Handle SELECT overlapping BEGIN
|
|
|
|
/**
|
|
* Function showShim
|
|
* Scope Public
|
|
* @param {Object} Pvkbd Visual keyboard DIV element
|
|
* @param {Object} Pframe IFRAME shim element
|
|
* @param {Object} Phelp OSK Help DIV element
|
|
* Description Display iFrame under OSK at its currently defined position, to allow OSK to overlap SELECT elements (IE6 fix)
|
|
*/
|
|
util['showShim'] = util.showShim = function(Pvkbd,Pframe,Phelp)
|
|
{
|
|
if(Pframe)
|
|
try
|
|
{
|
|
Pframe.style.left = util._GetAbsoluteX(Pvkbd)+'px';
|
|
Pframe.style.top = util._GetAbsoluteY(Pvkbd)+'px';
|
|
if(Phelp)
|
|
{
|
|
Pframe.style.width = (util._GetAbsoluteX(Phelp)-util._GetAbsoluteX(Pvkbd)+Phelp.offsetWidth)+"px";
|
|
Pframe.style.height = (util._GetAbsoluteY(Phelp)-util._GetAbsoluteY(Pvkbd)+Phelp.offsetHeight)+"px";
|
|
}
|
|
else
|
|
{
|
|
Pframe.style.width = Pvkbd.offsetWidth+"px";
|
|
Pframe.style.height = Pvkbd.offsetHeight+"px";
|
|
}
|
|
Pframe.style.zindex = '9999';
|
|
Pframe.style.display = 'block';
|
|
} catch (Lerr) {}
|
|
}
|
|
|
|
/**
|
|
* Function hideShim
|
|
* Scope Public
|
|
* @param {Object} Pframe IFRAME shim element
|
|
* Description Hide iFrame shim containing OSK
|
|
*/
|
|
util['hideShim'] = function(Pframe)
|
|
{
|
|
try {
|
|
if(Pframe) Pframe.style.display = 'none';
|
|
} catch (err) {}
|
|
}
|
|
// I1476 - Handle SELECT overlapping END
|
|
|
|
/**
|
|
* Function rgba
|
|
* Scope Public
|
|
* @param {Object} s element style object
|
|
* @param {number} r red value, 0-255
|
|
* @param {number} g green value, 0-255
|
|
* @param {number} b blue value, 0-255
|
|
* @param {number} a opacity value, 0-1.0
|
|
* @return {string} background colour style string
|
|
* Description Browser-independent alpha-channel management
|
|
*/
|
|
util['rgba'] = util.rgba = function(s,r,g,b,a)
|
|
{ var bgColor='transparent';
|
|
if(util._GetIEVersion() < 9)
|
|
{
|
|
var pcOpacity=Math.floor(100*a),rs=r.toString(16),gs=g.toString(16),bs=b.toString(16),hexColor;
|
|
rs=('00'+rs).substr(-2); gs=('00'+gs).substr(-2); bs=('00'+bs).substr(-2);
|
|
hexColor=pcOpacity+rs+gs+bs;
|
|
s.filter='progid:DXImageTransform.Microsoft.gradient(startColorstr=#'+hexColor+',endColorstr=#'+hexColor+')';
|
|
s.zoom='1';
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
bgColor='rgba('+r+','+g+','+b+','+a+')';
|
|
}
|
|
catch(ex)
|
|
{
|
|
bgColor='rgb('+r+','+g+','+b+')';
|
|
}
|
|
}
|
|
return bgColor;
|
|
}
|
|
|
|
/**
|
|
* Add a stylesheet to a page programmatically, for use by the OSK, the UI or the page creator
|
|
*
|
|
* @param {string} s style string
|
|
* @return {Object} returns the object reference
|
|
**/
|
|
util['addStyleSheet'] = util.addStyleSheet = function(s)
|
|
{
|
|
var _ElemStyle = document.createElement('STYLE');
|
|
|
|
_ElemStyle.type = 'text/css';
|
|
if(_ElemStyle.styleSheet) // IE only
|
|
{
|
|
_ElemStyle.styleSheet.cssText = s;
|
|
}
|
|
else // all other browsers
|
|
{
|
|
_ElemStyle.appendChild(document.createTextNode(s));
|
|
}
|
|
var _ElemHead=document.getElementsByTagName('HEAD');
|
|
if(_ElemHead.length > 0)
|
|
{
|
|
_ElemHead[0].appendChild(_ElemStyle);
|
|
}
|
|
else
|
|
{
|
|
document.body.appendChild(_ElemStyle); // Won't work on Chrome, ah well
|
|
}
|
|
return _ElemStyle;
|
|
}
|
|
|
|
/**
|
|
* Remove a stylesheet element
|
|
*
|
|
* @param {Object} s style sheet reference
|
|
* @return {boolean} false if element is not a style sheet
|
|
**/
|
|
util['removeStyleSheet'] = util.removeStyleSheet = function(s)
|
|
{
|
|
if(s == null || typeof(s) != 'object') return false;
|
|
if(s.nodeName != 'STYLE') return false;
|
|
if(typeof(s.parentNode) == 'undefined' || s.parentNode == null) return false;
|
|
s.parentNode.removeChild(s);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add a reference to an external stylesheet file
|
|
*
|
|
* @param {string} s path to stylesheet file
|
|
*/
|
|
util['linkStyleSheet']=util.linkStyleSheet = function(s)
|
|
{
|
|
var headElements=document.getElementsByTagName('head');
|
|
if(headElements.length > 0)
|
|
{
|
|
var linkElement=document.createElement('link');
|
|
linkElement.type='text/css';
|
|
linkElement.rel='stylesheet';
|
|
linkElement.href=s;
|
|
headElements[0].appendChild(linkElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a stylesheet with a font-face CSS descriptor for the embedded font appropriate
|
|
* for the browser being used
|
|
*
|
|
* @param {Object} fd keymanweb font descriptor
|
|
**/
|
|
util.addFontFaceStyleSheet = function(fd)
|
|
{
|
|
// Test if a valid font descriptor
|
|
if(typeof(fd) == 'undefined') return;
|
|
|
|
if(typeof(fd['files']) == 'undefined') fd['files']=fd['source'];
|
|
if(typeof(fd['files']) == 'undefined') return;
|
|
|
|
var i,ttf='',woff='',eot='',svg='',fList=[];
|
|
|
|
// TODO: 22 Aug 2014: check that font path passed from cloud is actually used!
|
|
|
|
// Do not add a new font-face style sheet if already added for this font
|
|
for(i=0; i<keymanweb.embeddedFonts.length; i++)
|
|
if(keymanweb.embeddedFonts[i] == fd['family']) return;
|
|
|
|
if(typeof(fd['files']) == 'string') fList[0]=fd['files']; else fList=fd['files'];
|
|
|
|
for(i=0;i<fList.length;i++)
|
|
{
|
|
if(fList[i].indexOf('.ttf') > 0) ttf=fList[i];
|
|
if(fList[i].indexOf('.woff') > 0) woff=fList[i];
|
|
if(fList[i].indexOf('.eot') > 0) eot=fList[i];
|
|
if(fList[i].indexOf('.svg') > 0) svg=fList[i];
|
|
}
|
|
|
|
// Font path qualified to support page-relative fonts (build 347)
|
|
if(ttf != '' && (ttf.indexOf('/') < 0)) ttf=keymanweb.options['fonts']+ttf;
|
|
if(woff != '' && (woff.indexOf('/') < 0)) woff=keymanweb.options['fonts']+woff;
|
|
if(eot != '' && (eot.indexOf('/') < 0)) eot=keymanweb.options['fonts']+eot;
|
|
if(svg != '' && (svg.indexOf('/') < 0)) svg=keymanweb.options['fonts']+svg;
|
|
|
|
// Build the font-face definition according to the browser being used
|
|
var s='@font-face {\nfont-family:'
|
|
+fd['family']+';\nfont-style:normal;\nfont-weight:normal;\n';
|
|
|
|
// Detect if Internet Explorer and version if so
|
|
var IE=util._GetIEVersion();
|
|
|
|
// Build the font source string according to the browser,
|
|
// but return without adding the style sheet if the required font type is unavailable
|
|
|
|
// Modern browsers: use WOFF, TTF and fallback finally to SVG. Don't provide EOT
|
|
if(IE >= 9)
|
|
{
|
|
if(device.OS == 'iOS')
|
|
{
|
|
if(ttf != '')
|
|
{
|
|
// Modify the url if required to prevent caching
|
|
ttf = util.unCached(ttf);
|
|
s=s+'src:url(\''+ttf+'\') format(\'truetype\');';
|
|
}
|
|
else return;
|
|
}
|
|
else
|
|
{
|
|
var s0 = [];
|
|
|
|
if(device.OS == 'Android') {
|
|
// Android 4.2 and 4.3 have bugs in their rendering for some scripts
|
|
// with embedded ttf or woff. svg mostly works so is a better initial
|
|
// choice on the Android browser.
|
|
if(svg != '') s0.push("url('"+svg+"') format('svg')");
|
|
if(woff != '') s0.push("url('"+woff+"') format('woff')");
|
|
if(ttf != '') s0.push("url('"+ttf+"') format('truetype')");
|
|
} else {
|
|
if(woff != '') s0.push("url('"+woff+"') format('woff')");
|
|
if(ttf != '') s0.push("url('"+ttf+"') format('truetype')");
|
|
if(svg != '') s0.push("url('"+svg+"') format('svg')");
|
|
}
|
|
if(s0.length == 0) return;
|
|
|
|
s += 'src:'+s0.join(',')+';';
|
|
}
|
|
}
|
|
// IE 6-8
|
|
else
|
|
{
|
|
if(eot != '')
|
|
s=s+'src:url(\''+eot+'\');';
|
|
else return;
|
|
}
|
|
s=s+'\n}\n';
|
|
|
|
util.addStyleSheet(s);
|
|
keymanweb.embeddedFonts.push(fd['family']);
|
|
}
|
|
|
|
/**
|
|
* Allow forced reload if necessary (stub only here)
|
|
*
|
|
* @param {string} s unmodified URL
|
|
* @return modified URL
|
|
*/
|
|
util.unCached = function(s)
|
|
{
|
|
// var t=(new Date().getTime());
|
|
// s = s + '?v=' + t;
|
|
return s;
|
|
}
|
|
|
|
/**
|
|
* Document cookie parsing for use by kernel, OSK, UI etc.
|
|
*
|
|
* @param {string=} cn cookie name (optional)
|
|
* @return {Object} array of names and strings, or array of variables and values
|
|
*/
|
|
util['loadCookie'] = util.loadCookie = function(cn)
|
|
{
|
|
var v={};
|
|
if(arguments.length > 0)
|
|
{
|
|
var cx = util['loadCookie']();
|
|
for(var t in cx)
|
|
{
|
|
if(t == cn)
|
|
{
|
|
var d = unescape(cx[t]).split(';');
|
|
for(var i=0; i<d.length; i++)
|
|
{
|
|
var xc = d[i].split('=');
|
|
if(xc.length > 1) v[xc[0]] = xc[1]; else v[xc[0]] = '';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(typeof(document.cookie) != 'undefined' && document.cookie != '')
|
|
{
|
|
var c = document.cookie.split(/;\s*/);
|
|
for(var i = 0; i < c.length; i++)
|
|
{
|
|
var d = c[i].split('=');
|
|
if(d.length == 2) v[d[0]] = d[1];
|
|
}
|
|
}
|
|
}
|
|
return v;
|
|
}
|
|
|
|
/**
|
|
* Standard cookie saving for use by kernel, OSK, UI etc.
|
|
*
|
|
* @param {string} cn name of cookie
|
|
* @param {Object} cv object with array of named arguments and values
|
|
*/
|
|
|
|
util['saveCookie'] = util.saveCookie = function(cn,cv)
|
|
{
|
|
var s='';
|
|
for(var v in cv)
|
|
s = s + v+'='+cv[v]+";";
|
|
|
|
var d = new Date(new Date().valueOf() + 1000 * 60 * 60 * 24 * 30).toGMTString();
|
|
document.cookie = cn+'='+escape(s)+'; path=/; expires='+d;//Fri, 31 Dec 2099 23:59:59 GMT;';
|
|
}
|
|
|
|
/**
|
|
* Function toNumber
|
|
* Scope Public
|
|
* @param {string} s numeric string
|
|
* @param {number} dflt default value
|
|
* @return {number}
|
|
* Description Return string converted to integer or default value
|
|
*/
|
|
util['toNumber'] = util.toNumber = function(s,dflt)
|
|
{
|
|
var x = parseInt(s,10);
|
|
return isNaN(x) ? dflt : x;
|
|
}
|
|
|
|
/**
|
|
* Function toNumber
|
|
* Scope Public
|
|
* @param {string} s numeric string
|
|
* @param {number} dflt default value
|
|
* @return {number}
|
|
* Description Return string converted to real value or default value
|
|
*/
|
|
util['toFloat'] = util.toFloat = function(s,dflt)
|
|
{
|
|
var x = parseFloat(s);
|
|
return isNaN(x) ? dflt : x;
|
|
}
|
|
|
|
/**
|
|
* Function toNzString
|
|
* Scope Public
|
|
* @param {*} item variable to test
|
|
* @param {?*=} dflt default value
|
|
* @return {*}
|
|
* Description Test if a variable is null, false, empty string, or undefined, and return as string
|
|
*/
|
|
util['toNzString'] = util.nzString = function(item,dflt)
|
|
{
|
|
var dfltValue = '';
|
|
if(arguments.length > 1) dfltValue = dflt;
|
|
if(typeof(item) == 'undefined') return dfltValue;
|
|
if(item == null) return dfltValue;
|
|
if(item == 0 || item == '') return dfltValue;
|
|
return ''+item;
|
|
}
|
|
|
|
/**
|
|
* Function deepCopy
|
|
* Scope Private
|
|
* @param {Object} p object to copy
|
|
* @param {Array=} c0 array member being copied
|
|
* @return {Object} clone ('deep copy') of object
|
|
* Description Makes an actual copy (not a reference) of an object, copying simple members,
|
|
* arrays and member objects but not functions, so use with care!
|
|
*/
|
|
util.deepCopy = function(p,c0)
|
|
{
|
|
var c = c0 || {};
|
|
for (var i in p)
|
|
{
|
|
if(typeof p[i] === 'object')
|
|
{
|
|
c[i] = (p[i].constructor === Array )? [] : {};
|
|
util.deepCopy(p[i],c[i]);
|
|
}
|
|
else c[i] = p[i];
|
|
}
|
|
return c;
|
|
}
|
|
|
|
/**
|
|
* Extend Array function by adding indexOf array member if undefined (IE < IE9)
|
|
*/
|
|
if(! ('indexOf' in Array))
|
|
{
|
|
Array.prototype.indexOf = function(obj, start)
|
|
{
|
|
for(var i=(start || 0); i<this.length; i++)
|
|
{
|
|
if(this[i] == obj) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the event target for any browser
|
|
*
|
|
* @param {Event} e event
|
|
* @return {Object} HTML element
|
|
*/
|
|
util.eventTarget = function(e)
|
|
{
|
|
if(e && e.target) // most browsers
|
|
return e.target;
|
|
else if(window.event) //IE 8 (and earlier)
|
|
return window.event.srcElement;
|
|
else
|
|
return null; // shouldn't happen!
|
|
}
|
|
|
|
/**
|
|
* Return the event type for any browser
|
|
*
|
|
* @param {Event} e event
|
|
* @return {string} type of event
|
|
*/
|
|
util.eventType = function(e)
|
|
{
|
|
if(e && e.type) // most browsers
|
|
return e.type;
|
|
else if(window.event) //IE 8 (and earlier)
|
|
return window.event.type;
|
|
else
|
|
return ''; // shouldn't happen!
|
|
}
|
|
|
|
/**
|
|
* Customized alert
|
|
*
|
|
* @param {string} s alert text
|
|
* @param {function()=} fn function to call when alert dismissed
|
|
*/
|
|
util['alert'] = util.alert = function(s,fn)
|
|
{
|
|
var bg=keymanweb.waiting,nn=bg.firstChild.childNodes;
|
|
nn[0].style.display='block';
|
|
nn[1].className='kmw-alert-text'; nn[1].innerHTML=s;
|
|
nn[2].style.display='none';
|
|
bg.style.display='block';
|
|
if(arguments.length > 1) bg.dismiss=fn; else bg.dismiss=null;
|
|
}
|
|
|
|
/**
|
|
* Prepare the background and keyboard loading wait message box
|
|
* Should not be called before options are defined during initialization
|
|
**/
|
|
util.prepareWait = function()
|
|
{
|
|
var bg=document.createElement('DIV'),
|
|
lb=document.createElement('DIV'),
|
|
lt=document.createElement('DIV'),
|
|
gr=document.createElement('DIV'),
|
|
bx=document.createElement('DIV');
|
|
|
|
bg.className='kmw-wait-background'; lb.className='kmw-wait-box'; bg.dismiss=null;
|
|
lt.className='kmw-wait-text'; gr.className='kmw-wait-graphic';
|
|
bx.className='kmw-alert-close';
|
|
// Close alert if anywhere in box is touched, since close box is too small on mobiles
|
|
lb.onmousedown=lb.onclick=function(e)
|
|
{
|
|
// Ignore if waiting, only handle for alert
|
|
if(bx.style.display == 'block')
|
|
{
|
|
bg.style.display='none';
|
|
if(bg.dismiss)bg.dismiss();
|
|
}
|
|
};
|
|
lb.addEventListener('touchstart', lb.onclick, false);
|
|
bg.onmousedown=bg.onclick=function(e){e.preventDefault();e.stopPropagation();}
|
|
bg.addEventListener('touchstart', bg.onclick, false);
|
|
lb.appendChild(bx); lb.appendChild(lt); lb.appendChild(gr);
|
|
bg.appendChild(lb); document.body.appendChild(bg);
|
|
keymanweb.waiting=bg;
|
|
}
|
|
|
|
/**
|
|
* Get path of keymanweb script, for relative references
|
|
*
|
|
* *** This is not currently used, but may possibly be needed if ***
|
|
* *** script identification during loading proves unreliable. ***
|
|
*
|
|
* @param {string} sName filename prefix
|
|
*@return {string} path to source, with trailing slash
|
|
**/
|
|
util.myPath = function(sName)
|
|
{
|
|
var i,scripts=document.getElementsByTagName('script'),ss;
|
|
|
|
for(i=0; i<scripts.length; i++)
|
|
{
|
|
ss=scripts[i];
|
|
if(ss.src.indexOf(sName) >= 0)
|
|
return ss.src.substr(0,ss.src.lastIndexOf('/')+1);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
// Prepend the appropriate protocol if not included in path
|
|
util.prependProtocol = function(path)
|
|
{
|
|
var pattern = new RegExp('^https?:');
|
|
|
|
if(pattern.test(path))
|
|
return path;
|
|
else if(path.substr(0,2) == '//')
|
|
return keymanweb.protocol+path;
|
|
else if(path.substr(0,1) == '/')
|
|
return keymanweb.protocol+'/'+path;
|
|
else
|
|
return keymanweb.protocol+'//'+path;
|
|
}
|
|
|
|
/**
|
|
* Return the appropriate test string for a given font
|
|
*
|
|
* TODO: Tidy up and remove arrays once 'sample' included in font metadata
|
|
*
|
|
* @param {Object} fd font meta-data object
|
|
* @return {string} string to compare width
|
|
*
|
|
*/
|
|
util.testString = function(fd)
|
|
{
|
|
var fontName=fd['family'],
|
|
i,s='BESbswy';
|
|
if('sample' in fd && typeof(fd['sample']) == 'string')
|
|
return s+fd['sample'];
|
|
|
|
var f=['TamilWeb','TibetanWeb','LatinWeb','CherokeeWeb',
|
|
'EgyptianWeb','SinhalaWeb','KhmerWeb','ArabicWeb',
|
|
'BurmeseWeb','LaoWeb','OriyaWeb','GeezWeb'],
|
|
t=['\u0BBE\u0BF5','\u0F7F\u0FD0','\u02B0\u02A4','\u13D0\u13C9',
|
|
'\uA723\uF7D3','\u0DD8\u0DA3','\u17D6\u178E','\u0639\u06B3',
|
|
'\u1038\u1024','\u0EC0\u0EDD','\u0B03\u0B06','\u1361\u132C'];
|
|
for(i=0; i<f.length; i++)
|
|
{
|
|
if(fontName == f[i]) return s+t[i];
|
|
}
|
|
return s;
|
|
}
|
|
|
|
/**
|
|
* Test if a font is installed (or available) on the target platform
|
|
*
|
|
* @param {Object} fd font structure
|
|
* @return {boolean} true if font available
|
|
*/
|
|
util.checkFont = function(fd)
|
|
{
|
|
var fontReady=false,fontName=fd['family'];
|
|
|
|
// Create an absolute positioned div and two paragraph elements with spans for the test string.
|
|
// The paragraph elements ensure that the spans are measured from the same point, otherwise
|
|
// pixel rounding can result in different widths for the same string and styles.
|
|
// Using a separate invisible DIV is more reliable than other positioning.
|
|
var d=document.createElement('DIV'),ds=d.style,
|
|
p1=document.createElement('P'),
|
|
p2=document.createElement('P'),
|
|
t1=document.createElement('SPAN'),s1=t1.style,
|
|
t2=document.createElement('SPAN'),s2=t2.style;
|
|
|
|
ds.position='absolute';ds.top='10px';ds.left='10px';
|
|
ds.visibility='hidden';
|
|
document.body.appendChild(d);
|
|
d.appendChild(p1); d.appendChild(p2);
|
|
p1.appendChild(t1); p2.appendChild(t2);
|
|
|
|
// Firefox fails without the !important prefix on the fallback font,
|
|
// apparently applying the same font to both elements.
|
|
// But it also fails to distinguish the two if !important is added to the test font!
|
|
// *** TODO: See if still true after changes Dec 2013 ***
|
|
// Must apply !important tag to font-family, but must apply it to the CSS style, not the JS object member
|
|
// c.f. http://stackoverflow.com/questions/462537/overriding-important-style-using-javascript
|
|
t1.setAttribute('style','font-family:monospace !important');
|
|
s2.fontFamily=fontName+',monospace';
|
|
s1.fontSize=s2.fontSize='24px'; // Not too large, to avoid wrapping or overflow
|
|
|
|
// Include narrow and wide characters from each unique script
|
|
t1.innerHTML=t2.innerHTML=util.testString(fd);
|
|
|
|
// Compare the actual width of each span. Checking monospace, serif,
|
|
// and sans-serif helps to avoid falsely reporting the font as ready
|
|
// The width must be different for all three tests.
|
|
if(t1.offsetWidth != t2.offsetWidth)
|
|
{
|
|
t1.setAttribute('style','font-family:sans-serif !important');
|
|
s2.fontFamily=fontName+',sans-serif';
|
|
if(t1.offsetWidth != t2.offsetWidth)
|
|
{
|
|
t1.setAttribute('style','font-family:serif !important');
|
|
s2.fontFamily=fontName+',serif';
|
|
}
|
|
}
|
|
fontReady=(t1.offsetWidth != t2.offsetWidth);
|
|
|
|
// Delete test elements
|
|
p1.removeChild(t1);p2.removeChild(t2);
|
|
d.removeChild(p1);d.removeChild(p2);
|
|
document.body.removeChild(d);
|
|
|
|
return fontReady;
|
|
}
|
|
|
|
/**
|
|
* Expose font testing to allow checking that SpecialOSK or custom font has
|
|
* been correctly loaded by browser
|
|
*
|
|
* @param {string} fName font-family name
|
|
* @return {boolean} true if available
|
|
**/
|
|
keymanweb['isFontAvailable'] = function(fName)
|
|
{
|
|
return util.checkFont({'family':fName});
|
|
}
|
|
|
|
/**
|
|
* Check a font descriptor for font availability, returning true if undefined
|
|
*
|
|
* @param {Object} fd font descriptor member of keyboard stub
|
|
* @return {boolean}
|
|
**/
|
|
util.checkFontDescriptor = function(fd)
|
|
{
|
|
if(typeof(fd) == 'undefined' || typeof(fd['family']) != 'string') return true;
|
|
return util.checkFont(fd);
|
|
}
|
|
|
|
})();
|
|
|