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