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 function, override it to extend the construction behavior, remember to call "this._super()" in the extended "ctor" function.
 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  * cc.imeDispatcher is a singleton object which manage input message dispatching.
136  * @class
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 function, override it to extend the construction behavior, remember to call "this._super()" in the extended "ctor" function.
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         this._domInputControl.addEventListener("input", function () {
174             selfPointer._processDomInputString(selfPointer._domInputControl.value);
175         }, false);
176         this._domInputControl.addEventListener("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             this._domInputControl.addEventListener("keyup", function (e) {
190                 if (e.keyCode === cc.KEY.backspace) {
191                     selfPointer._processDomInputString(selfPointer._domInputControl.value);
192                 }
193             }, false);
194         }
195 
196         window.addEventListener('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 
391             var tipMessage = delegate.getTipMessage ? delegate.getTipMessage() : "please enter your word:";
392             // wechat cover the prompt function .So need use the Window.prototype.prompt
393             var userInput;
394             var win = window.Window;
395             if(win && win.prototype.prompt && win.prototype.prompt != prompt){
396                 userInput = win.prototype.prompt.call(window, tipMessage, this._currentInputString);
397             }else{
398                 userInput = prompt(tipMessage, this._currentInputString);
399             }
400             if(userInput != null)
401                 this._processDomInputString(userInput);
402             this.dispatchInsertText("\n", 1);
403         }else{
404             this.impl._delegateWithIme = delegate;
405             this._currentInputString = delegate.string || "";
406             delegate.didAttachWithIME();
407             this._domInputControl.focus();
408             this._domInputControl.value = this._currentInputString;
409             this._domInputControlTranslate();
410         }
411     },
412 
413     _domInputControlTranslate:function () {
414         if (/msie/i.test(navigator.userAgent)) {
415             this._domInputControl.style.left = this._lastClickPosition.x + "px";
416             this._domInputControl.style.top = this._lastClickPosition.y + "px";
417         } else {
418             this._domInputControl.translates(this._lastClickPosition.x, this._lastClickPosition.y);
419         }
420     },
421 
422     /**
423      * Detach the pDeleate with ime.
424      * @param {cc.IMEDelegate} delegate
425      * @return {Boolean} If the old delegate can detattach with ime and the new delegate can attach with ime, return true, otherwise return false.
426      * @example
427      * //example
428      * var ret = cc.imeDispatcher.detachDelegateWithIME(this);
429      */
430     detachDelegateWithIME:function (delegate) {
431         if (!this.impl || !delegate)
432             return false;
433 
434         // if delegate is not the current delegate attached with ime, return
435         if (this.impl._delegateWithIme !== delegate)
436             return false;
437 
438         if (!delegate.canDetachWithIME())
439             return false;
440 
441         this.impl._delegateWithIme = null;
442         delegate.didDetachWithIME();
443         cc._canvas.focus();
444         return true;
445     },
446 
447     /**
448      * Remove the delegate from the delegates who concern ime msg
449      * @param {cc.IMEDelegate} delegate
450      * @example
451      * //example
452      * cc.imeDispatcher.removeDelegate(this);
453      */
454     removeDelegate:function (delegate) {
455         if (!this.impl || !delegate)
456             return;
457 
458         // if delegate is not in delegate list, return
459         if (this.impl._delegateList.indexOf(delegate) === -1)
460             return;
461 
462         if (this.impl._delegateWithIme) {
463             if (delegate === this.impl._delegateWithIme) {
464                 this.impl._delegateWithIme = null;
465             }
466         }
467         cc.arrayRemoveObject(this.impl._delegateList, delegate);
468     },
469 
470     /**
471      * Process keydown's keycode
472      * @param {Number} keyCode
473      * @example
474      * //example
475      * document.addEventListener("keydown", function (e) {
476      *      cc.imeDispatcher.processKeycode(e.keyCode);
477      * });
478      */
479     processKeycode:function (keyCode) {
480         if (keyCode < 32) {
481             if (keyCode === cc.KEY.backspace) {
482                 this.dispatchDeleteBackward();
483             } else if (keyCode === cc.KEY.enter) {
484                 this.dispatchInsertText("\n", 1);
485             } else if (keyCode === cc.KEY.tab) {
486                 //tab input
487             } else if (keyCode === cc.KEY.escape) {
488                 //ESC input
489             }
490         } else if (keyCode < 255) {
491             this.dispatchInsertText(String.fromCharCode(keyCode), 1);
492         } else {
493             //
494         }
495     }
496 });
497 
498 /**
499  * Create the cc.IMEDispatcher.Imp Object. <br />
500  * This is the inner class...
501  * @class
502  * @extends cc.Class
503  * @name cc.IMEDispatcher.Impl
504  */
505 cc.IMEDispatcher.Impl = cc.Class.extend(/** @lends cc.IMEDispatcher.Impl# */{
506     _delegateWithIme:null,
507     _delegateList:null,
508     /**
509      * Constructor function, override it to extend the construction behavior, remember to call "this._super()" in the extended "ctor" function.
510      */
511     ctor:function () {
512         this._delegateList = [];
513     },
514     /**
515      * Find delegate
516      * @param {cc.IMEDelegate} delegate
517      * @return {Number|Null}
518      */
519     findDelegate:function (delegate) {
520         for (var i = 0; i < this._delegateList.length; i++) {
521             if (this._delegateList[i] === delegate)
522                 return i;
523         }
524         return null;
525     }
526 });
527 
528 // Initialize imeDispatcher singleton
529 cc.imeDispatcher = new cc.IMEDispatcher();
530 
531 document.body ?
532     cc.imeDispatcher.init() :
533     window.addEventListener('load', function () {
534         cc.imeDispatcher.init();
535     }, false);