1 /**
  2  * @fileOverview Base Controller class for KISSY Component.
  3  * @author yiminghe@gmail.com
  4  */
  5 KISSY.add("component/controller", function (S, Event, Component, UIBase, Manager, Render, undefined) {
  6 
  7     function wrapperViewSetter(attrName) {
  8         return function (ev) {
  9             var self = this;
 10             // in case bubbled from sub component
 11             if (self == ev.target) {
 12                 var value = ev.newVal,
 13                     view = self.get("view");
 14                 view && view.set(attrName, value);
 15             }
 16         };
 17     }
 18 
 19     function wrapperViewGetter(attrName) {
 20         return function (v) {
 21             var self = this,
 22                 view = self.get("view");
 23             return v === undefined ? view && view.get(attrName) : v;
 24         };
 25     }
 26 
 27     function initChild(self, c, elBefore) {
 28         // 生成父组件的 dom 结构
 29         self.create();
 30         var contentEl = self.getContentElement();
 31         c = Component.create(c, self);
 32         c.__set("parent", self);
 33         // set 通知 view 也更新对应属性
 34         c.set("render", contentEl);
 35         c.set("elBefore", elBefore);
 36         // 如果 parent 已经渲染好了子组件也要立即渲染,就 创建 dom ,绑定事件
 37         if (self.get("rendered")) {
 38             c.render();
 39         }
 40         // 如果 parent 也没渲染,子组件 create 出来和 parent 节点关联
 41         // 子组件和 parent 组件一起渲染
 42         else {
 43             // 之前设好属性,view ,logic 同步还没 bind ,create 不是 render ,还没有 bindUI
 44             c.create(undefined);
 45         }
 46         return c;
 47     }
 48 
 49     /**
 50      * 不使用 valueFn,
 51      * 只有 render 时需要找到默认,其他时候不需要,防止莫名其妙初始化
 52      */
 53     function constructView(self) {
 54         // 逐层找默认渲染器
 55         var attrs,
 56             attrCfg,
 57             attrName,
 58             cfg = {},
 59             v,
 60             Render = self.get('xrender');
 61 
 62         if (Render) {
 63             /**
 64              * 将渲染层初始化所需要的属性,直接构造器设置过去
 65              */
 66             attrs = self.getAttrs();
 67 
 68             // 整理属性,对纯属于 view 的属性,添加 getter setter 直接到 view
 69             for (attrName in attrs) {
 70                 if (attrs.hasOwnProperty(attrName)) {
 71                     attrCfg = attrs[attrName];
 72                     if (attrCfg.view) {
 73 
 74                         // 先取后 getter
 75                         // 防止死循环
 76                         if (( v = self.get(attrName) ) !== undefined) {
 77                             cfg[attrName] = v;
 78                         }
 79 
 80                         // setter 不应该有实际操作,仅用于正规化比较好
 81                         // attrCfg.setter = wrapperViewSetter(attrName);
 82                         self.on("after" + S.ucfirst(attrName) + "Change",
 83                             wrapperViewSetter(attrName));
 84                         // 逻辑层读值直接从 view 层读
 85                         // 那么如果存在默认值也设置在 view 层
 86                         // 逻辑层不要设置 getter
 87                         attrCfg.getter = wrapperViewGetter(attrName);
 88                     }
 89                 }
 90             }
 91             return new Render(cfg);
 92         }
 93         return 0;
 94     }
 95 
 96     function setViewCssClassByHierarchy(self, view) {
 97         var constructor = self.constructor,
 98             re = [];
 99         while (constructor && constructor != Controller) {
100             var cls = Manager.getXClassByConstructor(constructor);
101             if (cls) {
102                 re.push(cls);
103             }
104             constructor = constructor.superclass && constructor.superclass.constructor;
105         }
106         return view.__componentClasses = re.join(" ");
107     }
108 
109     function isMouseEventWithinElement(e, elem) {
110         var relatedTarget = e.relatedTarget;
111         // 在里面或等于自身都不算 mouseenter/leave
112         return relatedTarget &&
113             ( relatedTarget === elem[0] ||
114                 elem.contains(relatedTarget) );
115     }
116 
117     /**
118      * @memberOf Component
119      * @name Controller
120      * @extends Component.UIBase
121      * @extends Component.UIBase.Box
122      * @class
123      * Base Controller class for KISSY Component.
124      * xclass: 'controller'.
125      */
126     var Controller = UIBase.extend([UIBase.Box],
127         /** @lends Component.Controller# */
128         {
129 
130             /**
131              * Get full class name for current component
132              * @param classes {String} class names without prefixCls. Separated by space.
133              * @function
134              * @return {String} class name with prefixCls
135              */
136             getCssClassWithPrefix:Manager.getCssClassWithPrefix,
137 
138             /**
139              * From UIBase, Initialize this component.
140              * @override
141              * @protected
142              */
143             initializer:function () {
144                 // initialize view
145                 this.get("view");
146             },
147 
148             /**
149              * From UIBase. Constructor(or get) view object to create ui elements.
150              * @protected
151              * @override
152              */
153             createDom:function () {
154                 var self = this,
155                     view = self.get("view");
156                 setViewCssClassByHierarchy(self, view);
157                 view.create();
158                 var el = view.getKeyEventTarget();
159                 if (self.get("focusable")) {
160                     el.attr("tabIndex", 0);
161                 } else {
162                     el.unselectable(undefined);
163                 }
164                 self.__set("view", view);
165             },
166 
167             /**
168              * From UIBase. Call view object to render ui elements.
169              * @protected
170              * @override
171              */
172             renderUI:function () {
173                 var self = this, i, children, child;
174                 self.get("view").render();
175                 //then render my children
176                 children = self.get("children");
177                 for (i = 0; i < children.length; i++) {
178                     child = children[i];
179                     // 不在 Base 初始化设置属性时运行,防止和其他初始化属性冲突
180                     child = initChild(self, child);
181                     children[i] = child;
182                     child.render();
183                 }
184             },
185 
186             /**
187              * From UIBase. Bind focus event if component is focusable.
188              * @protected
189              * @override
190              */
191             bindUI:function () {
192                 var self = this,
193                     focusable = self.get("focusable"),
194                     handleMouseEvents = self.get("handleMouseEvents"),
195                     el = self.getKeyEventTarget();
196                 if (focusable) {
197                     el.on("focus", self.handleFocus, self)
198                         .on("blur", self.handleBlur, self)
199                         .on("keydown", self.handleKeydown, self);
200                 }
201                 if (handleMouseEvents) {
202                     el = self.get("el");
203                     el.on("mouseenter", self.handleMouseEnter, self)
204                         .on("mouseleave", self.handleMouseLeave, self)
205                         .on("mousedown", self.handleMouseDown, self)
206                         .on("mouseup", self.handleMouseUp, self)
207                         .on("dblclick", self.handleDblClick, self);
208                 }
209             },
210 
211             _uiSetFocused:function (v) {
212                 if (v) {
213                     this.getKeyEventTarget()[0].focus();
214                 }
215             },
216 
217             /**
218              * 子组件将要渲染到的节点,在 render 类上覆盖对应方法
219              * @private
220              */
221             getContentElement:function () {
222                 var view = this.get('view');
223                 return view && view.getContentElement();
224             },
225 
226             /**
227              * 焦点所在元素即键盘事件处理元素,在 render 类上覆盖对应方法
228              * @private
229              */
230             getKeyEventTarget:function () {
231                 var view = this.get('view');
232                 return view && view.getKeyEventTarget();
233             },
234 
235             /**
236              * Add the specified component as a child of current component
237              * at the given 0-based index.
238              * @param {Component.Controller|Object} c
239              * Child component instance to be added
240              * or
241              * Object describe child component
242              * @param {String} [c.xclass] When c is a object, specify its child class.
243              * @param {Number} [index]  0-based index at which
244              * the new child component is to be inserted;
245              * If not specified , the new child component will be inserted at last position.
246              */
247             addChild:function (c, index) {
248                 var self = this,
249                     children = self.get("children"),
250                     elBefore;
251                 if (index === undefined) {
252                     index = children.length;
253                 }
254                 elBefore = children[index] && children[index].get("el") || null;
255                 c = initChild(self, c, elBefore);
256                 children.splice(index, 0, c);
257                 return c;
258             },
259 
260             /**
261              * Removed the given child from this component,and returns it.
262              *
263              * If destroy is true, calls {@link Component.UIBase.#destroy} on the removed child component,
264              * and subsequently detaches the child's DOM from the document.
265              * Otherwise it is the caller's responsibility to
266              * clean up the child component's DOM.
267              *
268              * @param {Component.Controller} c The child component to be removed.
269              * @param {Boolean} [destroy=false] If true,
270              * calls {@link Component.UIBase.#destroy} on the removed child component.
271              * @return {Component.Controller} The removed component.
272              */
273             removeChild:function (c, destroy) {
274                 var children = this.get("children"),
275                     index = S.indexOf(c, children);
276                 if (index != -1) {
277                     children.splice(index, 1);
278                 }
279                 if (destroy &&
280                     // c is still json
281                     c.destroy) {
282                     c.destroy();
283                 }
284                 return c;
285             },
286 
287             /**
288              * Removes every child component attached to current component.
289              * @see Component.Controller#removeChild
290              * @param {Boolean} [destroy] If true,
291              * calls {@link Component.UIBase.#destroy} on the removed child component.
292              */
293             removeChildren:function (destroy) {
294                 var self = this,
295                     i,
296                     t = [].concat(self.get("children"));
297                 for (i = 0; i < t.length; i++) {
298                     self.removeChild(t[i], destroy);
299                 }
300                 self.__set("children", []);
301             },
302 
303             /**
304              * Returns the child at the given index, or null if the index is out of bounds.
305              * @param {Number} index 0-based index.
306              * @return {Component.Controller} The child at the given index; null if none.
307              */
308             getChildAt:function (index) {
309                 var children = this.get("children");
310                 return children[index] || null;
311             },
312 
313             /**
314              * Handle dblclick events. By default, this performs its associated action by calling
315              * {@link Component.Controller#performActionInternal}.
316              * @protected
317              * @param {Event.Object} ev DOM event to handle.
318              */
319             handleDblClick:function (ev) {
320                 var self = this;
321                 if (self.get("disabled")) {
322                     return true;
323                 }
324                 self.performActionInternal(ev);
325             },
326 
327             /**
328              * Called by it's container component to dispatch mouseenter event.
329              * @private
330              * @param {Event.Object} ev DOM event to handle.
331              */
332             handleMouseOver:function (ev) {
333                 var self = this,
334                     el = self.get("el");
335                 if (self.get("disabled")) {
336                     return true;
337                 }
338                 if (!isMouseEventWithinElement(ev, el)) {
339                     self.handleMouseEnter(ev);
340                 }
341             },
342 
343             /**
344              * Called by it's container component to dispatch mouseleave event.
345              * @private
346              * @param {Event.Object} ev DOM event to handle.
347              */
348             handleMouseOut:function (ev) {
349                 var self = this,
350                     el = self.get("el");
351                 if (self.get("disabled")) {
352                     return true;
353                 }
354                 if (!isMouseEventWithinElement(ev, el)) {
355                     self.handleMouseLeave(ev);
356                 }
357             },
358 
359             /**
360              * Handle mouseenter events. If the component is not disabled, highlights it.
361              * @protected
362              * @param {Event.Object} ev DOM event to handle.
363              */
364             handleMouseEnter:function (ev) {
365                 var self = this;
366                 if (self.get("disabled")) {
367                     return true;
368                 }
369                 self.set("highlighted", !!ev);
370             },
371 
372             /**
373              * Handle mouseleave events. If the component is not disabled, de-highlights it.
374              * @protected
375              * @param {Event.Object} ev DOM event to handle.
376              */
377             handleMouseLeave:function (ev) {
378                 var self = this;
379                 if (self.get("disabled")) {
380                     return true;
381                 }
382                 self.set("active", false);
383                 self.set("highlighted", !ev);
384             },
385 
386             /**
387              * Handles mousedown events. If the component is not disabled,
388              * If the component is activeable, then activate it.
389              * If the component is focusable, then focus it,
390              * else prevent it from receiving keyboard focus.
391              * @protected
392              * @param {Event.Object} ev DOM event to handle.
393              */
394             handleMouseDown:function (ev) {
395                 var self = this,
396                     isMouseActionButton = ev['which'] == 1,
397                     el;
398                 if (self.get("disabled")) {
399                     return true;
400                 }
401                 if (isMouseActionButton) {
402                     el = self.getKeyEventTarget();
403                     if (self.get("activeable")) {
404                         self.set("active", true);
405                     }
406                     if (self.get("focusable")) {
407                         el[0].focus();
408                         self.set("focused", true);
409                     } else {
410                         // firefox /chrome 不会引起焦点转移
411                         var n = ev.target.nodeName;
412                         n = n && n.toLowerCase();
413                         // do not prevent focus when click on editable element
414                         if (n != "input" && n != "textarea") {
415                             ev.preventDefault();
416                         }
417                     }
418                 }
419             },
420 
421             /**
422              * Handles mouseup events.
423              * If this component is not disabled, performs its associated action by calling
424              * {@link Component.Controller#performActionInternal}, then deactivates it.
425              * @protected
426              * @param {Event.Object} ev DOM event to handle.
427              */
428             handleMouseUp:function (ev) {
429                 var self = this;
430                 if (self.get("disabled")) {
431                     return true;
432                 }
433                 // 左键
434                 if (self.get("active") && ev.which == 1) {
435                     self.performActionInternal(ev);
436                     self.set("active", false);
437                 }
438             },
439 
440             /**
441              * Handles focus events. Style focused class.
442              * @protected
443              * @param {Event.Object} ev DOM event to handle.
444              */
445             handleFocus:function (ev) {
446                 this.set("focused", !!ev);
447                 this.fire("focus");
448             },
449 
450             /**
451              * Handles blur events. Remove focused class.
452              * @protected
453              * @param {Event.Object} ev DOM event to handle.
454              */
455             handleBlur:function (ev) {
456                 this.set("focused", !ev);
457                 this.fire("blur");
458             },
459 
460             /**
461              * Handle enter keydown event to {@link Component.Controller#performActionInternal}.
462              * @protected
463              * @param {Event.Object} ev DOM event to handle.
464              */
465             handleKeyEventInternal:function (ev) {
466                 if (ev.keyCode == Event.KeyCodes.ENTER) {
467                     return this.performActionInternal(ev);
468                 }
469             },
470 
471             /**
472              * Handle keydown events.
473              * If the component is not disabled, call {@link Component.Controller#handleKeyEventInternal}
474              * @protected
475              * @param {Event.Object} ev DOM event to handle.
476              */
477             handleKeydown:function (ev) {
478                 var self = this;
479                 if (self.get("disabled")) {
480                     return true;
481                 }
482                 if (self.handleKeyEventInternal(ev)) {
483                     ev.halt();
484                 }
485             },
486 
487             /**
488              * Performs the appropriate action when this component is activated by the user.
489              * @protected
490              * @param {Event.Object} ev DOM event to handle.
491              */
492             performActionInternal:function (ev) {
493             },
494 
495             destructor:function () {
496                 var self = this,
497                     i,
498                     view,
499                     children = self.get("children");
500                 for (i = 0; i < children.length; i++) {
501                     children[i].destroy && children[i].destroy();
502                 }
503                 view = self.get("view");
504                 if (view) {
505                     view.destroy();
506                 }
507             }
508         },
509         {
510             ATTRS:/**
511              * @lends Component.Controller#
512              */
513             {
514 
515                 /**
516                  * Enables or disables mouse event handling for the component.
517                  * Containers may set this attribute to disable mouse event handling
518                  * in their child component.
519                  * Default : true.
520                  * @type Boolean
521                  */
522                 handleMouseEvents:{
523                     value:true
524                 },
525 
526                 /**
527                  * Whether this component can get focus.
528                  * Default : true.
529                  * @type Boolean
530                  */
531                 focusable:{
532                     view:1
533                 },
534 
535                 /**
536                  * Whether this component can be activated.
537                  * Default : true.
538                  * @type Boolean
539                  */
540                 activeable:{
541                     value:true
542                 },
543 
544                 /**
545                  * Whether this component has focus.
546                  * @type Boolean
547                  */
548                 focused:{
549                     view:1
550                 },
551 
552                 /**
553                  * Whether this component is activated.
554                  * @type Boolean
555                  */
556                 active:{
557                     view:1
558                 },
559 
560                 /**
561                  * Whether this component is highlighted.
562                  * @type Boolean
563                  */
564                 highlighted:{
565                     view:1
566                 },
567 
568                 /**
569                  * Array of child components
570                  * @type Component.Controller[]
571                  */
572                 children:{
573                     value:[]
574                 },
575 
576                 /**
577                  * This component's prefix css class.
578                  * @type String
579                  */
580                 prefixCls:{
581                     value:'ks-', // box srcNode need
582                     view:1
583                 },
584 
585                 /**
586                  * This component's parent component.
587                  * @type Component.Controller
588                  */
589                 parent:{
590                 },
591 
592                 /**
593                  * Renderer used to render this component.
594                  * @type Component.Render
595                  */
596                 view:{
597                     valueFn:function () {
598                         return constructView(this);
599                     }
600                 },
601 
602                 /**
603                  * Whether this component is disabled.
604                  * @type Boolean
605                  */
606                 disabled:{
607                     view:1
608                 },
609 
610                 xrender:{
611                     value:Render
612                 }
613             }
614         }, {
615             xclass:'controller'
616         });
617 
618     return Controller;
619 }, {
620     requires:['event', './base', './uibase', './manager', './render']
621 });
622 /**
623  * observer synchronization, model 分成两类:
624  *  - view 负责监听 view 类 model 变化更新界面
625  *  - control 负责监听 control 类变化改变逻辑
626  * problem: Observer behavior is hard to understand and debug
627  * because it's implicit behavior.
628  *
629  * Keeping screen state and session state synchronized is an important task
630  * Data Binding.
631  *
632  * In general data binding gets tricky
633  * because if you have to avoid cycles where a change to the control,
634  * changes the record set, which updates the control,
635  * which updates the record set....
636  * The flow of usage helps avoid these -
637  * we load from the session state to the screen when the screen is opened,
638  * after that any changes to the screen state propagate back to the session state.
639  * It's unusual for the session state to be updated directly once the screen is up.
640  * As a result data binding might not be entirely bi-directional -
641  * just confined to initial upload and
642  * then propagating changes from the controls to the session state.
643  *
644  *  Refer
645  *    - http://martinfowler.com/eaaDev/uiArchs.html
646  *
647  *  控制层元属性配置中 view 的作用
648  *   - 如果没有属性变化处理函数,自动生成属性变化处理函数,自动转发给 view 层
649  *   - 如果没有指定 view 层实例,在生成默认 view 实例时,所有用户设置的 view 的属性都转到默认 view 实例中
650  **/