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 **/