mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-09 02:15:32 +00:00
Merge pull request #8280 from keymanapp/refactor/web/cookie-serialization
refactor(web): cookie serialization + deserialization 🧩
This commit is contained in:
commit
884efc700b
7 changed files with 308 additions and 346 deletions
91
web/src/engine/dom-utils/src/cookieSerializer.ts
Normal file
91
web/src/engine/dom-utils/src/cookieSerializer.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
type DecodedCookieFieldValue = string | number | boolean;
|
||||
|
||||
type FilteredRecordEncoder = (value: DecodedCookieFieldValue, key: string) => string;
|
||||
type FilteredRecordDecoder = (value: string, key: string) => DecodedCookieFieldValue;
|
||||
const no_change = (val: string) => val as string;
|
||||
|
||||
export default class CookieSerializer<Type extends Record<keyof Type, DecodedCookieFieldValue>> {
|
||||
readonly name: string;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
load(decoder?: FilteredRecordDecoder): Type {
|
||||
return this.loadCookie(this.name, decoder || no_change) as Type;
|
||||
}
|
||||
|
||||
save(cookie: Type, encoder?: FilteredRecordEncoder) {
|
||||
this.saveCookie(this.name, cookie, encoder || no_change);
|
||||
}
|
||||
|
||||
/**
|
||||
* Document cookie parsing for use by kernel, OSK, UI etc.
|
||||
*
|
||||
* @return {Object} array of names and strings
|
||||
*/
|
||||
private _loadRawCookies(): Record<string, string> {
|
||||
let v: Record<string, string> = {};
|
||||
if(typeof(document.cookie) != 'undefined' && document.cookie != '') {
|
||||
let c = document.cookie.split(/;\s*/);
|
||||
for(let i = 0; i < c.length; i++) {
|
||||
let d = c[i].split('=');
|
||||
if(d.length == 2) {
|
||||
v[d[0]] = d[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Document cookie parsing for use by kernel, OSK, UI etc.
|
||||
*
|
||||
* @param {string} cookieName cookie name
|
||||
* @return {Object} array of variables and values
|
||||
*/
|
||||
private loadCookie(cookieName: string, decoder: FilteredRecordDecoder): Record<string, DecodedCookieFieldValue> {
|
||||
let cookie: Record<string, DecodedCookieFieldValue> = {};
|
||||
let allCookies = this._loadRawCookies();
|
||||
const encodedCookie = allCookies[cookieName];
|
||||
|
||||
if(encodedCookie) {
|
||||
let rawDecode = decodeURIComponent(encodedCookie).split(';');
|
||||
for(let i=0; i<rawDecode.length; i++) {
|
||||
// Prevent accidental empty-key entries caused by cookie-final ';'.
|
||||
if(i == rawDecode.length - 1 && !rawDecode[i]) {
|
||||
break;
|
||||
}
|
||||
|
||||
let record = rawDecode[i].split('=');
|
||||
if(record.length > 1) {
|
||||
const [key, value] = record;
|
||||
// key, value
|
||||
cookie[key] = decoder(value, key);
|
||||
} else {
|
||||
// key, <implied 'true', as boolean flag>
|
||||
cookie[record[0]] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard cookie saving for use by kernel, OSK, UI etc.
|
||||
*
|
||||
* @param {string} cookieName name of cookie
|
||||
* @param {Object} cookieValueMap object with array of named arguments and values
|
||||
*/
|
||||
private saveCookie(cookieName: string, cookieValueMap: Record<string, DecodedCookieFieldValue>, encoder: FilteredRecordEncoder) {
|
||||
let serialization='';
|
||||
for(let key in cookieValueMap) {
|
||||
serialization += key + '=' + encoder(cookieValueMap[key], key) + ";";
|
||||
}
|
||||
|
||||
let d = new Date(new Date().valueOf() + 1000 * 60 * 60 * 24 * 30).toUTCString();
|
||||
let cookieConfig = ' path=/; expires=' + d; //Fri, 31 Dec 2099 23:59:59 GMT;';
|
||||
document.cookie = `${cookieName}=${encodeURIComponent(serialization)}; ${cookieConfig}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export { getAbsoluteX, getAbsoluteY } from './getAbsolute.js';
|
||||
export { default as createUnselectableElement } from './createUnselectableElement.js';
|
||||
export { createStyleSheet, StylesheetManager } from './stylesheets.js';
|
||||
export { default as landscapeView } from './landscapeView.js';
|
||||
export { default as landscapeView } from './landscapeView.js';
|
||||
export { default as CookieSerializer } from './cookieSerializer.js';
|
||||
|
|
@ -29,8 +29,6 @@ namespace com.keyman {
|
|||
activeDevice: Device;
|
||||
physicalDevice: Device;
|
||||
|
||||
linkedStylesheets: (HTMLLinkElement|HTMLStyleElement)[] = [];
|
||||
|
||||
waiting: HTMLDivElement; // The element displayed for util.wait and util.alert.
|
||||
|
||||
// An object mapping event names to individual event lists. Maps strings to arrays.
|
||||
|
|
@ -262,37 +260,6 @@ namespace com.keyman {
|
|||
setOption(optionName,value) {
|
||||
this.keyman.options[optionName] = value;
|
||||
}
|
||||
|
||||
// Unofficial API used by our desktop UIs.
|
||||
getAbsoluteX(Pobj: HTMLElement): number {
|
||||
return dom.Utils.getAbsoluteX(Pobj);
|
||||
}
|
||||
|
||||
// Unofficial API used by our desktop UIs.
|
||||
getAbsoluteY(Pobj: HTMLElement): number {
|
||||
return dom.Utils.getAbsoluteY(Pobj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function getAbsolute
|
||||
* Scope Public
|
||||
* @param {Object} Pobj HTML element
|
||||
* @return {Object.<string,number>}
|
||||
* Description Returns absolute position of Pobj element with respect to page
|
||||
*/
|
||||
getAbsolute(Pobj: HTMLElement) {
|
||||
var p={
|
||||
/* @ export */
|
||||
x: this.getAbsoluteX(Pobj),
|
||||
/* @ export */
|
||||
y: this.getAbsoluteY(Pobj)
|
||||
};
|
||||
return p;
|
||||
}
|
||||
|
||||
// Unofficial API used by our desktop UIs.
|
||||
_GetAbsolute = this.getAbsolute;
|
||||
|
||||
/**
|
||||
* Select start handler (to replace multiple inline handlers) (Build 360)
|
||||
*/
|
||||
|
|
@ -541,196 +508,6 @@ namespace com.keyman {
|
|||
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
|
||||
**/
|
||||
addStyleSheet(s: string): HTMLStyleElement {
|
||||
var _ElemStyle: HTMLStyleElement = <HTMLStyleElement>document.createElement<'style'>('style');
|
||||
|
||||
_ElemStyle.type = 'text/css';
|
||||
_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
|
||||
}
|
||||
|
||||
this.linkedStylesheets.push(_ElemStyle);
|
||||
|
||||
return _ElemStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a stylesheet element
|
||||
*
|
||||
* @param {Object} s style sheet reference
|
||||
* @return {boolean} false if element is not a style sheet
|
||||
**/
|
||||
removeStyleSheet(s: HTMLStyleElement) {
|
||||
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
|
||||
*/
|
||||
linkStyleSheet(s: string): void {
|
||||
try {
|
||||
if(document.querySelector("link[href="+JSON.stringify(s)+"]") != null) {
|
||||
// We've already linked this stylesheet, don't do it again
|
||||
return;
|
||||
}
|
||||
} catch(e) {
|
||||
// We've built an invalid href, somehow?
|
||||
return;
|
||||
}
|
||||
|
||||
var headElements=document.getElementsByTagName('head');
|
||||
if(headElements.length > 0) {
|
||||
var linkElement=document.createElement('link');
|
||||
linkElement.type='text/css';
|
||||
linkElement.rel='stylesheet';
|
||||
linkElement.href=s;
|
||||
this.linkedStylesheets.push(linkElement);
|
||||
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
|
||||
**/
|
||||
addFontFaceStyleSheet(fd: any) { // TODO: Font descriptor object needs definition!
|
||||
// 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<this.embeddedFonts.length; i++) {
|
||||
if(this.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].toLowerCase().indexOf('.otf') > 0) ttf=fList[i];
|
||||
if(fList[i].toLowerCase().indexOf('.ttf') > 0) ttf=fList[i];
|
||||
if(fList[i].toLowerCase().indexOf('.woff') > 0) woff=fList[i];
|
||||
if(fList[i].toLowerCase().indexOf('.eot') > 0) eot=fList[i];
|
||||
if(fList[i].toLowerCase().indexOf('.svg') > 0) svg=fList[i];
|
||||
}
|
||||
|
||||
// Font path qualified to support page-relative fonts (build 347)
|
||||
if(ttf != '' && (ttf.indexOf('/') < 0)) {
|
||||
ttf = this.keyman.options['fonts']+ttf;
|
||||
}
|
||||
|
||||
if(woff != '' && (woff.indexOf('/') < 0)) {
|
||||
woff = this.keyman.options['fonts']+woff;
|
||||
}
|
||||
|
||||
if(eot != '' && (eot.indexOf('/') < 0)) {
|
||||
eot = this.keyman.options['fonts']+eot;
|
||||
}
|
||||
|
||||
if(svg != '' && (svg.indexOf('/') < 0)) {
|
||||
svg = this.keyman.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';
|
||||
|
||||
// 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(this.device.OS == 'iOS') {
|
||||
if(ttf != '') {
|
||||
// Modify the url if required to prevent caching
|
||||
ttf = this.unCached(ttf);
|
||||
s=s+'src:url(\''+ttf+'\') format(\'truetype\');';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
var s0 = [];
|
||||
|
||||
if(this.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(',')+';';
|
||||
}
|
||||
|
||||
s=s+'\n}\n';
|
||||
|
||||
this.addStyleSheet(s);
|
||||
this.embeddedFonts.push(fd['family']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow forced reload if necessary (stub only here)
|
||||
*
|
||||
|
|
@ -743,59 +520,6 @@ namespace com.keyman {
|
|||
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
|
||||
*/
|
||||
loadCookie(cn?: string) {
|
||||
var v={};
|
||||
if(arguments.length > 0) {
|
||||
var cx = this.loadCookie();
|
||||
for(var t in cx) {
|
||||
if(t == cn) {
|
||||
var d = decodeURIComponent(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
|
||||
*/
|
||||
saveCookie(cn: string, 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).toUTCString();
|
||||
document.cookie = cn+'='+encodeURIComponent(s)+'; path=/; expires='+d;//Fri, 31 Dec 2099 23:59:59 GMT;';
|
||||
}
|
||||
|
||||
/**
|
||||
* Function toNumber
|
||||
* Scope Public
|
||||
|
|
@ -973,14 +697,6 @@ namespace com.keyman {
|
|||
|
||||
// Remove any KMW-added DOM element clutter.
|
||||
this.waiting.parentNode.removeChild(this.waiting);
|
||||
|
||||
for(let ss of this.linkedStylesheets) {
|
||||
if(ss.remove) {
|
||||
ss.remove();
|
||||
} else if(ss.parentNode) {
|
||||
ss.parentNode.removeChild(ss);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
export { Codes, DeviceSpec, Keyboard, KeyboardProperties, SpacebarText } from '@keymanapp/keyboard-processor';
|
||||
|
||||
export { default as FloatingOSKView } from './views/floatingOskView.js';
|
||||
export { default as FloatingOSKViewCookie } from './views/floatingOskCookie.js';
|
||||
export { default as AnchoredOSKView } from './views/anchoredOskView.js';
|
||||
export { default as InlinedOSKView } from './views/inlinedOskView.js';
|
||||
export { BannerController } from './banner/bannerView.js';
|
||||
// Is referenced by at least one desktop UI module.
|
||||
export { FloatingOSKCookie as FloatingOSKViewCookie } from './views/floatingOskCookie.js';
|
||||
export { default as VisualKeyboard } from './visualKeyboard.js';
|
||||
export type { default as ViewConfiguration } from './config/viewConfiguration.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,75 @@
|
|||
export default interface FloatingOSKCookie {
|
||||
visible: '0' | '1';
|
||||
userSet: '0' | '1';
|
||||
left: string;
|
||||
top: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
import { CookieSerializer } from 'keyman/engine/dom-utils';
|
||||
|
||||
export interface FloatingOSKCookie {
|
||||
/**
|
||||
* Notes whether or not the OSK was hidden at the end of the previous session.
|
||||
*/
|
||||
visible: 0 | 1;
|
||||
|
||||
/**
|
||||
* Notes whether or not the OSK was pinned (located by the user) at the end
|
||||
* of the previous session.
|
||||
*/
|
||||
userSet: 0 | 1;
|
||||
|
||||
/**
|
||||
* Denotes the left-position of the OSK at the end of the previous session if pinned.
|
||||
* Defaults to -1 if the value was undefined.
|
||||
*/
|
||||
left: number;
|
||||
|
||||
/**
|
||||
* Denotes the left-position of the OSK at the end of the previous session if pinned.
|
||||
* Defaults to -1 if the value was undefined.
|
||||
*/
|
||||
top: number;
|
||||
|
||||
/**
|
||||
* The previously-set OSK width.
|
||||
*/
|
||||
width?: number;
|
||||
|
||||
/**
|
||||
* The previously-set OSK height.
|
||||
*/
|
||||
height?: number;
|
||||
|
||||
/**
|
||||
* The version of KeymanWeb active when this cookie was generated.
|
||||
*/
|
||||
_version: string;
|
||||
}
|
||||
|
||||
export class FloatingOSKCookieSerializer extends CookieSerializer<Required<FloatingOSKCookie>> {
|
||||
constructor() {
|
||||
super('KeymanWeb_OnScreenKeyboard');
|
||||
}
|
||||
|
||||
loadWithDefaults(defaults: Required<FloatingOSKCookie>) {
|
||||
return {...defaults, ...this.load()};
|
||||
}
|
||||
|
||||
load() {
|
||||
const cookie = super.load((value, key) => {
|
||||
switch(key) {
|
||||
case 'version':
|
||||
return value;
|
||||
default:
|
||||
return Number.parseInt(value, 10);
|
||||
}
|
||||
});
|
||||
|
||||
if(!cookie.width) {
|
||||
delete cookie.width; // in case of a '' entry.
|
||||
}
|
||||
if(!cookie.height) {
|
||||
delete cookie.height; // in case of a '' entry.
|
||||
}
|
||||
|
||||
return cookie;
|
||||
}
|
||||
|
||||
save(cookie: Required<FloatingOSKCookie>) {
|
||||
super.save(cookie);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import MouseDragOperation from '../input/mouseDragOperation.js';
|
|||
import { getViewportScale } from '../screenUtils.js';
|
||||
import Configuration from '../config/viewConfiguration.js';
|
||||
import TwoStateActivator from './twoStateActivator.js';
|
||||
import FloatingOSKCookie from './floatingOskCookie.js';
|
||||
import { FloatingOSKCookie, FloatingOSKCookieSerializer } from './floatingOskCookie.js';
|
||||
|
||||
/***
|
||||
KeymanWeb 10.0
|
||||
|
|
@ -18,12 +18,6 @@ import FloatingOSKCookie from './floatingOskCookie.js';
|
|||
|
||||
interface FloatingOSKViewConfiguration extends Configuration {
|
||||
activator: TwoStateActivator<HTMLElement>;
|
||||
|
||||
// Designed to replace util.saveCookie() within the OSK subproject.
|
||||
saveViewLayout?: (data: FloatingOSKCookie) => void;
|
||||
|
||||
// Designed to replace util.loadCookie() within the OSK subproject.
|
||||
reloadViewLayout?: () => FloatingOSKCookie;
|
||||
}
|
||||
|
||||
export default class FloatingOSKView extends OSKView {
|
||||
|
|
@ -36,6 +30,8 @@ export default class FloatingOSKView extends OSKView {
|
|||
dfltX: string;
|
||||
dfltY: string;
|
||||
|
||||
layoutSerializer = new FloatingOSKCookieSerializer();
|
||||
|
||||
private titleBar: TitleBar;
|
||||
private resizeBar: ResizeBar;
|
||||
|
||||
|
|
@ -46,20 +42,12 @@ export default class FloatingOSKView extends OSKView {
|
|||
public constructor(config: FloatingOSKViewConfiguration) {
|
||||
config.activator = config.activator || new TwoStateActivator<HTMLElement>();
|
||||
|
||||
config.saveViewLayout = config.saveViewLayout || (() => {});
|
||||
|
||||
config.reloadViewLayout = config.reloadViewLayout || (() => {
|
||||
return {} as unknown as FloatingOSKCookie;
|
||||
});
|
||||
|
||||
super(config);
|
||||
|
||||
this.typedActivationModel.on('triggerChange', () => this.setDisplayPositioning());
|
||||
|
||||
document.body.appendChild(this._Box);
|
||||
|
||||
this.loadCookie();
|
||||
|
||||
// Add header element to OSK only for desktop browsers
|
||||
this.titleBar = new TitleBar(this.titleDragHandler);
|
||||
this.titleBar.on('help', () => {
|
||||
|
|
@ -77,6 +65,8 @@ export default class FloatingOSKView extends OSKView {
|
|||
this.resizeBar.on('showBuild', () => this.emit('showBuild'));
|
||||
|
||||
this.headerView = this.titleBar;
|
||||
|
||||
this.loadPersistedLayout();
|
||||
}
|
||||
|
||||
private get typedActivationModel(): TwoStateActivator<HTMLElement> {
|
||||
|
|
@ -120,7 +110,7 @@ export default class FloatingOSKView extends OSKView {
|
|||
this.footerView = null;
|
||||
}
|
||||
|
||||
this.loadCookie();
|
||||
this.loadPersistedLayout();
|
||||
this.setNeedsLayout();
|
||||
}
|
||||
|
||||
|
|
@ -137,13 +127,13 @@ export default class FloatingOSKView extends OSKView {
|
|||
let dragPromise = new ManagedPromise<void>();
|
||||
this.emit('dragMove', dragPromise.corePromise);
|
||||
|
||||
this.loadCookie();
|
||||
this.loadPersistedLayout();
|
||||
this.userPositioned=false;
|
||||
if(!keepDefaultPosition) {
|
||||
delete this.dfltX;
|
||||
delete this.dfltY;
|
||||
}
|
||||
this.saveCookie();
|
||||
this.savePersistedLayout();
|
||||
|
||||
if(isVisible) {
|
||||
this.present();
|
||||
|
|
@ -178,24 +168,23 @@ export default class FloatingOSKView extends OSKView {
|
|||
/**
|
||||
* Save size, position, font size and visibility of OSK
|
||||
*/
|
||||
saveCookie() {
|
||||
private savePersistedLayout() {
|
||||
var p = this.getPos();
|
||||
|
||||
const c: FloatingOSKCookie = {
|
||||
visible: this.displayIfActive ? '1' : '0',
|
||||
userSet: this.userPositioned ? '1' : '0',
|
||||
left: '' + p.left,
|
||||
top: '' + p.top,
|
||||
visible: this.displayIfActive ? 1 : 0,
|
||||
userSet: this.userPositioned ? 1 : 0,
|
||||
left: p.left,
|
||||
top: p.top,
|
||||
_version: Version.CURRENT.toString()
|
||||
}
|
||||
|
||||
if(this.vkbd) {
|
||||
c['width'] = '' + this.width.val;
|
||||
c['height'] = '' + this.height.val;
|
||||
c.width = this.width.val;
|
||||
c.height = this.height.val;
|
||||
}
|
||||
|
||||
const typedConfiguration = this.configuration as FloatingOSKViewConfiguration;
|
||||
typedConfiguration.saveViewLayout(c);
|
||||
this.layoutSerializer.save(c as Required<FloatingOSKCookie>);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -203,30 +192,27 @@ export default class FloatingOSKView extends OSKView {
|
|||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
loadCookie(): void {
|
||||
function parseIntWithDefault(str: string, fallback: number) {
|
||||
let val = Number.parseInt(str, 10);
|
||||
return isNaN(val) ? fallback: val;
|
||||
}
|
||||
private loadPersistedLayout(): void {
|
||||
let c = this.layoutSerializer.loadWithDefaults({
|
||||
visible: 1,
|
||||
userSet: 0,
|
||||
left: -1,
|
||||
top: -1,
|
||||
_version: undefined,
|
||||
width: 0.3*screen.width,
|
||||
height: 0.15*screen.height
|
||||
});
|
||||
|
||||
const typedConfiguration = this.configuration as FloatingOSKViewConfiguration;
|
||||
var c: FloatingOSKCookie = typedConfiguration.reloadViewLayout();
|
||||
|
||||
this.activationModel.enabled = parseIntWithDefault(c['visible'], 1) == 1;
|
||||
this.userPositioned = parseIntWithDefault(c['userSet'], 0) == 1;
|
||||
this.x = parseIntWithDefault(c['left'],-1);
|
||||
this.y = parseIntWithDefault(c['top'],-1);
|
||||
let cookieVersionString = c['_version'];
|
||||
this.activationModel.enabled = c.visible == 1;
|
||||
this.userPositioned = c.userSet == 1;
|
||||
this.x = c.left;
|
||||
this.y = c.top;
|
||||
const cookieVersionString = c._version;
|
||||
|
||||
// Restore OSK size - font size now fixed in relation to OSK height, unless overridden (in em) by keyboard
|
||||
let dfltWidth=0.3*screen.width;
|
||||
let dfltHeight=0.15*screen.height;
|
||||
|
||||
let newWidth = parseInt(c['width'], 10);
|
||||
let newHeight = parseInt(c['height'], 10);
|
||||
let isNewCookie = isNaN(newHeight);
|
||||
newWidth = isNaN(newWidth) ? dfltWidth : newWidth;
|
||||
newHeight = isNaN(newHeight) ? dfltHeight : newHeight;
|
||||
const isNewCookie = cookieVersionString === undefined;
|
||||
let newWidth = c.width;
|
||||
let newHeight = c.height;
|
||||
|
||||
// Limit the OSK dimensions to reasonable values
|
||||
if(newWidth < 0.2*screen.width) {
|
||||
|
|
@ -420,7 +406,7 @@ export default class FloatingOSKView extends OSKView {
|
|||
this.movementEnabled = !this.noDrag;
|
||||
}
|
||||
// Save the user-defined OSK size
|
||||
this.saveCookie();
|
||||
this.savePersistedLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -553,7 +539,7 @@ export default class FloatingOSKView extends OSKView {
|
|||
super.startHide(hiddenByUser);
|
||||
|
||||
if(hiddenByUser) {
|
||||
this.saveCookie(); // Save current OSK state, size and position (desktop only)
|
||||
this.savePersistedLayout(); // Save current OSK state, size and position (desktop only)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -563,7 +549,7 @@ export default class FloatingOSKView extends OSKView {
|
|||
} else {
|
||||
super['show']();
|
||||
}
|
||||
this.saveCookie();
|
||||
this.savePersistedLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -665,7 +651,7 @@ export default class FloatingOSKView extends OSKView {
|
|||
this.dragPromise.then(() => {
|
||||
_this.userPositioned = true;
|
||||
_this.doResizeMove();
|
||||
_this.saveCookie();
|
||||
_this.savePersistedLayout();
|
||||
});
|
||||
this.dragPromise = null;
|
||||
}
|
||||
|
|
@ -743,7 +729,7 @@ export default class FloatingOSKView extends OSKView {
|
|||
// Remainder should be done after anything else pending on the Promise.
|
||||
this.dragPromise.then(() => {
|
||||
_this.doResizeMove();
|
||||
_this.saveCookie();
|
||||
_this.savePersistedLayout();
|
||||
});
|
||||
this.dragPromise = null;
|
||||
}
|
||||
|
|
|
|||
102
web/src/test/auto/dom/cases/dom-utils/cookies.js
Normal file
102
web/src/test/auto/dom/cases/dom-utils/cookies.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import Device from '/@keymanapp/keyman/build/engine/device-detect/lib/index.mjs';
|
||||
import { default as CookieSerializer } from '/@keymanapp/keyman/build/engine/dom-utils/obj/cookieSerializer.js';
|
||||
|
||||
let assert = chai.assert;
|
||||
|
||||
const device = new Device();
|
||||
device.detect();
|
||||
|
||||
const RESET="max-age=0";
|
||||
|
||||
describe('CookieSerializer', function () {
|
||||
describe('SimpleTestCookie', () => {
|
||||
const COOKIE_ID = "SimpleTestCookie";
|
||||
|
||||
beforeEach(() => {
|
||||
// Purge the cookie!
|
||||
document.cookie = `${COOKIE_ID}=foobar; ${RESET}`;
|
||||
|
||||
const cookieLoader = new CookieSerializer(COOKIE_ID);
|
||||
assert.deepEqual(cookieLoader.load(), {});
|
||||
});
|
||||
|
||||
it('serializes & reloads when all values are strings', () => {
|
||||
const cookieWriter = new CookieSerializer(COOKIE_ID);
|
||||
const obj = {
|
||||
foo: 'bar',
|
||||
widget: 'sprog',
|
||||
fruity: 'tooty',
|
||||
flagProp: ''
|
||||
}
|
||||
cookieWriter.save(obj);
|
||||
|
||||
const cookieReader = new CookieSerializer(COOKIE_ID);
|
||||
const reloadedObj = cookieReader.load();
|
||||
|
||||
assert.deepEqual(reloadedObj, obj);
|
||||
assert.notStrictEqual(reloadedObj, obj);
|
||||
});
|
||||
|
||||
it('serializes all values to strings', () => {
|
||||
const cookieWriter = new CookieSerializer(COOKIE_ID);
|
||||
const obj = {
|
||||
foo: 'bar',
|
||||
two: 2,
|
||||
true: true
|
||||
}
|
||||
const expectedObj = {
|
||||
foo: 'bar',
|
||||
two: `${2}`,
|
||||
true: `${true}`
|
||||
}
|
||||
cookieWriter.save(obj);
|
||||
|
||||
const cookieReader = new CookieSerializer(COOKIE_ID);
|
||||
const reloadedObj = cookieReader.load();
|
||||
|
||||
assert.deepEqual(reloadedObj, expectedObj);
|
||||
assert.notStrictEqual(reloadedObj, expectedObj);
|
||||
});
|
||||
|
||||
it('accepts custom deserialization', () => {
|
||||
const cookieWriter = new CookieSerializer(COOKIE_ID);
|
||||
const obj = {
|
||||
foo: 'bar',
|
||||
two: 2,
|
||||
true: true
|
||||
}
|
||||
cookieWriter.save(obj);
|
||||
|
||||
const cookieReader = new CookieSerializer(COOKIE_ID);
|
||||
const reloadedObj = cookieReader.load((value, key) => {
|
||||
switch(key) {
|
||||
case 'two':
|
||||
return Number.parseInt(value, 10);
|
||||
case 'true':
|
||||
return value === 'true';
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
assert.deepEqual(reloadedObj, obj);
|
||||
assert.notStrictEqual(reloadedObj, obj);
|
||||
});
|
||||
|
||||
it('works with encodeURIComponent + decodeURIComponent', () => {
|
||||
const cookieWriter = new CookieSerializer(COOKIE_ID);
|
||||
const obj = {
|
||||
foo: 'bar',
|
||||
symbols: ':;+&?@ =\n', // Stuff that'll royally wreck cookies if not encoded.
|
||||
star: '⭐' // Sure, why not test with an emoji?
|
||||
}
|
||||
cookieWriter.save(obj, encodeURIComponent);
|
||||
|
||||
const cookieReader = new CookieSerializer(COOKIE_ID);
|
||||
const reloadedObj = cookieReader.load(decodeURIComponent);
|
||||
|
||||
assert.deepEqual(reloadedObj, obj);
|
||||
assert.notStrictEqual(reloadedObj, obj);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue