'use strict'; /* global BopomofoEncoder, DataLoader, JSZhuyinDataPackStorage, CacheStore */ /** * A queue to run one action at a time. * @this {object} ActionQueue instance. */ var ActionQueue = function ActionQueue() { this.pendingActions = []; this.waiting = false; }; /** * Function to call to run each action. * @type {function} */ ActionQueue.prototype.handle = null; /** * Queue and action. The arguments will be kept. * @this {object} ActionQueue instance. */ ActionQueue.prototype.queue = function aq_queue(type, data, reqId) { if (this.waiting) { this.pendingActions.push([type, data, reqId]); return; } this.waiting = true; this.handle(type, data, reqId); }; /** * handle() are suppose to call done() when it finishes. * @this {object} ActionQueue instance. */ ActionQueue.prototype.done = function aq_done() { if (!this.waiting) { throw 'Calling queue.done() when we are not waiting.'; } var args = this.pendingActions.shift(); if (!args) { this.waiting = false; return; } this.handle.apply(this, args); }; var JSZhuyinCandidateMetadata = function() { this.dataMap = new Map(); // Start with a number other than any obvious ones to remove the meaning of // this incremental ID. this.nextId = 42; this.NULL_DATA = [0, 0]; }; JSZhuyinCandidateMetadata.prototype.NULL_ID = 0; JSZhuyinCandidateMetadata.prototype.saveData = function jcm_saveData(encodedSoundsLength, index) { this.dataMap.set(this.nextId, [ encodedSoundsLength, index ]); return this.nextId++; }; JSZhuyinCandidateMetadata.prototype.getData = function(id) { if (id === this.NULL_ID) { return this.NULL_DATA; } var data = this.dataMap.get(id); if (!data) { throw new Error('JSZhuyinCandidateMetadata: ' + 'Inexistent or outdated candidate.'); } return data; }; JSZhuyinCandidateMetadata.prototype.clear = function() { this.dataMap.clear(); }; var JSZhuyinQueryData = function(symbols, longestPhraseLength) { this.symbols = symbols; this.expendedEncodedSounds = BopomofoEncoder.encodeExpended(this.symbols); this.longestPhraseLength = longestPhraseLength; this._trimmedSymbols = undefined; this._trimmedSymbolsCombinations = undefined; }; JSZhuyinQueryData.prototype.getTrimmedSymbols = function() { if (this._trimmedSymbols) { return this._trimmedSymbols; } return (this._trimmedSymbols = BopomofoEncoder.trimToLength(this.symbols, this.longestPhraseLength)); }; JSZhuyinQueryData.prototype.getTrimmedSymbolsCombinations = function() { if (this._trimmedSymbolsCombinations) { return this._trimmedSymbolsCombinations; } return (this._trimmedSymbolsCombinations = BopomofoEncoder.getSymbolCombinations(this.getTrimmedSymbols())); }; var JSZhuyinComposedCandidatesBuilder = function JSZhuyinComposedCandidatesBuilder() { this.cache = null; }; JSZhuyinComposedCandidatesBuilder.prototype.LONGEST_PHRASE_LENGTH = undefined; JSZhuyinComposedCandidatesBuilder.prototype.load = function(storage) { this.storage = storage; this.cache = new CacheStore(); }; JSZhuyinComposedCandidatesBuilder.prototype.unload = function() { this.cache = null; this.storage = null; }; JSZhuyinComposedCandidatesBuilder.prototype.addToCache = function(expendedEncodedSounds, data) { this.cache.add(expendedEncodedSounds, data); return data; }; JSZhuyinComposedCandidatesBuilder.prototype.cleanupCache = function(supersetCodes) { this.cache.cleanup(supersetCodes); }; JSZhuyinComposedCandidatesBuilder.prototype.getComposedCandidates = function(queryData) { var expendedEncodedSounds = queryData.expendedEncodedSounds; // The optimization here is based on the fact that the newly appended symbols // always forms compositions beginning with the previous results. // Remaining compositions with lower scores cannot possibly results // in higher score so it does not make sense to calculate them all and // rank them for every new symbols. Rather, it make more sense to cache the // results and only create possible compositions and results from them and // rank them. // The |previousFirstResults| array contains JSZhuyinCachedResult instances // from the cache, or, if the sliced expended encoded sounds has not be // calculated, we would calculate it here and put it into the cache. // // The final result will be the matched first composed result of the // unsliced expended encoded sound. // // Note that the result data of the final result and in the cache are both // calculated with |_getFirstComposedResult()|. var previousFirstResults = [ undefined ]; var lastComposedResultData; var i = 0; var n = expendedEncodedSounds.length; var slicedExpendedEncodedSounds; // This loop will ensure previousFirstResults is populated with // results from length = 1 to n, where length = n will happen to be the one // result we would need to output. while (++i <= n) { slicedExpendedEncodedSounds = expendedEncodedSounds.slice(0, i); lastComposedResultData = this.cache.get(slicedExpendedEncodedSounds); // Empty result will be cached as a |null|. A "not found" result is // explicitly an undefined. if (lastComposedResultData === undefined) { lastComposedResultData = this._getFirstComposedResult( slicedExpendedEncodedSounds, previousFirstResults); this.addToCache(slicedExpendedEncodedSounds, lastComposedResultData); } previousFirstResults[i] = lastComposedResultData; } return lastComposedResultData; }; JSZhuyinComposedCandidatesBuilder.prototype._getFirstComposedResult = function(expendedEncodedSounds, previousFirstResults) { var storage = this.storage; // We would only need to worry about an element in previousFirstResults // follow by exactly one phrase, instead of getting all possible // compositions, because it will always be ruled out. var composedResultDataArr = previousFirstResults .reduce(function(composedResultDataArr, previousFirstResult, i) { if (i !== 0 && previousFirstResult === null) { return composedResultDataArr; } BopomofoEncoder.getSymbolCombinations(expendedEncodedSounds.slice(i)) .filter(function(symbolCodes) { return (symbolCodes.length <= this.LONGEST_PHRASE_LENGTH); }.bind(this)) .map(function(symbolCodes) { return [storage.getIncompleteMatched(symbolCodes), symbolCodes]; }) .filter(function(symbolCodesResultData) { return !!symbolCodesResultData[0]; }) .map(function(symbolCodesResultData) { var symbolCodesFirstResult = symbolCodesResultData[0].getFirstResult(); var composedResultData; if (i !== 0) { composedResultData = [].concat(previousFirstResult); composedResultData[0] += symbolCodesFirstResult.str; composedResultData[1] += symbolCodesFirstResult.score; composedResultData[2] = symbolCodesFirstResult.index; } else { composedResultData = [ symbolCodesFirstResult.str, symbolCodesFirstResult.score, symbolCodesFirstResult.index, [ symbolCodesFirstResult.str, BopomofoEncoder.decode(symbolCodesResultData[1]).length ] ]; } return composedResultData; }) .forEach(function(composedResultData) { composedResultDataArr.push(composedResultData); }); return composedResultDataArr; }.bind(this), /* composedResultDataArr */ []) .sort(function(a, b) { if (b[1] > a[1]) { return 1; } if (b[1] < a[1]) { return -1; } if (b[0] > a[0]) { return 1; } if (b[0] < a[0]) { return -1; } return 0; }); return composedResultDataArr[0] || null; }; var JSZhuyinPartialMatchingCandidatesBuilder = function() { }; JSZhuyinPartialMatchingCandidatesBuilder.prototype.load = function(storage) { this.storage = storage; }; JSZhuyinPartialMatchingCandidatesBuilder.prototype.unload = function() { this.cache = null; }; JSZhuyinPartialMatchingCandidatesBuilder.prototype.getCandidates = function(queryData) { var storage = this.storage; var insertFullyMatched = (queryData.getTrimmedSymbols() !== queryData.symbols); var dataPackResultDataArr = queryData.getTrimmedSymbolsCombinations() .map(function(symbolCodes) { var symbolCodeDataArr = []; var arr, symbolLength; var i = -1; var n = insertFullyMatched ? symbolCodes.length : symbolCodes.length - 1; while (i++ < n) { arr = symbolCodes.slice(0, i); symbolLength = BopomofoEncoder.decode(arr).length; symbolCodeDataArr.push([symbolLength, arr]); } return symbolCodeDataArr; }) .reduce(function(orderedSymbolCodeArrs, symbolCodeDataArr) { symbolCodeDataArr.forEach(function(symbolCodeData) { if (!orderedSymbolCodeArrs[symbolCodeData[0]]) { orderedSymbolCodeArrs[symbolCodeData[0]] = []; } orderedSymbolCodeArrs[symbolCodeData[0]].push(symbolCodeData[1]); }); return orderedSymbolCodeArrs; }, /* orderedSymbolCodeArrs */ []) .map(function(symbolCodeArrs, i) { var allDataPackResult = symbolCodeArrs .map(function(symbolCodes) { if (!i || !storage.getIncompleteMatched(symbolCodes)) { return null; } return storage.getIncompleteMatched(symbolCodes).getResults(); }) .filter(function(dataPackResult) { return dataPackResult !== null; }) .reduce(function(allDataPackResult, dataPackResult) { return allDataPackResult.concat(dataPackResult); }, /* allDataPackResult */ []) .sort(function(a, b) { if (b.score > a.score) { return 1; } if (b.score < a.score) { return -1; } if (b.str > a.str) { return 1; } if (b.str < a.str) { return -1; } return 0; }); return [i, allDataPackResult]; }) .reverse(); return dataPackResultDataArr; }; /** * The main IME logic. * @this {object} JSZhuyin instance. */ var JSZhuyin = function JSZhuyin() { this.storage = null; this.symbols = ''; this.confirmedPartIndex = 0; this.confirmedCharacters = ''; this.defaultCandidate = undefined; this.composedCandidatesBuilder = new JSZhuyinComposedCandidatesBuilder(); this.partiallyMatchedCandidatesBuilder = new JSZhuyinPartialMatchingCandidatesBuilder(); this.overflowCandidateString = ''; this.overflowCandidateSymbolLength = 0; this.queue = null; this.candidateMetadata = null; }; /** * Limit the length of the symbols in the compositions. * @type {number} */ JSZhuyin.prototype.MAX_SOUNDS_LENGTH = 48; /** * Longest possible phrase in the database, any longer than this will not be * matched * @type {Number} */ JSZhuyin.prototype.LONGEST_PHRASE_LENGTH = 6; /** * Suggest phrases after confirming characters. * @type {boolean} */ JSZhuyin.prototype.SUGGEST_PHRASES = true; /** * Allow re-order of symbol input. * Better error-handling for typing with hardware keyboard. */ JSZhuyin.prototype.REORDER_SYMBOLS = false; /** * When searching for matching words/phrases, consider these pairs of symbols * are interchangables. * Must be a string representing 2n sounds in Bopomofo symbols. * * Example string: 'ㄣㄥㄌㄖㄨㄛㄡ', making ㄣ interchangable with ㄥ and * ㄌ interchangable with ㄖ, and ㄨㄛ with ㄡ. * @type {string} */ JSZhuyin.prototype.INTERCHANGABLE_PAIRS = ''; /** * Overwritten path of database file. * @type {string} */ JSZhuyin.prototype.dataURL = ''; /** * Run when the loading is complete. * @type {function} */ JSZhuyin.prototype.onloadend = null; /** * Run when loading is successful. * @type {function} */ JSZhuyin.prototype.onload = null; /** * Run when unload. * @type {function} */ JSZhuyin.prototype.onunload = null; /** * Run when error occours. * @type {function} */ JSZhuyin.prototype.onerror = null; /** * Run when an action is handled; receives reqId passed to the functions. * @type {function} */ JSZhuyin.prototype.onactionhandled = null; /** * Callback to call when the composition updates. * @type {function} */ JSZhuyin.prototype.oncompositionupdate = null; /** * Callback to call when the composition ends. * @type {function} */ JSZhuyin.prototype.oncompositionend = null; /** * Callback to call when candidate menu updates. * @type {function} */ JSZhuyin.prototype.oncandidateschange = null; /** * Handle a key with it's DOM UI Event Level 3 key value. * @param {string} key The key property of the key to handle, * should be the printable character or a registered * name in the spec. * @param {any} reqId ID of the request. * @return {boolean} Return true if the key will be handled async. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.handleKey = function jz_handleKey(key, reqId) { if (!this.queue) { throw 'JSZhuyin: You need to load() first.'; } if (typeof key !== 'string') { throw 'JSZhuyin: key passed to handleKey must be a string.'; } if (BopomofoEncoder.isBopomofoSymbol(key)) { // We must handle Bopomofo symbols. this.queue.queue('key', key, reqId); return true; } if (this.defaultCandidate || this.symbols) { // We must handle all the keys if there are pending symbols or candidates. this.queue.queue('key', key, reqId); return true; } return false; }; /** * Handle a key event with it's keyCode or charCode. Deprecated. * @param {number} code charCode of the keyboard event. * If charCode is 0, you should pass keyCode instead. * @param {any} reqId ID of the request. * @return {boolean} Return true if the key will be handled async. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.handleKeyEvent = function jz_handleKeyEvent(code, reqId) { var key; switch (code) { case 0x08: key = 'Backspace'; break; case 0x0d: key = 'Enter'; break; case 0x1b: key = 'Escape'; break; default: // XXX: We are considering everything reach here is a printable character. key = String.fromCharCode(code); } return this.handleKey(key, reqId); }; /** * Select a candidate. Will be handled in the action queue. * @param {object} candidate One of the candidate that was sent via * oncandidateschange callback. * @param {any} reqId ID of the request. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.selectCandidate = function jz_selCandi(candidate, reqId) { if (!Array.isArray(candidate) || typeof candidate[0] !== 'string' || typeof candidate[1] !== 'number') { throw new Error('JSZhuyin: ' + 'malformed candidate object in selectCandidate call.'); } this.queue.queue('candidateSelection', candidate, reqId); }; /** * Load JSZhuyin; loading the database and register callbacks, etc. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.load = function jz_load(data) { if (this.loaded) { throw 'Already loaded.'; } this.loaded = true; this.symbols = ''; this.defaultCandidate = undefined; this.queue = new ActionQueue(); this.queue.handle = this.handle.bind(this); this.storage = new JSZhuyinDataPackStorage(); this.candidateMetadata = new JSZhuyinCandidateMetadata(); this.composedCandidatesBuilder.load(this.storage); this.partiallyMatchedCandidatesBuilder.load(this.storage); if (data instanceof ArrayBuffer) { this.storage.load(data); if (typeof this.onload === 'function') { this.onload(); } if (typeof this.onloadend === 'function') { this.onloadend(); } } else { this.dataLoader = new DataLoader(); if (this.dataURL) { this.dataLoader.DATA_URL = this.dataURL; } this.dataLoader.onerror = function() { if (typeof this.onerror === 'function') { this.onerror(); } }.bind(this); this.dataLoader.onload = function() { this.storage.load(this.dataLoader.data); if (typeof this.onload === 'function') { this.onload(); } }.bind(this); this.dataLoader.onloadend = function() { if (typeof this.onloadend === 'function') { this.onloadend(); } }.bind(this); this.dataLoader.load(); } }; /** * Set configurations. * @param {object} config Configuration to set on JSZhuyin. */ JSZhuyin.prototype.setConfig = function(config) { for (var key in config) { this[key] = config[key]; } }; /** * Unload JSZhuyin. Close the database connection and purge things * from memory. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.unload = function jz_unload() { if (!this.loaded) { throw 'Already unloaded.'; } this.loaded = false; if (this.storage) { this.storage.unload(); this.storage = null; } if (this.dataLoader) { this.dataLoader = null; } if (this.composedCandidatesBuilder) { this.composedCandidatesBuilder.unload(); this.composedCandidatesBuilder = null; } if (this.partiallyMatchedCandidatesBuilder) { this.partiallyMatchedCandidatesBuilder.unload(); this.partiallyMatchedCandidatesBuilder = null; } this.symbols = ''; this.storage = null; this.defaultCandidate = undefined; this.queue.handle = null; this.queue = null; if (typeof this.onunload === 'function') { this.onunload(); } }; /** * Actual function to handle an action in the action queue. * You should not call this method directly. * @param {string} type A type keyword. * @param {any} data Data to handle for the action. * @param {any} reqId ID of the request. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.handle = function jz_handle(type, data, reqId) { switch (type) { case 'key': if (BopomofoEncoder.isBopomofoSymbol(data)) { var mode = this.REORDER_SYMBOLS ? BopomofoEncoder.APPEND_MODE_REORDER : BopomofoEncoder.APPEND_MODE_NONE; this.symbols = BopomofoEncoder.appendToSymbols(this.symbols, data, mode); this.updateComposition(reqId); this.query(reqId); break; } switch (data) { case 'Backspace': if (this.symbols.length === 0) { // Sliently discard the key here. Any meaningful response at // this stage would be throw the event back to the client, // which it would not be able to handle it either. this.sendActionHandled(reqId); this.queue.done(); break; } this.symbols = this.symbols.substr(0, this.symbols.length - 1); this.updateComposition(reqId); this.query(reqId); break; case 'Enter': if (!this.defaultCandidate) { // Sliently discard the key here. Any meaningful response at // this stage would be throw the event back to the client, // which it would not be able to handle it either. this.sendActionHandled(reqId); this.queue.done(); break; } this.confirmCandidate(this.defaultCandidate, reqId); break; case 'Escape': this.symbols = ''; this.updateComposition(reqId); this.query(reqId); break; default: // All other keys. // XXX: We could only handle it as if it's a printable character here. this.confirmCandidate( [this.defaultCandidate[0] + data, this.defaultCandidate[1]], reqId); break; } break; case 'candidateSelection': this.confirmCandidate(data, reqId); break; default: throw 'Unknown action type: ' + type; } }; /** * Run the query against the current symbols. * You should not call this method directly. * @param {any} reqId ID of the request. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.query = function jz_query(reqId) { if (this.symbols.length === 0) { this.candidateMetadata.clear(); this.updateCandidates([]); this.sendActionHandled(reqId); this.queue.done(); return; } // First, create the object that holds different forms of our current // symbols to query. var queryData = new JSZhuyinQueryData( this.symbols, this.LONGEST_PHRASE_LENGTH); // Confirming the overflow candidate if the lengths is too long. if (queryData.expendedEncodedSounds.length > this.MAX_SOUNDS_LENGTH) { this.endComposition(this.overflowCandidateString, reqId); this.symbols = this.symbols.substr(this.overflowCandidateSymbolLength); queryData = new JSZhuyinQueryData(this.symbols, this.LONGEST_PHRASE_LENGTH); this.updateComposition(reqId); } this.storage.setInterchangeablePairs(this.INTERCHANGABLE_PAIRS); // ==== START COMPOSING THE RESULT ==== var results = []; var storage = this.storage; this.overflowCandidateString = ''; this.overflowCandidateSymbolLength = 0; this.candidateMetadata.clear(); // ==== PART I: PHRASES ==== // List all the choices if the entire query happens to match to // phrases. results = this._insertFullyMatchingCandidates(results, queryData); // ==== PART II: COMPOSED RESULTS ==== // Insert one composed results, composed by the top-ranked phrases. results = this._insertFullyMatchingComposedCandidates(results, queryData); // ==== PART III: PHRASES THAT MATCHES SYMBOLS PARTIALLY ==== results = this._insertPartialMatchingCandidates(results, queryData); // ==== PART IV: UNFORTUNATE TYPO ==== // Lastly, append the symbols as the last candidate to give out a proper // feedback (if applicable). results = this._insertTypoHints(results, queryData.expendedEncodedSounds); this.updateCandidates(results); this.sendActionHandled(reqId); // Remove extra data in the composed result cache. this.composedCandidatesBuilder.cleanupCache(queryData.expendedEncodedSounds); // Flatten codes array of all single compositions for cache cleanning in // storage. // TODO: We don't really need to keep every sliced code for some sequences // that would always backed by composed result cache. var supersetCodes = BopomofoEncoder.getSymbolCombinations(this.symbols) .reduce(function(supersetCodes, symbolCodes) { return supersetCodes.concat(symbolCodes); }, /* supersetCodes */ []); // Remove cache entires from storage cache. storage.cleanupCache(supersetCodes); this.queue.done(); }; /** * Internal method for Part I of query(), fully matched candidates into the * result. * @param {array(array(string, number))} results Result candidates. * @param {object} queryData JSZhuyinQueryData * instance. * @return {array(array(string, number))} The result candidates array passed * (modified in-place). */ JSZhuyin.prototype._insertFullyMatchingCandidates = function(results, queryData) { if (queryData.getTrimmedSymbols() !== this.symbols) { return results; } var storage = this.storage; var expendedEncodedSounds = queryData.expendedEncodedSounds; var combinations = queryData.getTrimmedSymbolsCombinations(); combinations .map(function(symbolCodes) { // No need to search in the storage. if (symbolCodes.length > this.LONGEST_PHRASE_LENGTH) { return null; } return storage.getIncompleteMatched(symbolCodes); }.bind(this)) .reduce(function(resultsArr, dataPack) { if (!dataPack) { return resultsArr; } return resultsArr.concat(dataPack.getResults()); }, /* resultsArr */ []) .sort(function(a, b) { if (b.score > a.score) { return 1; } if (b.score < a.score) { return -1; } if (b.str > a.str) { return 1; } if (b.str < a.str) { return -1; } return 0; }) .forEach(function(result, i) { if (!this.overflowCandidateString) { this.overflowCandidateString = result.str; this.overflowCandidateSymbolLength = this.symbols.length; } if (i === 0) { this.composedCandidatesBuilder.addToCache( expendedEncodedSounds, [ result.str, result.index, result.score, [ result.str, this.symbols.length ] ]); } results.push([result.str, this.candidateMetadata.saveData(this.symbols.length, result.index)]); }.bind(this)); return results; }; /** * Internal method for Part I of query(), fully matched candidates into the * result. * @param {array(array(string, number))} results Result candidates. * @param {object} queryData JSZhuyinQueryData * instance. * @return {array(array(string, number))} The result candidates array passed * (modified in-place). */ JSZhuyin.prototype._insertFullyMatchingComposedCandidates = function(results, queryData) { // We don't need to compose any results if there is already results from // part I. // Assuming Part II reult is always inferior than Part I result? if (results.length) { return results; } this.composedCandidatesBuilder.LONGEST_PHRASE_LENGTH = this.LONGEST_PHRASE_LENGTH; var composedResultData = this.composedCandidatesBuilder.getComposedCandidates(queryData); if (!composedResultData) { return results; } if (!this.overflowCandidateString) { this.overflowCandidateString = composedResultData[3][0]; this.overflowCandidateSymbolLength = composedResultData[3][1]; } // XXX: suggest() not only needs the lastPartIndex but also the last // part of the confirmed characters to work. results.push([composedResultData[0], this.candidateMetadata.saveData( this.symbols.length, composedResultData[2])]); return results; }; /** * Internal method for Part III of query(), insert partially matched candidates * into results object * @param {array(array(string, number))} results Result candidates. * @param {object} queryData JSZhuyinQueryData * instance. * @return {array(array(string, number))} The result candidates array passed * (modified in-place). */ JSZhuyin.prototype._insertPartialMatchingCandidates = function(results, queryData) { var dataPackResultDataArr = this.partiallyMatchedCandidatesBuilder.getCandidates(queryData); dataPackResultDataArr .forEach(function(dataPackResultData) { var symbolLength = dataPackResultData[0]; dataPackResultData[1].forEach(function(result) { var isDuplication = [].concat(results).reverse() .some(function(previousResult) { return (previousResult[0] === result.str); }); if (isDuplication) { return; } if (results.length === 0) { // If there is still no candidate from any of the previous parts, // we could have to construct a candidate covering all symbols here, // to ensure there is enough feedback in the candidate panel when // user types. // // (Unlikely happen on real database though) results.push([ result.str + this.symbols.substr(symbolLength), this.candidateMetadata.saveData(this.symbols.length, 0) ]); } if (!this.overflowCandidateString) { this.overflowCandidateString = result.str; this.overflowCandidateSymbolLength = symbolLength; } var res = [result.str, this.candidateMetadata.saveData(symbolLength, result.index)]; results.push(res); }.bind(this)); }.bind(this)); return results; }; /** * Internal method for Part IV of query(), insert typo hints. * into results object * @param {array(array(string, number))} results Result * candidates. * @param {array(number)} expendedEncodedSounds Symbols encoded. * @return {array(array(string, number))} The result candidates array passed * (modified in-place). */ JSZhuyin.prototype._insertTypoHints = function(results, expendedEncodedSounds) { var storage = this.storage; // If there is still no candidate from any of the previous parts, // we would need to show the enitre string as-is. if (results.length === 0 && expendedEncodedSounds.length > 1) { results.push([ this.symbols, this.candidateMetadata.saveData(this.symbols.length, 0) ]); } // Show the first sound symbols if it doesn't match anything itself. // (not likely to happen to the real dataset) var str; if (!storage.getIncompleteMatched([expendedEncodedSounds[0]])) { str = BopomofoEncoder.decode([expendedEncodedSounds[0]]); results.push([str, this.candidateMetadata.saveData(str.length, 0)]); } str = BopomofoEncoder.decode([expendedEncodedSounds[0]]); if (!this.overflowCandidateString) { this.overflowCandidateString = str; this.overflowCandidateSymbolLength = str.length; } return results; }; /** * Suggest possible phrases after the user had enter a term. * @param {any} reqId ID of the request. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.suggest = function jz_suggest(reqId) { this.candidateMetadata.clear(); if (this.confirmedPartIndex === 0 || !this.SUGGEST_PHRASES) { this.updateCandidates([]); this.sendActionHandled(reqId); this.queue.done(); return; } var suggests = []; var confirmedCharactersLength = this.confirmedCharacters.length; var results = this.storage.getRangeFromContentIndex(this.confirmedPartIndex); results.forEach(function each_suggest(dataPack) { var dataPackResults = dataPack.getResultsBeginsWith(this.confirmedCharacters); dataPackResults.forEach(function each_result(dataPackResult) { // Don't push duplicate entries. var found = suggests.some(function finddup(suggest) { return (dataPackResult.str === suggest.str); }); if (!found) { suggests.push(dataPackResult); } }); }.bind(this)); var candidates = []; suggests.sort(function sort_suggests(a, b) { if (b.score > a.score) { return 1; } if (b.score < a.score) { return -1; } if (b.str > a.str) { return 1; } if (b.str < a.str) { return -1; } return 0; }).forEach(function each_suggests(suggests) { candidates.push([ suggests.str.substr(confirmedCharactersLength), this.candidateMetadata.NULL_ID]); }, this); this.updateCandidates(candidates); this.sendActionHandled(reqId); this.queue.done(); }; /** * Update composition by call the oncompositionupdate callback. * You should not call this method directly. * @param {any} reqId ID of the request. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.updateComposition = function jz_updateComposition(reqId) { if (typeof this.oncompositionupdate === 'function') { this.oncompositionupdate(this.symbols, reqId); } }; /** * End composition by call the oncompositionend callback. * You should not call this method directly. * @param {string} str String to end composition with. * @param {any} reqId ID of the request. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.endComposition = function jz_endComposition(str, reqId) { if (typeof this.oncompositionend === 'function') { this.oncompositionend(str, reqId); } }; /** * Update the candidate with query results. * You should not call this method directly. * @param {array} results The result array. * @param {any} reqId ID of the request. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.updateCandidates = function jz_updateCandidates(results, reqId) { this.defaultCandidate = results[0]; if (typeof this.oncandidateschange === 'function') { this.oncandidateschange(results, reqId); } }; /** * Confirm a selection by calling the compositionend callback and run * the query again. * You should not call this method directly. * @param {object} candidate One of the candidate that was sent via * oncandidateschange callback. * @param {any} reqId ID of the request. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.confirmCandidate = function jz_confirmCandidate(candidate, reqId) { this.endComposition(candidate[0], reqId); this.confirmedCharacters = candidate[0]; var metadata = this.candidateMetadata.getData(candidate[1]); this.confirmedPartIndex = metadata[1]; this.symbols = this.symbols.substr(metadata[0]); this.updateComposition(reqId); if (this.symbols.length !== 0) { this.query(reqId); } else { this.suggest(reqId); } }; /** * Call the onactionhandled callback. * You should not call this method directly. * @param {any} reqId ID of the request. * @this {object} JSZhuyin instance. */ JSZhuyin.prototype.sendActionHandled = function jz_sendActionHandled(reqId) { if (typeof this.onactionhandled === 'function') { this.onactionhandled(reqId); } };