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  * Input Method Edit Message Dispatcher.
136  * @namespace
137  * @name cc.imeDispatcher
138  */
139 cc.IMEDispatcher = cc.Class.extend(/**  @lends cc.imeDispatcher# */{
140     _domInputControl:null,
141     impl:null,
142     _currentInputString:"",
143     _lastClickPosition:null,
144     /**
145      * Constructor
146      */
147     ctor:function () {
148         this.impl = new cc.IMEDispatcher.Impl();
149         this._lastClickPosition = cc.p(0, 0);
150     },
151 
152     init:function () {
153         if (cc.sys.isMobile)
154             return;
155         this._domInputControl = cc.$("#imeDispatcherInput");
156         if (!this._domInputControl) {
157             this._domInputControl = cc.$new("input");
158             this._domInputControl.setAttribute("type", "text");
159             this._domInputControl.setAttribute("id", "imeDispatcherInput");
160             this._domInputControl.resize(0.0, 0.0);
161             this._domInputControl.translates(0, 0);
162             this._domInputControl.style.opacity = "0";
163             //this._domInputControl.style.filter = "alpha(opacity = 0)";
164             this._domInputControl.style.fontSize = "1px";
165             this._domInputControl.setAttribute('tabindex', 2);
166             this._domInputControl.style.position = "absolute";
167             this._domInputControl.style.top = 0;
168             this._domInputControl.style.left = 0;
169             document.body.appendChild(this._domInputControl);
170         }
171         var selfPointer = this;
172         //add event listener
173         cc._addEventListener(this._domInputControl, "input", function () {
174             selfPointer._processDomInputString(selfPointer._domInputControl.value);
175         }, false);
176         cc._addEventListener(this._domInputControl, "keydown", function (e) {
177             // ignore tab key
178             if (e.keyCode === cc.KEY.tab) {
179                 e.stopPropagation();
180                 e.preventDefault();
181             } else if (e.keyCode == cc.KEY.enter) {
182                 selfPointer.dispatchInsertText("\n", 1);
183                 e.stopPropagation();
184                 e.preventDefault();
185             }
186         }, false);
187 
188         if (/msie/i.test(navigator.userAgent)) {
189             cc._addEventListener(this._domInputControl, "keyup", function (e) {
190                 if (e.keyCode == cc.KEY.backspace) {
191                     selfPointer._processDomInputString(selfPointer._domInputControl.value);
192                 }
193             }, false);
194         }
195 
196         cc._addEventListener(window, 'mousedown', function (event) {
197             var tx = event.pageX || 0;
198             var ty = event.pageY || 0;
199 
200             selfPointer._lastClickPosition.x = tx;
201             selfPointer._lastClickPosition.y = ty;
202         }, false);
203     },
204 
205     _processDomInputString:function (text) {
206         var i, startPos;
207         var len = this._currentInputString.length < text.length ? this._currentInputString.length : text.length;
208         for (startPos = 0; startPos < len; startPos++) {
209             if (text[startPos] !== this._currentInputString[startPos])
210                 break;
211         }
212         var delTimes = this._currentInputString.length - startPos;
213         var insTimes = text.length - startPos;
214         for (i = 0; i < delTimes; i++)
215             this.dispatchDeleteBackward();
216 
217         for (i = 0; i < insTimes; i++)
218             this.dispatchInsertText(text[startPos + i], 1);
219 
220         this._currentInputString = text;
221     },
222 
223     /**
224      * Dispatch the input text from ime
225      * @param {String} text
226      * @param {Number} len
227      */
228     dispatchInsertText:function (text, len) {
229         if (!this.impl || !text || len <= 0)
230             return;
231 
232         // there is no delegate attach with ime
233         if (!this.impl._delegateWithIme)
234             return;
235 
236         this.impl._delegateWithIme.insertText(text, len);
237     },
238 
239     /**
240      * Dispatch the delete backward operation
241      */
242     dispatchDeleteBackward:function () {
243         if (!this.impl) {
244             return;
245         }
246 
247         // there is no delegate attach with ime
248         if (!this.impl._delegateWithIme)
249             return;
250 
251         this.impl._delegateWithIme.deleteBackward();
252     },
253 
254     /**
255      * Get the content text, which current CCIMEDelegate which attached with IME has.
256      * @return {String}
257      */
258     getContentText:function () {
259         if (this.impl && this.impl._delegateWithIme) {
260             var pszContentText = this.impl._delegateWithIme.getContentText();
261             return (pszContentText) ? pszContentText : "";
262         }
263         return "";
264     },
265 
266     /**
267      * Dispatch keyboard notification
268      * @param {cc.IMEKeyboardNotificationInfo} info
269      */
270     dispatchKeyboardWillShow:function (info) {
271         if (this.impl) {
272             for (var i = 0; i < this.impl._delegateList.length; i++) {
273                 var delegate = this.impl._delegateList[i];
274                 if (delegate) {
275                     delegate.keyboardWillShow(info);
276                 }
277             }
278         }
279     },
280 
281     /**
282      * Dispatch keyboard notification
283      * @param {cc.IMEKeyboardNotificationInfo} info
284      */
285     dispatchKeyboardDidShow:function (info) {
286         if (this.impl) {
287             for (var i = 0; i < this.impl._delegateList.length; i++) {
288                 var delegate = this.impl._delegateList[i];
289                 if (delegate)
290                     delegate.keyboardDidShow(info);
291             }
292         }
293     },
294 
295     /**
296      * Dispatch keyboard notification
297      * @param {cc.IMEKeyboardNotificationInfo} info
298      */
299     dispatchKeyboardWillHide:function (info) {
300         if (this.impl) {
301             for (var i = 0; i < this.impl._delegateList.length; i++) {
302                 var delegate = this.impl._delegateList[i];
303                 if (delegate) {
304                     delegate.keyboardWillHide(info);
305                 }
306             }
307         }
308     },
309 
310     /**
311      * Dispatch keyboard notification
312      * @param {cc.IMEKeyboardNotificationInfo} info
313      */
314     dispatchKeyboardDidHide:function (info) {
315         if (this.impl) {
316             for (var i = 0; i < this.impl._delegateList.length; i++) {
317                 var delegate = this.impl._delegateList[i];
318                 if (delegate) {
319                     delegate.keyboardDidHide(info);
320                 }
321             }
322         }
323     },
324 
325     /**
326      * Add delegate to concern ime msg
327      * @param {cc.IMEDelegate} delegate
328      * @example
329      * //example
330      * cc.imeDispatcher.addDelegate(this);
331      */
332     addDelegate:function (delegate) {
333         if (!delegate || !this.impl)
334             return;
335 
336         if (this.impl._delegateList.indexOf(delegate) > -1) {
337             // delegate already in list
338             return;
339         }
340         this.impl._delegateList.splice(0, 0, delegate);
341     },
342 
343     /**
344      * Attach the pDeleate with ime.
345      * @param {cc.IMEDelegate} delegate
346      * @return {Boolean} If the old delegate can detattach with ime and the new delegate can attach with ime, return true, otherwise return false.
347      * @example
348      * //example
349      * var ret = cc.imeDispatcher.attachDelegateWithIME(this);
350      */
351     attachDelegateWithIME:function (delegate) {
352         if (!this.impl || !delegate)
353             return false;
354 
355         // if delegate is not in delegate list, return
356         if (this.impl._delegateList.indexOf(delegate) == -1)
357             return false;
358 
359         if (this.impl._delegateWithIme) {
360             // if old delegate canDetachWithIME return false
361             // or delegate canAttachWithIME return false,
362             // do nothing.
363             if (!this.impl._delegateWithIme.canDetachWithIME()
364                 || !delegate.canAttachWithIME())
365                 return false;
366 
367             // detach first
368             var pOldDelegate = this.impl._delegateWithIme;
369             this.impl._delegateWithIme = null;
370             pOldDelegate.didDetachWithIME();
371 
372             this._focusDomInput(delegate);
373             return true;
374         }
375 
376         // havn't delegate attached with IME yet
377         if (!delegate.canAttachWithIME())
378             return false;
379 
380         this._focusDomInput(delegate);
381         return true;
382     },
383 
384     _focusDomInput:function (delegate) {
385         if(cc.sys.isMobile){
386             this.impl._delegateWithIme = delegate;
387             delegate.didAttachWithIME();
388             //prompt
389             this._currentInputString = delegate.string || "";
390             var userInput = prompt("please enter your word:", this._currentInputString);
391             if(userInput != null)
392                 this._processDomInputString(userInput);
393             this.dispatchInsertText("\n", 1);
394         }else{
395             this.impl._delegateWithIme = delegate;
396             this._currentInputString = delegate.string || "";
397             delegate.didAttachWithIME();
398             this._domInputControl.focus();
399             this._domInputControl.value = this._currentInputString;
400             this._domInputControlTranslate();
401         }
402     },
403 
404     _domInputControlTranslate:function () {
405         if (/msie/i.test(navigator.userAgent)) {
406             this._domInputControl.style.left = this._lastClickPosition.x + "px";
407             this._domInputControl.style.top = this._lastClickPosition.y + "px";
408         } else {
409             this._domInputControl.translates(this._lastClickPosition.x, this._lastClickPosition.y);
410         }
411     },
412 
413     /**
414      * Detach the pDeleate with ime.
415      * @param {cc.IMEDelegate} delegate
416      * @return {Boolean} If the old delegate can detattach with ime and the new delegate can attach with ime, return true, otherwise return false.
417      * @example
418      * //example
419      * var ret = cc.imeDispatcher.detachDelegateWithIME(this);
420      */
421     detachDelegateWithIME:function (delegate) {
422         if (!this.impl || !delegate)
423             return false;
424 
425         // if delegate is not the current delegate attached with ime, return
426         if (this.impl._delegateWithIme != delegate)
427             return false;
428 
429         if (!delegate.canDetachWithIME())
430             return false;
431 
432         this.impl._delegateWithIme = null;
433         delegate.didDetachWithIME();
434         cc._canvas.focus();
435         return true;
436     },
437 
438     /**
439      * Remove the delegate from the delegates who concern ime msg
440      * @param {cc.IMEDelegate} delegate
441      * @example
442      * //example
443      * cc.imeDispatcher.removeDelegate(this);
444      */
445     removeDelegate:function (delegate) {
446         if (!this.impl || !delegate)
447             return;
448 
449         // if delegate is not in delegate list, return
450         if (this.impl._delegateList.indexOf(delegate) == -1)
451             return;
452 
453         if (this.impl._delegateWithIme) {
454             if (delegate == this.impl._delegateWithIme) {
455                 this.impl._delegateWithIme = null;
456             }
457         }
458         cc.arrayRemoveObject(this.impl._delegateList, delegate);
459     },
460 
461     /**
462      * Process keydown's keycode
463      * @param {Number} keyCode
464      * @example
465      * //example
466      * document.addEventListener("keydown", function (e) {
467      *      cc.imeDispatcher.processKeycode(e.keyCode);
468      * });
469      */
470     processKeycode:function (keyCode) {
471         if (keyCode < 32) {
472             if (keyCode == cc.KEY.backspace) {
473                 this.dispatchDeleteBackward();
474             } else if (keyCode == cc.KEY.enter) {
475                 this.dispatchInsertText("\n", 1);
476             } else if (keyCode == cc.KEY.tab) {
477                 //tab input
478             } else if (keyCode == cc.KEY.escape) {
479                 //ESC input
480             }
481         } else if (keyCode < 255) {
482             this.dispatchInsertText(String.fromCharCode(keyCode), 1);
483         } else {
484             //
485         }
486     }
487 });
488 
489 /**
490  * @class
491  * @extends cc.Class
492  * @name cc.IMEDispatcher.Impl
493  */
494 cc.IMEDispatcher.Impl = cc.Class.extend(/** @lends cc.IMEDispatcher.Impl# */{
495     _delegateWithIme:null,
496     _delegateList:null,
497     /**
498      * Constructor
499      */
500     ctor:function () {
501         this._delegateList = [];
502     },
503     /**
504      * Find delegate
505      * @param {cc.IMEDelegate} delegate
506      * @return {Number|Null}
507      */
508     findDelegate:function (delegate) {
509         for (var i = 0; i < this._delegateList.length; i++) {
510             if (this._delegateList[i] == delegate)
511                 return i;
512         }
513         return null;
514     }
515 });
516 
517 // Initialize imeDispatcher singleton
518 cc.imeDispatcher = new cc.IMEDispatcher();
519 
520 document.body ?
521     cc.imeDispatcher.init() :
522     cc._addEventListener(window, 'load', function () {
523         cc.imeDispatcher.init();
524     }, false);