forked from hifive/hifivemain
/
h5.core.controller.js
3063 lines (2837 loc) · 117 KB
/
h5.core.controller.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (C) 2012-2013 NS Solutions Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* hifive
*/
/* ------ h5.core.controller ------ */
(function() {
// =========================================================================
//
// Constants
//
// =========================================================================
// =============================
// Production
// =============================
var TYPE_OF_UNDEFINED = 'undefined';
var SUFFIX_CONTROLLER = 'Controller';
var SUFFIX_LOGIC = 'Logic';
var EVENT_NAME_H5_TRACKSTART = 'h5trackstart';
var EVENT_NAME_H5_TRACKMOVE = 'h5trackmove';
var EVENT_NAME_H5_TRACKEND = 'h5trackend';
var ROOT_ELEMENT_NAME = 'rootElement';
var EVENT_NAME_TRIGGER_INDICATOR = 'triggerIndicator';
/** インラインコメントテンプレートのコメントノードの開始文字列 */
var COMMENT_BINDING_TARGET_MARKER = '{h5view ';
// エラーコード
/** エラーコード: テンプレートに渡すセレクタが不正 */
var ERR_CODE_INVALID_TEMPLATE_SELECTOR = 6000;
/** エラーコード: バインド対象が指定されていない */
var ERR_CODE_BIND_TARGET_REQUIRED = 6001;
/** エラーコード: bindControllerメソッドにコントローラではないオブジェクトが渡された(このエラーはver.1.1.3時点では通常発生しないので削除) */
//var ERR_CODE_BIND_NOT_CONTROLLER = 6002;
/** エラーコード: バインド対象となるDOMがない */
var ERR_CODE_BIND_NO_TARGET = 6003;
/** エラーコード: バインド対象となるDOMが複数存在する */
var ERR_CODE_BIND_TOO_MANY_TARGET = 6004;
/** エラーコード: 指定された引数の数が少ない */
var ERR_CODE_TOO_FEW_ARGUMENTS = 6005;
/** エラーコード: コントローラの名前が指定されていない */
var ERR_CODE_INVALID_CONTROLLER_NAME = 6006;
/** エラーコード: コントローラの初期化パラメータが不正 */
var ERR_CODE_CONTROLLER_INVALID_INIT_PARAM = 6007;
/** エラーコード: 既にコントローラ化されている */
var ERR_CODE_CONTROLLER_ALREADY_CREATED = 6008;
/** エラーコード: コントローラの参照が循環している */
var ERR_CODE_CONTROLLER_CIRCULAR_REF = 6009;
/** エラーコード: コントローラ内のロジックの参照が循環している */
var ERR_CODE_LOGIC_CIRCULAR_REF = 6010;
/** エラーコード: コントローラの参照が循環している */
var ERR_CODE_CONTROLLER_SAME_PROPERTY = 6011;
/** エラーコード: イベントハンドラのセレクタに{this}が指定されている */
var ERR_CODE_EVENT_HANDLER_SELECTOR_THIS = 6012;
/** エラーコード: あるセレクタに対して重複したイベントハンドラが設定されている */
var ERR_CODE_SAME_EVENT_HANDLER = 6013;
/** エラーコード: __metaで指定されたプロパティがない */
var ERR_CODE_CONTROLLER_META_KEY_INVALID = 6014;
/** エラーコード: __metaで指定されたプロパティがnullである */
var ERR_CODE_CONTROLLER_META_KEY_NULL = 6015;
/** エラーコード: __metaで指定されたプロパティがコントローラではない */
var ERR_CODE_CONTROLLER_META_KEY_NOT_CONTROLLER = 6016;
/** エラーコード: ロジックの名前に文字列が指定されていない */
var ERR_CODE_INVALID_LOGIC_NAME = 6017;
/** エラーコード: 既にロジック化されている */
var ERR_CODE_LOGIC_ALREADY_CREATED = 6018;
/** エラーコード: exposeする際にコントローラ、もしくはロジックの名前がない */
var ERR_CODE_EXPOSE_NAME_REQUIRED = 6019;
/** エラーコード: Viewモジュールが組み込まれていない */
var ERR_CODE_NOT_VIEW = 6029;
/** エラーコード:バインド対象を指定する引数に文字列、オブジェクト、配列以外が渡された */
var ERR_CODE_BIND_TARGET_ILLEGAL = 6030;
/** エラーコード:ルートコントローラ以外ではcontroller.bind()はできない */
var ERR_CODE_BIND_ROOT_ONLY = 6031;
/** エラーコード:コントローラメソッドは最低2つの引数が必要 */
var ERR_CODE_CONTROLLER_TOO_FEW_ARGS = 6032;
/** エラーコード:コントローラの初期化処理がユーザーコードによって中断された(__initや__readyで返したプロミスがrejectした) */
var ERR_CODE_CONTROLLER_REJECTED_BY_USER = 6033;
// =============================
// Development Only
// =============================
var fwLogger = h5.log.createLogger('h5.core');
/* del begin */
// ログメッセージ
var FW_LOG_TEMPLATE_LOADED = 'コントローラ"{0}"のテンプレートの読み込みに成功しました。';
var FW_LOG_TEMPLATE_LOAD_FAILED = 'コントローラ"{0}"のテンプレートの読み込みに失敗しました。URL:{1}';
var FW_LOG_INIT_CONTROLLER_REJECTED = 'コントローラ"{0}"の{1}で返されたPromiseがfailしたため、コントローラの初期化を中断しdisposeしました。';
var FW_LOG_INIT_CONTROLLER_ERROR = 'コントローラ"{0}"の初期化中にエラーが発生しました。{0}はdisposeされました。';
var FW_LOG_INIT_CONTROLLER_BEGIN = 'コントローラ"{0}"の初期化を開始しました。';
var FW_LOG_INIT_CONTROLLER_COMPLETE = 'コントローラ"{0}"の初期化が正常に完了しました。';
var FW_LOG_INIT_CONTROLLER_THROWN_ERROR = 'コントローラ"{0}"の{1}内でエラーが発生したため、コントローラの初期化を中断しdisposeしました。';
// エラーコードマップ
var errMsgMap = {};
errMsgMap[ERR_CODE_INVALID_TEMPLATE_SELECTOR] = 'update/append/prepend() の第1引数に"window", "navigator", または"window.", "navigator."で始まるセレクタは指定できません。';
errMsgMap[ERR_CODE_BIND_TARGET_REQUIRED] = 'コントローラ"{0}"のバインド対象となる要素を指定して下さい。';
//errMsgMap[ERR_CODE_BIND_NOT_CONTROLLER] = 'コントローラ化したオブジェクトを指定して下さい。';
errMsgMap[ERR_CODE_BIND_NO_TARGET] = 'コントローラ"{0}"のバインド対象となる要素が存在しません。';
errMsgMap[ERR_CODE_BIND_TOO_MANY_TARGET] = 'コントローラ"{0}"のバインド対象となる要素が2つ以上存在します。バインド対象は1つのみにしてください。';
errMsgMap[ERR_CODE_TOO_FEW_ARGUMENTS] = '正しい数の引数を指定して下さい。';
errMsgMap[ERR_CODE_INVALID_CONTROLLER_NAME] = 'コントローラの名前は必須です。コントローラの__nameにコントローラ名を空でない文字列で設定して下さい。';
errMsgMap[ERR_CODE_CONTROLLER_INVALID_INIT_PARAM] = 'コントローラ"{0}"の初期化パラメータがプレーンオブジェクトではありません。初期化パラメータにはプレーンオブジェクトを設定してください。';
errMsgMap[ERR_CODE_CONTROLLER_ALREADY_CREATED] = '指定されたオブジェクトは既にコントローラ化されています。';
errMsgMap[ERR_CODE_CONTROLLER_CIRCULAR_REF] = 'コントローラ"{0}"で、参照が循環しているため、コントローラを生成できません。';
errMsgMap[ERR_CODE_LOGIC_CIRCULAR_REF] = 'コントローラ"{0}"のロジックで、参照が循環しているため、ロジックを生成できません。';
errMsgMap[ERR_CODE_CONTROLLER_SAME_PROPERTY] = 'コントローラ"{0}"のプロパティ"{1}"はコントローラ化によって追加されるプロパティと名前が重複しています。';
errMsgMap[ERR_CODE_EVENT_HANDLER_SELECTOR_THIS] = 'コントローラ"{0}"でセレクタ名にthisが指定されています。コントローラをバインドした要素自身を指定したい時はrootElementを指定してください。';
errMsgMap[ERR_CODE_SAME_EVENT_HANDLER] = 'コントローラ"{0}"のセレクタ"{1}"に対して"{2}"というイベントハンドラが重複して設定されています。';
errMsgMap[ERR_CODE_CONTROLLER_META_KEY_INVALID] = 'コントローラ"{0}"には__metaで指定されたプロパティ"{1}"がありません。';
errMsgMap[ERR_CODE_CONTROLLER_META_KEY_NULL] = 'コントローラ"{0}"の__metaに指定されたキー"{1}"の値がnullです。コントローラを持つプロパティキー名を指定してください。';
errMsgMap[ERR_CODE_CONTROLLER_META_KEY_NOT_CONTROLLER] = 'コントローラ"{0}"の__metaに指定されたキー"{1}"の値はコントローラではありません。コントローラを持つプロパティキー名を指定してください。';
errMsgMap[ERR_CODE_INVALID_LOGIC_NAME] = 'ロジック名は必須です。ロジックの__nameにロジック名を空でない文字列で設定して下さい。';
errMsgMap[ERR_CODE_LOGIC_ALREADY_CREATED] = '指定されたオブジェクトは既にロジック化されています。';
errMsgMap[ERR_CODE_EXPOSE_NAME_REQUIRED] = 'コントローラ、もしくはロジックの __name が設定されていません。';
errMsgMap[ERR_CODE_NOT_VIEW] = 'テンプレートはViewモジュールがなければ使用できません。';
errMsgMap[ERR_CODE_BIND_TARGET_ILLEGAL] = 'コントローラ"{0}"のバインド対象には、セレクタ文字列、または、オブジェクトを指定してください。';
errMsgMap[ERR_CODE_BIND_ROOT_ONLY] = 'コントローラのbind(), unbind()はルートコントローラでのみ使用可能です。';
errMsgMap[ERR_CODE_CONTROLLER_TOO_FEW_ARGS] = 'h5.core.controller()メソッドは、バインドターゲットとコントローラ定義オブジェクトの2つが必須です。';
errMsgMap[ERR_CODE_CONTROLLER_REJECTED_BY_USER] = 'コントローラ"{0}"の初期化処理がユーザによって中断されました。';
addFwErrorCodeMap(errMsgMap);
/* del end */
// =========================================================================
//
// Cache
//
// =========================================================================
// TODO 高速化のために他で定義されている関数などを変数に入れておく場合はここに書く
// =========================================================================
//
// Privates
//
// =========================================================================
// =============================
// Variables
// =============================
/**
* commonFailHandlerを発火させないために登録するdummyのfailハンドラ
*/
var dummyFailHandler = function() {
//
};
var getDeferred = h5.async.deferred;
var startsWith = h5.u.str.startsWith;
var endsWith = h5.u.str.endsWith;
var format = h5.u.str.format;
var argsToArray = h5.u.obj.argsToArray;
var getByPath = h5.u.obj.getByPath;
/**
* セレクタのタイプを表す定数 イベントコンテキストの中に格納する
*/
var selectorTypeConst = {
SELECTOR_TYPE_LOCAL: 1,
SELECTOR_TYPE_GLOBAL: 2,
SELECTOR_TYPE_OBJECT: 3
};
/**
* マウス/タッチイベントについてh5track*イベントをトリガしたかどうかを管理するため、イベントを格納する配列
*/
var storedEvents = [];
/**
* あるマウス/タッチイベントについてh5track*イベントをトリガ済みかのフラグを保持する配列<br>
* storedEventsに格納されているイベントオブジェクトに対応して、<br>
* [true, false, false] のように格納されている。
*/
var h5trackTriggeredFlags = [];
// =============================
// Functions
// =============================
/**
* セレクタのタイプを表す定数 イベントコンテキストの中に格納する
*/
function EventContext(controller, event, evArg, selector, selectorType) {
this.controller = controller;
this.event = event;
this.evArg = evArg;
this.selector = selector;
this.selectorType = selectorType;
}
// prototypeにセレクタのタイプを表す定数を追加
$.extend(EventContext.prototype, selectorTypeConst);
/**
* コントローラのexecuteListenersを見てリスナーを実行するかどうかを決定するインターセプタ。
*
* @param {Object} invocation インヴォケーション.
*/
function executeListenersInterceptor(invocation) {
if (!this.__controllerContext.executeListeners) {
return;
}
return invocation.proceed();
}
/**
* 指定されたオブジェクトの関数にアスペクトを織り込みます。
*
* @param {Object} controllerDefObject オブジェクト.
* @param {Object} prop プロパティ名.
* @param {Boolean} isEventHandler イベントハンドラかどうか.
* @returns {Object} AOPに必要なメソッドを織り込んだオブジェクト.
*/
function weaveControllerAspect(controllerDefObject, prop, isEventHandler) {
var interceptors = getInterceptors(controllerDefObject.__name, prop);
// イベントハンドラの場合、 enable/disableListeners()のために一番外側に制御用インターセプタを織り込む
if (isEventHandler) {
interceptors.push(executeListenersInterceptor);
}
return createWeavedFunction(controllerDefObject[prop], prop, interceptors);
}
/**
* 関数名とポイントカットを比べて、条件に合致すればインターセプタを返す.
*
* @param {String} targetName バインドする必要のある関数名.
* @param {Object} pcName ポイントカットで判別する対象名.
* @returns {Function[]} AOP用関数配列.
*/
function getInterceptors(targetName, pcName) {
/** @type Any */
var ret = [];
var aspects = h5.settings.aspects;
// 織り込むべきアスペクトがない場合はそのまま空の配列を返す
if (!aspects || aspects.length === 0) {
return ret;
}
aspects = wrapInArray(aspects);
for ( var i = aspects.length - 1; -1 < i; i--) {
var aspect = aspects[i];
if (aspect.target && !aspect.compiledTarget.test(targetName)) {
continue;
}
var interceptors = aspect.interceptors;
if (aspect.pointCut && !aspect.compiledPointCut.test(pcName)) {
continue;
}
if (!$.isArray(interceptors)) {
ret.push(interceptors);
continue;
}
for ( var j = interceptors.length - 1; -1 < j; j--) {
ret = ret.concat(interceptors[j]);
}
}
return ret;
}
/**
* 基本となる関数にアスペクトを織り込んだ関数を返します。
*
* @param {Function} baseFunc 基本関数.
* @param {String} funcName 基本関数名.
* @param {Function[]} aspects AOP用関数配列.
* @returns {Function} AOP用関数を織り込んだ関数.
*/
function createWeavedFunction(base, funcName, aspects) {
// 関数のウィービングを行う
var weave = function(baseFunc, fName, aspect) {
return function(/* var_args */) {
var that = this;
var invocation = {
target: that,
func: baseFunc,
funcName: fName,
args: arguments,
proceed: function() {
return baseFunc.apply(that, this.args);
}
};
return aspect.call(that, invocation);
};
};
var f = base;
for ( var i = 0, l = aspects.length; i < l; i++) {
f = weave(f, funcName, aspects[i]);
}
return f;
}
/**
* 指定されたオブジェクトの関数にアスペクトを織り込みます。
*
* @param {Object} logic ロジック.
* @returns {Object} AOPに必要なメソッドを織り込んだロジック.
*/
function weaveLogicAspect(logic) {
for ( var prop in logic) {
if ($.isFunction(logic[prop])) {
logic[prop] = createWeavedFunction(logic[prop], prop, getInterceptors(logic.__name,
prop));
} else {
logic[prop] = logic[prop];
}
}
return logic;
}
/**
* コントローラ定義オブジェクトのプロパティがライフサイクルイベントどうかを返します。
*
* @param {Object} controllerDefObject コントローラ定義オブジェクト
* @param {String} prop プロパティ名
* @returns {Boolean} コントローラ定義オブジェクトのプロパティがライフサイクルイベントかどうか
*/
function isLifecycleProperty(controllerDefObject, prop) {
// $.isFunction()による判定はいらないかも。
return (prop === '__ready' || prop === '__construct' || prop === '__init')
&& $.isFunction(controllerDefObject[prop]);
}
/**
* セレクタがコントローラの外側の要素を指しているかどうかを返します。<br>
* (外側の要素 = true)
*
* @param {String} selector セレクタ
* @returns {Boolean} コントローラの外側の要素を指しているかどうか
*/
function isGlobalSelector(selector) {
return !!selector.match(/^\{.*\}$/);
}
/**
* イベント名がjQuery.bindを使って要素にイベントをバインドするかどうかを返します。
*
* @param {String} eventName イベント名
* @returns {Boolean} jQuery.bindを使って要素にイベントをバインドするかどうか
*/
function isBindRequested(eventName) {
return !!eventName.match(/^\[.*\]$/);
}
/**
* セレクタから{}を外した文字列を返します。
*
* @param {String} selector セレクタ
* @returns {String} セレクタから{}を外した文字列
*/
function trimGlobalSelectorBracket(selector) {
return $.trim(selector.substring(1, selector.length - 1));
}
/**
* イベント名から[]を外した文字列を返す
*
* @param {String} eventName イベント名
* @returns {String} イベント名から[]を外した文字列
*/
function trimBindEventBracket(eventName) {
return $.trim(eventName.substring(1, eventName.length - 1));
}
/**
* 指定されたセレクタがwindow, window., document, document., navigator, navigator. で
* 始まっていればそのオブジェクトを、そうでなければそのまま文字列を返します。
*
* @param {String} selector セレクタ
* @returns {Object|String} パスで指定されたオブジェクト、もしくは未変換の文字列
*/
function getGlobalSelectorTarget(selector) {
var specialObj = ['window', 'document', 'navigator'];
for ( var i = 0, len = specialObj.length; i < len; i++) {
var s = specialObj[i];
if (selector === s) {
//特殊オブジェクトそのものを指定された場合
return getByPath(selector);
}
if (startsWith(selector, s + '.')) {
//window. などドット区切りで続いている場合
return getByPath(selector);
}
}
return selector;
}
/**
* 指定されたプロパティがイベントハンドラかどうかを返します。
*
* @param {Object} controllerDefObject コントローラ定義オブジェクト
* @param {String} prop プロパティ名
* @returns {Boolean} プロパティがイベントハンドラかどうか
*/
function isEventHandler(controllerDefObject, prop) {
return prop.indexOf(' ') !== -1 && $.isFunction(controllerDefObject[prop]);
}
/**
* コントローラ定義オブジェクトの子孫コントローラ定義が循環参照になっているかどうかをチェックします。
*
* @param {Object} controllerDefObject コントローラ定義オブジェクト
* @returns {Boolean} 循環参照になっているかどうか(true=循環参照)
*/
function checkControllerCircularRef(controllerDefObject) {
var checkCircular = function(controllerDef, ancestors) {
for ( var prop in controllerDef)
if ($.inArray(controllerDef, ancestors) >= 0 || endsWith(prop, SUFFIX_CONTROLLER)
&& checkCircular(controllerDef[prop], ancestors.concat([controllerDef]))) {
return true;
}
return false;
};
return checkCircular(controllerDefObject, []);
}
/**
* コントローラ定義オブジェクトのロジック定義が循環参照になっているかどうかをチェックします。
*
* @param {Object} controllerDefObject コントローラ定義オブジェクト
* @returns {Boolean} 循環参照になっているかどうか(true=循環参照)
*/
function checkLogicCircularRef(controllerDefObj) {
var checkCircular = function(controllerDef, ancestors) {
for ( var prop in controllerDef)
if ($.inArray(controllerDef, ancestors) >= 0 || endsWith(prop, SUFFIX_LOGIC)
&& checkCircular(controllerDef[prop], ancestors.concat([controllerDef]))) {
return true;
}
return false;
};
return checkCircular(controllerDefObj, []);
}
/**
* コントローラのプロパティが子コントローラかどうかを返します。
*
* @param {Object} controller コントローラ
* @param {String} プロパティ名
* @returns {Boolean} コントローラのプロパティが子コントローラかどうか(true=子コントローラである)
*/
function isChildController(controller, prop) {
var target = controller[prop];
return endsWith(prop, SUFFIX_CONTROLLER) && prop !== 'rootController'
&& prop !== 'parentController' && !$.isFunction(target)
&& (target && !target.__controllerContext.isRoot);
}
/**
* 指定されたコントローラの子孫コントローラのPromiseオブジェクトを全て取得します。
*
* @param {Object} controller コントローラ
* @param {String} propertyName プロパティ名(initPromise,readyPromise)
* @param {Object} aquireFromControllerContext コントローラコンテキストのプロパティかどうか
* @returns {Promise[]} Promiseオブジェクト配列
*/
function getDescendantControllerPromises(controller, propertyName, aquireFromControllerContext) {
var promises = [];
var targets = [];
var getPromisesInner = function(object) {
targets.push(object);
for ( var prop in object) {
if (isChildController(object, prop)) {
var c = object[prop];
var promise = aquireFromControllerContext ? c.__controllerContext[propertyName]
: c[propertyName];
if (promise) {
promises.push(promise);
}
if ($.inArray(c, targets) === -1) {
getPromisesInner(c);
}
}
}
};
getPromisesInner(controller);
return promises;
}
/**
* 子孫コントローラのイベントハンドラをバインドします。
*
* @param {Controller} controller コントローラ
*/
function bindDescendantHandlers(controller) {
var execute = function(controllerInstance) {
var meta = controllerInstance.__meta;
var notBindControllers = {};
if (meta) {
for ( var prop in meta) {
if (meta[prop].useHandlers === false) {
// trueより文字数が少ないため1を代入。機能的には"true"を表せば何を代入しても良い。
notBindControllers[prop] = 1;
}
}
}
for ( var prop in controllerInstance) {
var c = controllerInstance[prop];
if (!isChildController(controllerInstance, prop)) {
continue;
}
execute(c);
if (!notBindControllers[prop]) {
bindByBindMap(c);
}
}
};
execute(controller);
}
/**
* バインドマップに基づいてイベントハンドラをバインドします。
*
* @param {Controller} controller コントローラ
*/
function bindByBindMap(controller) {
var bindMap = controller.__controllerContext.bindMap;
for ( var s in bindMap) {
for ( var e in bindMap[s]) {
(function(selector, eventName) {
bindEventHandler(controller, selector, eventName);
})(s, e);
}
}
}
/**
* イベントハンドラのバインドを行います。
*
* @param {Controller} controller コントローラ
* @param {String} selector セレクタ
* @param {String} eventName イベント名
*/
function bindEventHandler(controller, selector, eventName) {
// bindMapに格納しておいたハンドラを取得
var func = controller.__controllerContext.bindMap[selector][eventName];
var event = eventName;
var bindRequested = isBindRequested(eventName);
if (bindRequested) {
event = trimBindEventBracket(eventName);
}
var bindObj = null;
switch (event) {
case 'mousewheel':
bindObj = getNormalizeMouseWheelBindObj(controller, selector, event, func);
break;
case EVENT_NAME_H5_TRACKSTART:
case EVENT_NAME_H5_TRACKMOVE:
case EVENT_NAME_H5_TRACKEND:
bindObj = getH5TrackBindObj(controller, selector, eventName, func);
break;
default:
bindObj = getNormalBindObj(controller, selector, event, func);
break;
}
if (!$.isArray(bindObj)) {
useBindObj(bindObj, bindRequested);
return;
}
for ( var i = 0, l = bindObj.length; i < l; i++) {
useBindObj(bindObj[i], bindRequested);
}
}
/**
* バインドオブジェクトに基づいてイベントハンドラをバインドします。
*
* @param {Object} bindObj バインドオブジェクト
*/
function bindByBindObject(bindObj) {
var controller = bindObj.controller;
var rootElement = controller.rootElement;
var selector = bindObj.selector;
var eventName = bindObj.eventName;
var handler = bindObj.handler;
var useBind = isBindRequested(eventName);
var event = useBind ? trimBindEventBracket(eventName) : eventName;
if (isGlobalSelector(selector)) {
// グローバルなセレクタの場合
var selectorTrimmed = trimGlobalSelectorBracket(selector);
var isSelf = false;
var selectTarget;
if (selectorTrimmed === ROOT_ELEMENT_NAME) {
selectTarget = rootElement;
isSelf = true;
} else {
selectTarget = getGlobalSelectorTarget(selectorTrimmed);
}
// バインド対象がオブジェクトの場合、必ず直接バインドする
if (isSelf || useBind || !isString(selectTarget)) {
// bindObjにselectorTypeを登録する
bindObj.evSelectorType = selectorTypeConst.SELECTOR_TYPE_OBJECT;
$(selectTarget).bind(event, handler);
} else {
// bindObjにselectorTypeを登録する
bindObj.evSelectorType = selectorTypeConst.SELECTOR_TYPE_GLOBAL;
$(document).delegate(selectTarget, event, handler);
}
// selectorがグローバル指定の場合はcontext.selectorに{}を取り除いた文字列を格納する
// selectorがオブジェクト指定(rootElement, window, document)の場合はオブジェクトを格納する
bindObj.evSelector = selectTarget;
} else {
// selectorがグローバル指定でない場合
// bindObjにselectorTypeを登録し、selectorは文字列を格納する
bindObj.evSelectorType = selectorTypeConst.SELECTOR_TYPE_LOCAL;
bindObj.evSelector = selector;
if (useBind) {
$(selector, rootElement).bind(event, handler);
} else {
$(rootElement).delegate(selector, event, handler);
}
}
}
/**
* バインドオブジェクトに対して必要であればイベント名を修正し、アンバインドマップにハンドラを追加した後、 実際にバインドを行います。
*
* @param {Object} bindObj バインドオブジェクト
* @param {Boolean} bindRequested イベントハンドラをバインド([]記法)すべきかどうか
*/
function useBindObj(bindObj, bindRequested) {
if (bindRequested) {
bindObj.eventName = '[' + bindObj.eventName + ']';
}
// イベントコンテキストを作成してからハンドラを呼び出すようにhandlerをラップする
// unbindMapにラップしたものが登録されるように、このタイミングで行う必要がある
var handler = bindObj.handler;
bindObj.handler = function(/* var args */) {
var currentTargetShortcut = h5.settings.listenerElementType === 1 ? $(arguments[0].currentTarget)
: arguments[0].currentTarget;
handler.call(bindObj.controller, createEventContext(bindObj, arguments),
currentTargetShortcut);
};
// アンバインドマップにハンドラを追加
registerUnbindMap(bindObj.controller, bindObj.selector, bindObj.eventName, bindObj.handler);
bindByBindObject(bindObj);
}
/**
* 子孫コントローラのイベントハンドラをアンバインドします。
*
* @param {Controller} controller コントローラ
*/
function unbindDescendantHandlers(controller) {
var execute = function(controllerInstance) {
var meta = controllerInstance.__meta;
var notBindControllers = {};
if (meta) {
for ( var prop in meta) {
if (meta[prop].useHandlers === false) {
// trueより文字数が少ないため1を代入。機能的には"true"を表せば何を代入しても良い。
notBindControllers[prop] = 1;
}
}
}
for ( var prop in controllerInstance) {
var c = controllerInstance[prop];
if (!isChildController(controllerInstance, prop)) {
continue;
}
execute(c);
if (!notBindControllers[prop]) {
unbindByBindMap(c);
}
}
};
execute(controller);
}
/**
* バインドマップに基づいてイベントハンドラをアンバインドします。
*
* @param {Controller} controller コントローラ
*/
function unbindByBindMap(controller) {
var rootElement = controller.rootElement;
var unbindMap = controller.__controllerContext.unbindMap;
for ( var selector in unbindMap) {
for ( var eventName in unbindMap[selector]) {
var handler = unbindMap[selector][eventName];
var useBind = isBindRequested(eventName);
var event = useBind ? trimBindEventBracket(eventName) : eventName;
if (isGlobalSelector(selector)) {
var selectTarget = trimGlobalSelectorBracket(selector);
var isSelf = false;
if (selectTarget === ROOT_ELEMENT_NAME) {
selectTarget = rootElement;
isSelf = true;
} else {
selectTarget = getGlobalSelectorTarget(selectTarget);
}
if (isSelf || useBind || !isString(selectTarget)) {
$(selectTarget).unbind(event, handler);
} else {
$(document).undelegate(selectTarget, event, handler);
}
} else {
if (useBind) {
$(selector, rootElement).unbind(event, handler);
} else {
$(rootElement).undelegate(selector, event, handler);
}
}
}
}
}
/**
* 指定されたフラグで子コントローラを含む全てのコントローラのexecuteListenersフラグを変更します。
*
* @param {Controller} controller コントローラ
* @param {Boolean} flag フラグ
*/
function setExecuteListenersFlag(controller, flag) {
controller.__controllerContext.executeListeners = flag;
var targets = [];
var changeFlag = function(controllerInstance) {
targets.push(controllerInstance);
for ( var prop in controllerInstance) {
if (isChildController(controllerInstance, prop)) {
var c = controllerInstance[prop];
c.__controllerContext.executeListeners = flag;
if ($.inArray(c, targets) === -1) {
changeFlag(c);
}
}
}
};
changeFlag(controller);
}
/**
* rootControllerとparentControllerをセットします。
*
* @param {Controller} controller コントローラ
*/
function initRootAndParentController(controller) {
var targets = [];
var init = function(controllerInstance, root, parent) {
controllerInstance.rootController = root;
controllerInstance.parentController = parent;
targets.push(controllerInstance);
for ( var prop in controllerInstance) {
if (isChildController(controllerInstance, prop)) {
var c = controllerInstance[prop];
if ($.inArray(c, targets) === -1) {
init(c, root, controllerInstance);
}
}
}
};
init(controller, controller, null);
}
/**
* __init, __readyイベントを実行する.
*
* @param {Object} controller コントローラ.
* @param {Booelan} isInitEvent __initイベントを実行するかどうか.
*/
function executeLifecycleEventChain(controller, isInitEvent) {
var funcName = isInitEvent ? '__init' : '__ready';
var leafDfd = getDeferred();
setTimeout(function() {
leafDfd.resolve();
}, 0);
var leafPromise = leafDfd.promise();
var execInner = function(controllerInstance) {
var isLeafController = true;
for ( var prop in controllerInstance) {
// 子コントローラがあれば再帰的に処理
if (isChildController(controllerInstance, prop)) {
isLeafController = false;
execInner(controllerInstance[prop]);
}
}
// 子孫コントローラの準備ができた時に実行させる関数を定義
var func = function() {
var ret = null;
var lifecycleFunc = controllerInstance[funcName];
var controllerName = controllerInstance.__name;
if (lifecycleFunc) {
try {
ret = controllerInstance[funcName]
(createInitializationContext(controllerInstance));
} catch (e) {
// __init, __readyで例外が投げられた
fwLogger.error(FW_LOG_INIT_CONTROLLER_THROWN_ERROR, controllerName,
isInitEvent ? '__init' : '__ready');
// 同じrootControllerを持つ他の子のdisposeによって、
// controller.rootControllerがnullになっている場合があるのでそのチェックをしてからdisposeする
controller.rootController && controller.rootController.dispose(arguments);
// dispose処理が終わったら例外を投げる
throw e;
}
}
// ライフサイクルイベント実行後に呼ぶべきコールバック関数を作成
var callback = isInitEvent ? createCallbackForInit(controllerInstance)
: createCallbackForReady(controllerInstance);
if (ret && $.isFunction(ret.promise)) {
// __init, __ready がpromiseを返している場合
ret.done(function() {
callback();
}).fail(
function(/* var_args */) {
// rejectされた場合は連鎖的にdisposeする
fwLogger.error(FW_LOG_INIT_CONTROLLER_REJECTED, controllerName,
isInitEvent ? '__init' : '__ready');
fwLogger.error(FW_LOG_INIT_CONTROLLER_ERROR,
controller.rootController.__name);
var failReason = createRejectReason(
ERR_CODE_CONTROLLER_REJECTED_BY_USER, controllerName,
argsToArray(arguments));
// 同じrootControllerを持つ他の子のdisposeによって、
// controller.rootControllerがnullになっている場合があるのでそのチェックをしてからdisposeする
controller.rootController
&& controller.rootController.dispose(failReason);
});
} else {
callback();
}
};
// getPromisesForXXXの戻り値が空の配列の場合はfunc()は同期的に呼ばれる
var promises = isInitEvent ? getPromisesForInit(controllerInstance)
: getPromisesForReady(controllerInstance);
if (isInitEvent && isLeafController) {
promises.push(leafPromise);
}
// dfdがrejectされたとき、commonFailHandlerが発火しないようにするため、dummyのfailハンドラを登録する
h5.async.when(promises).done(function() {
func();
}).fail(dummyFailHandler);
};
execInner(controller);
}
/**
* __initイベントを実行するために必要なPromiseを返します。
*
* @param {Controller} controller コントローラ
* @returns {Promise[]} Promiseオブジェクト
*/
function getPromisesForInit(controller) {
// 子孫コントローラのinitPromiseオブジェクトを取得
var initPromises = getDescendantControllerPromises(controller, 'initPromise');
// 自身のテンプレート用Promiseオブジェクトを取得
initPromises.push(controller.preinitPromise);
return initPromises;
}
/**
* __readyイベントを実行するために必要なPromiseを返します。
*
* @param {Controller} controller コントローラ
* @returns {Promise[]} Promiseオブジェクト
*/
function getPromisesForReady(controller) {
// 子孫コントローラのreadyPromiseオブジェクトを取得
return getDescendantControllerPromises(controller, 'readyPromise');
}
/**
* __initイベントで実行するコールバック関数を返します。
*
* @param {Controller} controller コントローラ
*/
function createCallbackForInit(controller) {
return function() {
// disopseされていたら何もしない。
if (isDisposing(controller)) {
return;
}
controller.isInit = true;
var initDfd = controller.__controllerContext.initDfd;
// FW、ユーザともに使用しないので削除
delete controller.__controllerContext.templatePromise;
delete controller.__controllerContext.preinitDfd;
delete controller.__controllerContext.initDfd;
initDfd.resolveWith(controller);
if (controller.__controllerContext && controller.__controllerContext.isRoot) {
// ルートコントローラであれば次の処理(イベントハンドラのバインドと__readyの実行)へ進む
bindAndTriggerReady(controller);
}
};
}
/**
* __readyイベントで実行するコールバック関数を返します。
*
* @param {Controller} controller コントローラ
*/
function createCallbackForReady(controller) {
return function() {
// disopseされていたら何もしない。
if (isDisposing(controller)) {
return;
}
controller.isReady = true;
var readyDfd = controller.__controllerContext.readyDfd;
// FW、ユーザともに使用しないので削除
delete controller.__controllerContext.readyDfd;
readyDfd.resolveWith(controller);
if (controller.__controllerContext && controller.__controllerContext.isRoot) {
// ルートコントローラであれば全ての処理が終了したことを表すイベント"h5controllerready"をトリガ
if (!controller.rootElement || !controller.isInit || !controller.isReady) {
return;
}
$(controller.rootElement).trigger('h5controllerready', controller);
}
};
}
/**
* テンプレートに渡すセレクタとして正しいかどうかを返します。
*
* @param {String} selector セレクタ
* @returns {Boolean} テンプレートに渡すセレクタとして正しいかどうか(true=正しい)
*/
function isCorrectTemplatePrefix(selector) {
if (startsWith(selector, 'window')) {
return false;
}
if (startsWith(selector, 'navigator')) {
return false;
}
return true;
}
/**
* 指定された要素が文字列があれば、ルートエレメント、{}記法を考慮した要素をjQueryオブジェクト化して返します。 DOM要素、jQueryオブジェクトであれば、
* jQueryオブジェクト化して(指定要素がjQueryオブジェクトの場合、無駄な処理になるがコスト的には問題ない)返します。
*
* @param {String|DOM|jQuery} セレクタ、DOM要素、jQueryオブジェクト
* @param {DOM} rootElement ルートエレメント
* @param {Boolean} isTemplate テンプレートで使用するかどうか
* @returns {jQuery} jQueryオブジェクト
*/
function getTarget(element, rootElement, isTemplate) {
if (!isString(element)) {
return $(element);
}
var $targets;
var selector = $.trim(element);
if (isGlobalSelector(selector)) {
var s = trimGlobalSelectorBracket(selector);
if (isTemplate && !isCorrectTemplatePrefix(s)) {
throwFwError(ERR_CODE_INVALID_TEMPLATE_SELECTOR);
}
$targets = $(getGlobalSelectorTarget(s));
} else {
$targets = $(rootElement).find(element);
}
return $targets;
}
/**
* ハンドラをアンバインドマップに登録します。
*
* @param {Controller} controller コントローラ
* @param {String} selector セレクタ
* @param {String} eventName イベント名
* @param {Function} handler ハンドラ
*/
function registerUnbindMap(controller, selector, eventName, handler) {
if (!controller.__controllerContext.unbindMap[selector]) {
controller.__controllerContext.unbindMap[selector] = {};
}
controller.__controllerContext.unbindMap[selector][eventName] = handler;