VulpesOS/gaia/apps/keyboard/js/imes/jszhuyin/jszhuyin.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

268 lines
6.9 KiB
JavaScript
Executable file

/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* global KeyboardEvent, KeyEvent, InputMethods */
'use strict';
/*
* This file implements a "glue" between FxOS Keyboard app UI and
* the JSZhuyin library.
*/
var JSZhuyinGaiaKeyboardGlue = function JSZhuyinGaiaKeyboardGlue() {
this.callbacks = null;
this.worker = null;
this.composing = false;
this.hasCandidate = false;
this.workerReady = false;
this.messageQueue = [];
this.requestId = 0;
this.handledId = 0;
this.unloadWorkerTimer = undefined;
};
JSZhuyinGaiaKeyboardGlue.prototype.kDEBUG = false;
JSZhuyinGaiaKeyboardGlue.prototype.DEACTIVATE_TIME = 60000; // 1 minute
JSZhuyinGaiaKeyboardGlue.prototype.init = function jszg_init(callbacks) {
if (this.kDEBUG) {
console.log('zhuyin:init:' + Date.now());
}
this.callbacks = callbacks;
};
JSZhuyinGaiaKeyboardGlue.prototype.uninit = function jszg_uninit() {
if (this.kDEBUG) {
console.log('zhuyin:uninit:' + Date.now());
}
this._unloadWorker();
this.callbacks = null;
};
JSZhuyinGaiaKeyboardGlue.prototype._loadWorker = function jszg_loadWorker() {
if (this.kDEBUG) {
console.log('zhuyin:loadWorker:' + Date.now());
}
if (this.worker) {
return;
}
var worker = this.worker = new Worker(this.callbacks.path + '/lib/worker.js');
worker.addEventListener('error', this);
worker.addEventListener('message', this);
worker.postMessage({ 'type': 'load' });
};
JSZhuyinGaiaKeyboardGlue.prototype._unloadWorker =
function jszg_unloadWorker() {
if (this.kDEBUG) {
console.log('zhuyin:unloadWorker:' + Date.now());
}
if (!this.worker) {
return;
}
var worker = this.worker;
worker.removeEventListener('error', this);
worker.removeEventListener('message', this);
worker.terminate();
this.worker = null;
this.composing = false;
this.hasCandidate = false;
this.workerReady = false;
this.messageQueue = [];
this.requestId = 0;
this.handledId = 0;
};
JSZhuyinGaiaKeyboardGlue.prototype.click = function jszg_click(code) {
// Let's not use BopomofoEncoder.isBopomofoSymbol(code) here
var BOPOMOFO_START = 0x3105;
var BOPOMOFO_END = 0x3129;
var BOPOMOFO_TONE_1 = 0x02c9;
var BOPOMOFO_TONE_2 = 0x02ca;
var BOPOMOFO_TONE_3 = 0x02c7;
var BOPOMOFO_TONE_4 = 0x02cb;
var BOPOMOFO_TONE_5 = 0x02d9;
// We must handle Bopomofo symbols.
if (code >= BOPOMOFO_START && code <= BOPOMOFO_END ||
code === BOPOMOFO_TONE_1 || code === BOPOMOFO_TONE_2 ||
code === BOPOMOFO_TONE_3 || code === BOPOMOFO_TONE_4 ||
code === BOPOMOFO_TONE_5) {
this.sendMessage('handleKeyEvent', code, ++this.requestId);
return;
}
// Send BOPOMOFO_TONE_1 for the SPACE key.
if (this.composing && code === KeyboardEvent.DOM_VK_SPACE) {
this.sendMessage('handleKeyEvent', BOPOMOFO_TONE_1, ++this.requestId);
return;
}
// Handle the key anyway if we might be currently composing.
if (this.composing || this.requestId !== this.handledId) {
this.sendMessage('handleKeyEvent', code, ++this.requestId);
return;
}
// Before send the key out, remove suggestions.
if (this.hasCandidate) {
this.hasCandidate = false;
this.callbacks.sendCandidates([]);
}
// Not handling the key; send it out directly.
this.callbacks.sendKey(code);
};
JSZhuyinGaiaKeyboardGlue.prototype.select = function jszg_select(text, data) {
this.sendMessage('selectCandidate', [text, data], ++this.requestId);
};
JSZhuyinGaiaKeyboardGlue.prototype.selectionChange = function(detail) {
if (detail.ownAction) {
return;
}
this.empty();
};
JSZhuyinGaiaKeyboardGlue.prototype.activate = function jszg_activate() {
if (this.kDEBUG) {
console.log('zhuyin:activate:' + Date.now());
}
if (!this.worker) {
this._loadWorker();
}
clearTimeout(this.unloadWorkerTimer);
this.empty();
};
JSZhuyinGaiaKeyboardGlue.prototype.deactivate = function jszg_deactivate() {
if (this.kDEBUG) {
console.log('zhuyin:deactivate:' + Date.now());
}
this.empty();
clearTimeout(this.unloadWorkerTimer);
this.unloadWorkerTimer =
setTimeout(this._unloadWorker.bind(this), this.DEACTIVATE_TIME);
};
JSZhuyinGaiaKeyboardGlue.prototype.empty = function jszg_empty() {
// Simply send escape.
if (this.composing) {
this.sendMessage('handleKeyEvent', KeyEvent.DOM_VK_ESCAPE,
++this.requestId);
}
};
JSZhuyinGaiaKeyboardGlue.prototype.handleEvent =
function jszg_handleEvent(evt) {
switch (evt.type) {
case 'error':
throw 'JSZhuyinGaiaKeyboardGlue: Worker error.';
case 'message':
this.handleMessage(evt.data);
break;
}
};
JSZhuyinGaiaKeyboardGlue.prototype.handleMessage =
function jszg_handleMessage(msg) {
var data = msg.data;
if (this.kDEBUG) {
if (Array.isArray(data)) {
console.log('zhuyin:msg:' + Date.now() +
':' + msg.type + ':' + data.length);
} else {
console.log('zhuyin:msg:' + Date.now() + ':' + msg.type + ':' + data);
}
}
switch (msg.type) {
case 'actionhandled':
this.handledId = data;
break;
case 'loadend':
this.workerReady = true;
var message;
while ((message = this.messageQueue.shift())) {
this.sendMessage.apply(this, message);
}
this.messageQueue = undefined;
break;
case 'candidateschange':
this.hasCandidate = data && !!data.length;
this.callbacks.sendCandidates(data);
break;
case 'compositionupdate':
if (data) {
// send compositionstart event
this.callbacks.setComposition(data);
} else {
// send compositionend event if we are indeed is composing.
// (do nothing if we aren't)
if (this.composing) {
this.callbacks.endComposition();
}
}
this.composing = !!data;
break;
case 'compositionend':
// We must manually start the composition
// (i.e. send compositionstart event) first here if we haven't.
if (!this.composing) {
this.callbacks.setComposition(data);
}
// Send compositionend event.
this.callbacks.endComposition(data);
this.composing = false;
break;
}
};
JSZhuyinGaiaKeyboardGlue.prototype.sendMessage =
function jszg_sendMessage(type, data, reqId) {
if (!this.worker) {
throw 'JSZhuyinGaiaKeyboardGlue:' +
' attempt to send message w/o load the worker first.';
}
if (!this.workerReady) {
this.messageQueue.push([type, data, reqId]);
return;
}
this.worker.postMessage(
{ 'type': type, 'data': data, 'requestId': reqId });
};
// Expose the engine to the Gaia keyboard
if (typeof InputMethods !== 'undefined') {
InputMethods.jszhuyin = new JSZhuyinGaiaKeyboardGlue();
}