1 /****************************************************************************
  2  Copyright (c) 2008-2010 Ricardo Quesada
  3  Copyright (c) 2011-2012 cocos2d-x.org
  4  Copyright (c) 2013-2014 Chukong Technologies Inc.
  5 
  6  http://www.cocos2d-x.org
  7 
  8  Permission is hereby granted, free of charge, to any person obtaining a copy
  9  of this software and associated documentation files (the "Software"), to deal
 10  in the Software without restriction, including without limitation the rights
 11  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12  copies of the Software, and to permit persons to whom the Software is
 13  furnished to do so, subject to the following conditions:
 14 
 15  The above copyright notice and this permission notice shall be included in
 16  all copies or substantial portions of the Software.
 17 
 18  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 23  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 24  THE SOFTWARE.
 25  ****************************************************************************/
 26 
 27 /**
 28  * IME Keyboard Notification Info structure
 29  * @param {cc.Rect} begin the soft keyboard rectangle when animatin begin
 30  * @param {cc.Rect} end the soft keyboard rectangle when animatin end
 31  * @param {Number} duration the soft keyboard animation duration
 32  */
 33 cc.IMEKeyboardNotificationInfo = function (begin, end, duration) {
 34     this.begin = begin || cc.rect(0, 0, 0, 0);
 35     this.end = end || cc.rect(0, 0, 0, 0);
 36     this.duration = duration || 0;
 37 };
 38 
 39 /**
 40  * Input method editor delegate.
 41  * @class
 42  * @extends cc.Class
 43  */
 44 cc.IMEDelegate = cc.Class.extend(/** @lends cc.IMEDelegate# */{
 45     /**
 46      * Constructor
 47      */
 48     ctor:function () {
 49         cc.imeDispatcher.addDelegate(this);
 50     },
 51     /**
 52      * Remove delegate
 53      */
 54     removeDelegate:function () {
 55         cc.imeDispatcher.removeDelegate(this);
 56     },
 57     /**
 58      * Remove delegate
 59      * @return {Boolean}
 60      */
 61     attachWithIME:function () {
 62         return cc.imeDispatcher.attachDelegateWithIME(this);
 63     },
 64     /**
 65      * Detach with IME
 66      * @return {Boolean}
 67      */
 68     detachWithIME:function () {
 69         return cc.imeDispatcher.detachDelegateWithIME(this);
 70     },
 71 
 72     /**
 73      * Decide the delegate instance is ready for receive ime message or not.<br />
 74      * Called by CCIMEDispatcher.
 75      * @return {Boolean}
 76      */
 77     canAttachWithIME:function () {
 78         return false;
 79     },
 80 
 81     /**
 82      * When the delegate detach with IME, this method call by CCIMEDispatcher.
 83      */
 84     didAttachWithIME:function () {
 85     },
 86 
 87     /**
 88      * Decide the delegate instance can stop receive ime message or not.
 89      * @return {Boolean}
 90      */
 91     canDetachWithIME:function () {
 92         return false;
 93     },
 94 
 95     /**
 96      * When the delegate detach with IME, this method call by CCIMEDispatcher.
 97      */
 98     didDetachWithIME:function () {
 99     },
100 
101     /**
102      * Called by CCIMEDispatcher when some text input from IME.
103      */
104     insertText:function (text, len) {
105     },
106 
107     /**
108      * Called by CCIMEDispatcher when user clicked the backward key.
109      */
110     deleteBackward:function () {
111     },
112 
113     /**
114      * Called by CCIMEDispatcher for get text which delegate already has.
115      * @return {String}
116      */
117     getContentText:function () {
118         return "";
119     },
120 
121     //////////////////////////////////////////////////////////////////////////
122     // keyboard show/hide notification
123     //////////////////////////////////////////////////////////////////////////
124     keyboardWillShow:function (info) {
125     },
126     keyboardDidShow:function (info) {
127     },
128     keyboardWillHide:function (info) {
129     },
130     keyboardDidHide:function (info) {
131     }
132 });
133 
134 /**
135  * @namespace Input Method Edit Message Dispatcher.
136  * @name cc.imeDispatcher
137  */
138 cc.IMEDispatcher = cc.Class.extend(/**  @lends cc.imeDispatcher# */{
139     _domInputControl:null,
140     impl:null,
141     _currentInputString:"",
142     _lastClickPosition:null,
143     /**
144      * Constructor
145      */
146     ctor:function () {
147         this.impl = new cc.IMEDispatcher.Impl();
148         this._lastClickPosition = cc.p(0, 0);
149     },
150 
151     init:function () {
152         if (cc.sys.isMobile)
153             return;
154         this._domInputControl = cc.$("#imeDispatcherInput");
155         if (!this._domInputControl) {
156             this._domInputControl = cc.$new("input");
157             this._domInputControl.setAttribute("type", "text");
158             this._domInputControl.setAttribute("id", "imeDispatcherInput");
159             this._domInputControl.resize(0.0, 0.0);
160             this._domInputControl.translates(0, 0);
161             this._domInputControl.style.opacity = "0";
162             //this._domInputControl.style.filter = "alpha(opacity = 0)";
163             this._domInputControl.style.fontSize = "1px";
164             this._domInputControl.setAttribute('tabindex', 2);
165             this._domInputControl.style.position = "absolute";
166             this._domInputControl.style.top = 0;
167             this._domInputControl.style.left = 0;
168             document.body.appendChild(this._domInputControl);
169         }
170         var selfPointer = this;
171         //add event listener
172         cc._addEventListener(this._domInputControl, "input", function () {
173             selfPointer._processDomInputString(selfPointer._domInputControl.value);
174         }, false);
175         cc._addEventListener(this._domInputControl, "keydown", function (e) {
176             // ignore tab key
177             if (e.keyCode === cc.KEY.tab) {
178                 e.stopPropagation();
179                 e.preventDefault();
180             } else if (e.keyCode == cc.KEY.enter) {
181                 selfPointer.dispatchInsertText("\n", 1);
182                 e.stopPropagation();
183                 e.preventDefault();
184             }
185         }, false);
186 
187         if (/msie/i.test(navigator.userAgent)) {
188             cc._addEventListener(this._domInputControl, "keyup", function (e) {
189                 if (e.keyCode == cc.KEY.backspace) {
190                     selfPointer._processDomInputString(selfPointer._domInputControl.value);
191                 }
192             }, false);
193         }
194 
195         cc._addEventListener(window, 'mousedown', function (event) {
196             var tx = event.pageX || 0;
197             var ty = event.pageY || 0;
198 
199             selfPointer._lastClickPosition.x = tx;
200             selfPointer._lastClickPosition.y = ty;
201         }, false);
202     },
203 
204     _processDomInputString:function (text) {
205         var i, startPos;
206         var len = this._currentInputString.length < text.length ? this._currentInputString.length : text.length;
207         for (startPos = 0; startPos < len; startPos++) {
208             if (text[startPos] !== this._currentInputString[startPos])
209                 break;
210         }
211         var delTimes = this._currentInputString.length - startPos;
212         var insTimes = text.length - startPos;
213         for (i = 0; i < delTimes; i++)
214             this.dispatchDeleteBackward();
215 
216         for (i = 0; i < insTimes; i++)
217             this.dispatchInsertText(text[startPos + i], 1);
218 
219         this._currentInputString = text;
220     },
221 
222     /**
223      * Dispatch the input text from ime
224      * @param {String} text
225      * @param {Number} len
226      */
227     dispatchInsertText:function (text, len) {
228         if (!this.impl || !text || len <= 0)
229             return;
230 
231         // there is no delegate attach with ime
232         if (!this.impl._delegateWithIme)
233             return;
234 
235         this.impl._delegateWithIme.insertText(text, len);
236     },
237 
238     /**
239      * Dispatch the delete backward operation
240      */
241     dispatchDeleteBackward:function () {
242         if (!this.impl) {
243             return;
244         }
245 
246         // there is no delegate attach with ime
247         if (!this.impl._delegateWithIme)
248             return;
249 
250         this.impl._delegateWithIme.deleteBackward();
251     },
252 
253     /**
254      * Get the content text, which current CCIMEDelegate which attached with IME has.
255      * @return {String}
256      */
257     getContentText:function () {
258         if (this.impl && this.impl._delegateWithIme) {
259             var pszContentText = this.impl._delegateWithIme.getContentText();
260             return (pszContentText) ? pszContentText : "";
261         }
262         return "";
263     },
264 
265     /**
266      * Dispatch keyboard notification
267      * @param {cc.IMEKeyboardNotificationInfo} info
268      */
269     dispatchKeyboardWillShow:function (info) {
270         if (this.impl) {
271             for (var i = 0; i < this.impl._delegateList.length; i++) {
272                 var delegate = this.impl._delegateList[i];
273                 if (delegate) {
274                     delegate.keyboardWillShow(info);
275                 }
276             }
277         }
278     },
279 
280     /**
281      * Dispatch keyboard notification
282      * @param {cc.IMEKeyboardNotificationInfo} info
283      */
284     dispatchKeyboardDidShow:function (info) {
285         if (this.impl) {
286             for (var i = 0; i < this.impl._delegateList.length; i++) {
287                 var delegate = this.impl._delegateList[i];
288                 if (delegate)
289                     delegate.keyboardDidShow(info);
290             }
291         }
292     },
293 
294     /**
295      * Dispatch keyboard notification
296      * @param {cc.IMEKeyboardNotificationInfo} info
297      */
298     dispatchKeyboardWillHide:function (info) {
299         if (this.impl) {
300             for (var i = 0; i < this.impl._delegateList.length; i++) {
301                 var delegate = this.impl._delegateList[i];
302                 if (delegate) {
303                     delegate.keyboardWillHide(info);
304                 }
305             }
306         }
307     },
308 
309     /**
310      * Dispatch keyboard notification
311      * @param {cc.IMEKeyboardNotificationInfo} info
312      */
313     dispatchKeyboardDidHide:function (info) {
314         if (this.impl) {
315             for (var i = 0; i < this.impl._delegateList.length; i++) {
316                 var delegate = this.impl._delegateList[i];
317                 if (delegate) {
318                     delegate.keyboardDidHide(info);
319                 }
320             }
321         }
322     },
323 
324     /**
325      * Add delegate to concern ime msg
326      * @param {cc.IMEDelegate} delegate
327      * @example
328      * //example
329      * cc.imeDispatcher.addDelegate(this);
330      */
331     addDelegate:function (delegate) {
332         if (!delegate || !this.impl)
333             return;
334 
335         if (this.impl._delegateList.indexOf(delegate) > -1) {
336             // delegate already in list
337             return;
338         }
339         this.impl._delegateList.splice(0, 0, delegate);
340     },
341 
342     /**
343      * Attach the pDeleate with ime.
344      * @param {cc.IMEDelegate} delegate
345      * @return {Boolean} If the old delegate can detattach with ime and the new delegate can attach with ime, return true, otherwise return false.
346      * @example
347      * //example
348      * var ret = cc.imeDispatcher.attachDelegateWithIME(this);
349      */
350     attachDelegateWithIME:function (delegate) {
351         if (!this.impl || !delegate)
352             return false;
353 
354         // if delegate is not in delegate list, return
355         if (this.impl._delegateList.indexOf(delegate) == -1)
356             return false;
357 
358         if (this.impl._delegateWithIme) {
359             // if old delegate canDetachWithIME return false
360             // or delegate canAttachWithIME return false,
361             // do nothing.
362             if (!this.impl._delegateWithIme.canDetachWithIME()
363                 || !delegate.canAttachWithIME())
364                 return false;
365 
366             // detach first
367             var pOldDelegate = this.impl._delegateWithIme;
368             this.impl._delegateWithIme = null;
369             pOldDelegate.didDetachWithIME();
370 
371             this._focusDomInput(delegate);
372             return true;
373         }
374 
375         // havn't delegate attached with IME yet
376         if (!delegate.canAttachWithIME())
377             return false;
378 
379         this._focusDomInput(delegate);
380         return true;
381     },
382 
383     _focusDomInput:function (delegate) {
384         if(cc.sys.isMobile){
385             this.impl._delegateWithIme = delegate;
386             delegate.didAttachWithIME();
387             //prompt
388             this._currentInputString = delegate.string || "";
389             var userInput = prompt("please enter your word:", this._currentInputString);
390             if(userInput != null)
391                 this._processDomInputString(userInput);
392             this.dispatchInsertText("\n", 1);
393         }else{
394             this.impl._delegateWithIme = delegate;
395             this._currentInputString = delegate.string || "";
396             delegate.didAttachWithIME();
397             this._domInputControl.focus();
398             this._domInputControl.value = this._currentInputString;
399             this._domInputControlTranslate();
400         }
401     },
402 
403     _domInputControlTranslate:function () {
404         if (/msie/i.test(navigator.userAgent)) {
405             this._domInputControl.style.left = this._lastClickPosition.x + "px";
406             this._domInputControl.style.top = this._lastClickPosition.y + "px";
407         } else {
408             this._domInputControl.translates(this._lastClickPosition.x, this._lastClickPosition.y);
409         }
410     },
411 
412     /**
413      * Detach the pDeleate with ime.
414      * @param {cc.IMEDelegate} delegate
415      * @return {Boolean} If the old delegate can detattach with ime and the new delegate can attach with ime, return true, otherwise return false.
416      * @example
417      * //example
418      * var ret = cc.imeDispatcher.detachDelegateWithIME(this);
419      */
420     detachDelegateWithIME:function (delegate) {
421         if (!this.impl || !delegate)
422             return false;
423 
424         // if delegate is not the current delegate attached with ime, return
425         if (this.impl._delegateWithIme != delegate)
426             return false;
427 
428         if (!delegate.canDetachWithIME())
429             return false;
430 
431         this.impl._delegateWithIme = null;
432         delegate.didDetachWithIME();
433         cc._canvas.focus();
434         return true;
435     },
436 
437     /**
438      * Remove the delegate from the delegates who concern ime msg
439      * @param {cc.IMEDelegate} delegate
440      * @example
441      * //example
442      * cc.imeDispatcher.removeDelegate(this);
443      */
444     removeDelegate:function (delegate) {
445         if (!this.impl || !delegate)
446             return;
447 
448         // if delegate is not in delegate list, return
449         if (this.impl._delegateList.indexOf(delegate) == -1)
450             return;
451 
452         if (this.impl._delegateWithIme) {
453             if (delegate == this.impl._delegateWithIme) {
454                 this.impl._delegateWithIme = null;
455             }
456         }
457         cc.arrayRemoveObject(this.impl._delegateList, delegate);
458     },
459 
460     /**
461      * Process keydown's keycode
462      * @param {Number} keyCode
463      * @example
464      * //example
465      * document.addEventListener("keydown", function (e) {
466      *      cc.imeDispatcher.processKeycode(e.keyCode);
467      * });
468      */
469     processKeycode:function (keyCode) {
470         if (keyCode < 32) {
471             if (keyCode == cc.KEY.backspace) {
472                 this.dispatchDeleteBackward();
473             } else if (keyCode == cc.KEY.enter) {
474                 this.dispatchInsertText("\n", 1);
475             } else if (keyCode == cc.KEY.tab) {
476                 //tab input
477             } else if (keyCode == cc.KEY.escape) {
478                 //ESC input
479             }
480         } else if (keyCode < 255) {
481             this.dispatchInsertText(String.fromCharCode(keyCode), 1);
482         } else {
483             //
484         }
485     }
486 });
487 
488 /**
489  * @class
490  * @extends cc.Class
491  * @name cc.IMEDispatcher.Impl
492  */
493 cc.IMEDispatcher.Impl = cc.Class.extend(/** @lends cc.IMEDispatcher.Impl# */{
494     _delegateWithIme:null,
495     _delegateList:null,
496     /**
497      * Constructor
498      */
499     ctor:function () {
500         this._delegateList = [];
501     },
502     /**
503      * Find delegate
504      * @param {cc.IMEDelegate} delegate
505      * @return {Number|Null}
506      */
507     findDelegate:function (delegate) {
508         for (var i = 0; i < this._delegateList.length; i++) {
509             if (this._delegateList[i] == delegate)
510                 return i;
511         }
512         return null;
513     }
514 });
515 
516 // Initialize imeDispatcher singleton
517 cc.imeDispatcher = new cc.IMEDispatcher();
518 
519 document.body ?
520     cc.imeDispatcher.init() :
521     cc._addEventListener(window, 'load', function () {
522         cc.imeDispatcher.init();
523     }, false);