- jquery ui 的所有 組件都是基於一個簡單,可重用的widget。
-
這個widget是jquery ui的核心部分,實用它能實現一致的API,創建有狀態的插件,而無需關心插件的內部轉換。
$.widget( name, base, prototype )widget一共有2或3個參數。base為可選。
這里之所以把base放在第二個參數里,主要是因為這樣寫代碼更直觀一些。(因為后面的prototype 是個代碼非常長的大對象)。
name:第一個參數是一個包含一個命名空間和組件名稱的字符串,通過”.”來分割。
命名空間必須有,它指向widget prototype存儲的全局jQuery對象。
如果命名空間沒有,widget factory將會為你生成。widget name是插件函數和原型的真實名稱,
比如: jQuery.widget( “demo.multi”, {…} ) 將會生成 jQuery.demo , jQuery.demo.multi , and jQuery.demo.multi.prototype .base:第二個參數(可選)是 widget prototype繼承於什么對象。
例如jQuery UI有一個“mouse”的插件,它可以作為其他的插件提供的基礎。
為了實現這個所有的基於mouse的插件比如draggable,
droppable可以這么做:jQuery.widget( "ui.draggable", $.ui.mouse, {...} );
如果沒有這個參數,widget默認繼承自“base widget” jQuery.Widget(注意jQuery.widget 和 jQuery.Widget不同) 。prototype:最后一個參數是一個對象文字,它會轉化為所有widget實例的prototype。widget factory會生成屬性鏈,連接到她繼承的widget的prototype。一直到最基本的 jQuery.Widget。
一旦你調用
jQuery.widget,它會在jQuery prototype ( jQuery.fn )上生成一個新的可用方法對應於widget的名字,比如我們這個例子jQuery.fn.multi。 .fn方法是包含Dom元素的jquery對象和你生成的 widget prototyp實例的接口,為每一個jQuery對象生成一個新的widget的實例。1 /*! 2 * jQuery UI Widget @VERSION 3 * http://jqueryui.com 4 * 5 * Copyright 2014 jQuery Foundation and other contributors 6 * Released under the MIT license. 7 * http://jquery.org/license 8 * 9 * http://api.jqueryui.com/jQuery.widget/ 10 */ 11 12 //這里判定是否支持amd or cmd 模式 13 (function(factory) { 14 if (typeof define === "function" && define.amd) { 15 16 // AMD. Register as an anonymous module. 17 define(["jquery"], factory); 18 } else { 19 20 // Browser globals 21 factory(jQuery); 22 } 23 }(function($) { 24 25 var widget_uuid = 0, 26 //插件的實例化數量 27 widget_slice = Array.prototype.slice; //數組的slice方法,這里的作用是將參賽arguments 轉為真正的數組 28 29 //清除插件的數據及緩存 30 $.cleanData = (function(orig) { 31 return function(elems) { 32 for (var i = 0, elem; 33 (elem = elems[i]) != null; i++) { 34 try { 35 // 重寫cleanData方法,調用后觸發每個元素的remove事件 36 $(elem).triggerHandler("remove"); 37 // http://bugs.jquery.com/ticket/8235 38 } catch (e) {} 39 } 40 orig(elems); 41 }; 42 })($.cleanData); 43 44 /** 45 * widget工廠方法,用於創建插件 46 * @param name 包含命名空間的插件名稱,格式 xx.xxx 47 * @param base 需要繼承的ui組件 48 * @param prototype 插件的實際代碼 49 * @returns {Function} 50 */ 51 $.widget = function(name, base, prototype) { 52 var fullName, //插件全稱 53 existingConstructor, //原有的構造函數 54 constructor, //當前構造函數 55 basePrototype, //父類的Prototype 56 // proxiedPrototype allows the provided prototype to remain unmodified 57 // so that it can be used as a mixin for multiple widgets (#8876) 58 proxiedPrototype = {}, 59 //可調用父類方法_spuer的prototype對象,擴展於prototype 60 namespace = name.split(".")[0]; 61 62 name = name.split(".")[1]; 63 fullName = namespace + "-" + name; 64 //如果只有2個參數 base默認為Widget類,組件默認會繼承base類的所有方法 65 if (!prototype) { 66 prototype = base; 67 base = $.Widget; 68 } 69 70 // console.log(base, $.Widget) 71 72 // create selector for plugin 73 //創建一個自定義的偽類選擇器 74 //如 $(':ui-menu') 則表示選擇定義了ui-menu插件的元素 75 $.expr[":"][fullName.toLowerCase()] = function(elem) { 76 return !!$.data(elem, fullName); 77 }; 78 79 // 判定命名空間對象是否存在,沒有的話 則創建一個空對象 80 $[namespace] = $[namespace] || {}; 81 //這里存一份舊版的插件,如果這個插件已經被使用或者定義了 82 existingConstructor = $[namespace][name]; 83 //這個是插件實例化的主要部分 84 //constructor存儲了插件的實例,同時也創建了基於命名空間的對象 85 //如$.ui.menu 86 constructor = $[namespace][name] = function(options, element) { 87 // allow instantiation without "new" keyword 88 //允許直接調用命名空間上的方法來創建組件 89 //比如:$.ui.menu({},'#id') 這種方式創建的話,默認沒有new 實例化。因為_createWidget是prototype上的方法,需要new關鍵字來實例化 90 //通過 調用 $.ui.menu 來實例化插件 91 if (!this._createWidget) { 92 console.info(this) 93 return new constructor(options, element); 94 } 95 96 // allow instantiation without initializing for simple inheritance 97 // must use "new" keyword (the code above always passes args) 98 //如果存在參數,則說明是正常調用插件 99 //_createWidget是創建插件的核心方法 100 if (arguments.length) { 101 this._createWidget(options, element); 102 } 103 }; 104 // extend with the existing constructor to carry over any static properties 105 //合並對象,將舊插件實例,及版本號、prototype合並到constructor 106 $.extend(constructor, existingConstructor, { 107 108 version: prototype.version, 109 // copy the object used to create the prototype in case we need to 110 // redefine the widget later 111 //創建一個新的插件對象 112 //將插件實例暴露給外部,可用戶修改及覆蓋 113 _proto: $.extend({}, prototype), 114 // track widgets that inherit from this widget in case this widget is 115 // redefined after a widget inherits from it 116 _childConstructors: [] 117 }); 118 119 //實例化父類 獲取父類的 prototype 120 basePrototype = new base(); 121 // we need to make the options hash a property directly on the new instance 122 // otherwise we'll modify the options hash on the prototype that we're 123 // inheriting from 124 //這里深復制一份options 125 basePrototype.options = $.widget.extend({}, basePrototype.options); 126 //在傳入的ui原型中有方法調用this._super 和this.__superApply會調用到base上(最基類上)的方法 127 $.each(prototype, function(prop, value) { 128 //如果val不是function 則直接給對象賦值字符串 129 if (!$.isFunction(value)) { 130 proxiedPrototype[prop] = value; 131 return; 132 } 133 //如果val是function 134 proxiedPrototype[prop] = (function() { 135 //兩種調用父類函數的方法 136 var _super = function() { 137 //將當期實例調用父類的方法 138 return base.prototype[prop].apply(this, arguments); 139 }, 140 _superApply = function(args) { 141 return base.prototype[prop].apply(this, args); 142 }; 143 return function() { 144 var __super = this._super, 145 __superApply = this._superApply, 146 returnValue; 147 // console.log(prop, value,this,this._super,'===') 148 // debugger; 149 //在這里調用父類的函數 150 this._super = _super; 151 this._superApply = _superApply; 152 153 returnValue = value.apply(this, arguments); 154 155 this._super = __super; 156 this._superApply = __superApply; 157 // console.log(this,value,returnValue,prop,'===') 158 return returnValue; 159 }; 160 })(); 161 }); 162 // console.info(proxiedPrototype) 163 // debugger; 164 //這里是實例化獲取的內容 165 constructor.prototype = $.widget.extend(basePrototype, { 166 // TODO: remove support for widgetEventPrefix 167 // always use the name + a colon as the prefix, e.g., draggable:start 168 // don't prefix for widgets that aren't DOM-based 169 widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name 170 }, proxiedPrototype, { 171 //重新把constructor指向 constructor 變量 172 constructor: constructor, 173 namespace: namespace, 174 widgetName: name, 175 widgetFullName: fullName 176 }); 177 178 // If this widget is being redefined then we need to find all widgets that 179 // are inheriting from it and redefine all of them so that they inherit from 180 // the new version of this widget. We're essentially trying to replace one 181 // level in the prototype chain. 182 //這里判定插件是否被使用了。一般來說,都不會被使用的。 183 //因為插件的開發者都是我們自己,呵呵 184 if (existingConstructor) { 185 $.each(existingConstructor._childConstructors, function(i, child) { 186 var childPrototype = child.prototype; 187 188 // redefine the child widget using the same prototype that was 189 // originally used, but inherit from the new version of the base 190 $.widget(childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto); 191 }); 192 // remove the list of existing child constructors from the old constructor 193 // so the old child constructors can be garbage collected 194 delete existingConstructor._childConstructors; 195 } else { 196 //父類添加當前插件的實例 主要用於作用域鏈查找 不至於斷層 197 base._childConstructors.push(constructor); 198 } 199 200 //將此方法掛在jQuery對象上 201 $.widget.bridge(name, constructor); 202 203 return constructor; 204 }; 205 206 //擴展jq的extend方法,實際上類似$.extend(true,..) 深復制 207 $.widget.extend = function(target) { 208 var input = widget_slice.call(arguments, 1), 209 inputIndex = 0, 210 inputLength = input.length, 211 key, value; 212 for (; inputIndex < inputLength; inputIndex++) { 213 for (key in input[inputIndex]) { 214 value = input[inputIndex][key]; 215 if (input[inputIndex].hasOwnProperty(key) && value !== undefined) { 216 // Clone objects 217 if ($.isPlainObject(value)) { 218 target[key] = $.isPlainObject(target[key]) ? $.widget.extend({}, target[key], value) : 219 // Don't extend strings, arrays, etc. with objects 220 $.widget.extend({}, value); 221 // Copy everything else by reference 222 } else { 223 target[key] = value; 224 } 225 } 226 } 227 } 228 return target; 229 }; 230 231 //bridge 是設計模式的一種,這里將對象轉為插件調用 232 $.widget.bridge = function(name, object) { 233 var fullName = object.prototype.widgetFullName || name; 234 //這里就是插件了 235 //這部分的實現主要做了幾個工作,也是制作一個優雅的插件的主要代碼 236 //1、初次實例化時將插件對象緩存在dom上,后續則可直接調用,避免在相同元素下widget的多實例化。簡單的說,就是一個單例方法。 237 //2、合並用戶提供的默認設置選項options 238 //3、可以通過調用插件時傳遞字符串來調用插件內的方法。如:$('#id').menu('hide') 實際就是實例插件並調用hide()方法。 239 //4、同時限制外部調用“_”下划線的私有方法 240 $.fn[name] = function(options) { 241 var isMethodCall = typeof options === "string", 242 args = widget_slice.call(arguments, 1), 243 returnValue = this; 244 245 // allow multiple hashes to be passed on init. 246 //可以簡單認為是$.extend(true,options,args[0],...),args可以是一個參數或是數組 247 options = !isMethodCall && args.length ? $.widget.extend.apply(null, [options].concat(args)) : options; 248 //這里對字符串和對象分別作處理 249 if (isMethodCall) { 250 this.each(function() { 251 var methodValue, instance = $.data(this, fullName); 252 //如果傳遞的是instance則將this返回。 253 if (options === "instance") { 254 returnValue = instance; 255 return false; 256 } 257 if (!instance) { 258 return $.error("cannot call methods on " + name + " prior to initialization; " + "attempted to call method '" + options + "'"); 259 } 260 //這里對私有方法的調用做了限制,直接調用會拋出異常事件 261 if (!$.isFunction(instance[options]) || options.charAt(0) === "_") { 262 return $.error("no such method '" + options + "' for " + name + " widget instance"); 263 } 264 //這里是如果傳遞的是字符串,則調用字符串方法,並傳遞對應的參數. 265 //比如插件有個方法hide(a,b); 有2個參數:a,b 266 //則調用時$('#id').menu('hide',1,2);//1和2 分別就是參數a和b了。 267 methodValue = instance[options].apply(instance, args); 268 if (methodValue !== instance && methodValue !== undefined) { 269 returnValue = methodValue && methodValue.jquery ? returnValue.pushStack(methodValue.get()) : methodValue; 270 return false; 271 } 272 }); 273 } else { 274 this.each(function() { 275 var instance = $.data(this, fullName); 276 277 if (instance) { 278 instance.option(options || {}); 279 //這里每次都調用init方法 280 if (instance._init) { 281 instance._init(); 282 } 283 } else { 284 //緩存插件實例 285 $.data(this, fullName, new object(options, this)); 286 } 287 }); 288 } 289 290 return returnValue; 291 }; 292 }; 293 294 //這里是真正的widget基類 295 $.Widget = function( /* options, element */ ) {}; 296 $.Widget._childConstructors = []; 297 298 $.Widget.prototype = { 299 widgetName: "widget", 300 //用來決定事件的名稱和插件提供的callbacks的關聯。 301 // 比如dialog有一個close的callback,當close的callback被執行的時候,一個dialogclose的事件被觸發。 302 // 事件的名稱和事件的prefix+callback的名稱。widgetEventPrefix 默認就是控件的名稱,但是如果事件需要不同的名稱也可以被重寫。 303 // 比如一個用戶開始拖拽一個元素,我們不想使用draggablestart作為事件的名稱,我們想使用dragstart,所以我們可以重寫事件的prefix。 304 // 如果callback的名稱和事件的prefix相同,事件的名稱將不會是prefix。 305 // 它阻止像dragdrag一樣的事件名稱。 306 widgetEventPrefix: "", 307 defaultElement: "<div>", 308 //屬性會在創建模塊時被覆蓋 309 options: { 310 disabled: false, 311 312 // callbacks 313 create: null 314 }, 315 _createWidget: function(options, element) { 316 element = $(element || this.defaultElement || this)[0]; 317 this.element = $(element); 318 this.uuid = widget_uuid++; 319 this.eventNamespace = "." + this.widgetName + this.uuid; 320 this.options = $.widget.extend({}, this.options, this._getCreateOptions(), options); 321 322 this.bindings = $(); 323 this.hoverable = $(); 324 this.focusable = $(); 325 326 if (element !== this) { 327 // debugger 328 $.data(element, this.widgetFullName, this); 329 this._on(true, this.element, { 330 remove: function(event) { 331 if (event.target === element) { 332 this.destroy(); 333 } 334 } 335 }); 336 this.document = $(element.style ? 337 // element within the document 338 element.ownerDocument : 339 // element is window or document 340 element.document || element); 341 this.window = $(this.document[0].defaultView || this.document[0].parentWindow); 342 } 343 344 this._create(); 345 //創建插件時,有個create的回調 346 this._trigger("create", null, this._getCreateEventData()); 347 this._init(); 348 }, 349 _getCreateOptions: $.noop, 350 _getCreateEventData: $.noop, 351 _create: $.noop, 352 _init: $.noop, 353 //銷毀模塊:去除綁定事件、去除數據、去除樣式、屬性 354 destroy: function() { 355 this._destroy(); 356 // we can probably remove the unbind calls in 2.0 357 // all event bindings should go through this._on() 358 this.element.unbind(this.eventNamespace).removeData(this.widgetFullName) 359 // support: jquery <1.6.3 360 // http://bugs.jquery.com/ticket/9413 361 .removeData($.camelCase(this.widgetFullName)); 362 this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass( 363 this.widgetFullName + "-disabled " + "ui-state-disabled"); 364 365 // clean up events and states 366 this.bindings.unbind(this.eventNamespace); 367 this.hoverable.removeClass("ui-state-hover"); 368 this.focusable.removeClass("ui-state-focus"); 369 }, 370 _destroy: $.noop, 371 372 widget: function() { 373 return this.element; 374 }, 375 //設置選項函數 376 option: function(key, value) { 377 var options = key, 378 parts, curOption, i; 379 380 if (arguments.length === 0) { 381 // don't return a reference to the internal hash 382 //返回一個新的對象,不是內部數據的引用 383 return $.widget.extend({}, this.options); 384 } 385 386 if (typeof key === "string") { 387 // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } 388 options = {}; 389 parts = key.split("."); 390 key = parts.shift(); 391 if (parts.length) { 392 curOption = options[key] = $.widget.extend({}, this.options[key]); 393 for (i = 0; i < parts.length - 1; i++) { 394 curOption[parts[i]] = curOption[parts[i]] || {}; 395 curOption = curOption[parts[i]]; 396 } 397 key = parts.pop(); 398 if (arguments.length === 1) { 399 return curOption[key] === undefined ? null : curOption[key]; 400 } 401 curOption[key] = value; 402 } else { 403 if (arguments.length === 1) { 404 return this.options[key] === undefined ? null : this.options[key]; 405 } 406 options[key] = value; 407 } 408 } 409 410 this._setOptions(options); 411 412 return this; 413 }, 414 _setOptions: function(options) { 415 var key; 416 417 for (key in options) { 418 this._setOption(key, options[key]); 419 } 420 421 return this; 422 }, 423 _setOption: function(key, value) { 424 this.options[key] = value; 425 426 if (key === "disabled") { 427 this.widget().toggleClass(this.widgetFullName + "-disabled", !! value); 428 429 // If the widget is becoming disabled, then nothing is interactive 430 if (value) { 431 this.hoverable.removeClass("ui-state-hover"); 432 this.focusable.removeClass("ui-state-focus"); 433 } 434 } 435 436 return this; 437 }, 438 439 enable: function() { 440 return this._setOptions({ 441 disabled: false 442 }); 443 }, 444 disable: function() { 445 return this._setOptions({ 446 disabled: true 447 }); 448 }, 449 450 _on: function(suppressDisabledCheck, element, handlers) { 451 var delegateElement, instance = this; 452 453 // no suppressDisabledCheck flag, shuffle arguments 454 if (typeof suppressDisabledCheck !== "boolean") { 455 handlers = element; 456 element = suppressDisabledCheck; 457 suppressDisabledCheck = false; 458 } 459 460 // no element argument, shuffle and use this.element 461 if (!handlers) { 462 handlers = element; 463 element = this.element; 464 delegateElement = this.widget(); 465 } else { 466 // accept selectors, DOM elements 467 element = delegateElement = $(element); 468 this.bindings = this.bindings.add(element); 469 } 470 471 $.each(handlers, function(event, handler) { 472 function handlerProxy() { 473 // allow widgets to customize the disabled handling 474 // - disabled as an array instead of boolean 475 // - disabled class as method for disabling individual parts 476 if (!suppressDisabledCheck && (instance.options.disabled === true || $(this).hasClass("ui-state-disabled"))) { 477 return; 478 } 479 return (typeof handler === "string" ? instance[handler] : handler).apply(instance, arguments); 480 } 481 482 // copy the guid so direct unbinding works 483 if (typeof handler !== "string") { 484 handlerProxy.guid = handler.guid = handler.guid || handlerProxy.guid || $.guid++; 485 } 486 487 var match = event.match(/^([\w:-]*)\s*(.*)$/), 488 eventName = match[1] + instance.eventNamespace, 489 selector = match[2]; 490 if (selector) { 491 delegateElement.delegate(selector, eventName, handlerProxy); 492 } else { 493 element.bind(eventName, handlerProxy); 494 } 495 }); 496 }, 497 498 _off: function(element, eventName) { 499 eventName = (eventName || "").split(" ").join(this.eventNamespace + " ") + this.eventNamespace; 500 element.unbind(eventName).undelegate(eventName); 501 }, 502 503 _delay: function(handler, delay) { 504 function handlerProxy() { 505 return (typeof handler === "string" ? instance[handler] : handler).apply(instance, arguments); 506 } 507 var instance = this; 508 return setTimeout(handlerProxy, delay || 0); 509 }, 510 511 _hoverable: function(element) { 512 this.hoverable = this.hoverable.add(element); 513 this._on(element, { 514 mouseenter: function(event) { 515 $(event.currentTarget).addClass("ui-state-hover"); 516 }, 517 mouseleave: function(event) { 518 $(event.currentTarget).removeClass("ui-state-hover"); 519 } 520 }); 521 }, 522 523 _focusable: function(element) { 524 this.focusable = this.focusable.add(element); 525 this._on(element, { 526 focusin: function(event) { 527 $(event.currentTarget).addClass("ui-state-focus"); 528 }, 529 focusout: function(event) { 530 $(event.currentTarget).removeClass("ui-state-focus"); 531 } 532 }); 533 }, 534 535 _trigger: function(type, event, data) { 536 var prop, orig, callback = this.options[type]; 537 538 data = data || {}; 539 event = $.Event(event); 540 event.type = (type === this.widgetEventPrefix ? type : this.widgetEventPrefix + type).toLowerCase(); 541 // the original event may come from any element 542 // so we need to reset the target on the new event 543 event.target = this.element[0]; 544 545 // copy original event properties over to the new event 546 orig = event.originalEvent; 547 if (orig) { 548 for (prop in orig) { 549 if (!(prop in event)) { 550 event[prop] = orig[prop]; 551 } 552 } 553 } 554 555 this.element.trigger(event, data); 556 return !($.isFunction(callback) && callback.apply(this.element[0], [event].concat(data)) === false || event.isDefaultPrevented()); 557 } 558 }; 559 560 $.each({ 561 show: "fadeIn", 562 hide: "fadeOut" 563 }, function(method, defaultEffect) { 564 $.Widget.prototype["_" + method] = function(element, options, callback) { 565 if (typeof options === "string") { 566 options = { 567 effect: options 568 }; 569 } 570 var hasOptions, effectName = !options ? method : options === true || typeof options === "number" ? defaultEffect : options.effect || defaultEffect; 571 options = options || {}; 572 if (typeof options === "number") { 573 options = { 574 duration: options 575 }; 576 } 577 hasOptions = !$.isEmptyObject(options); 578 options.complete = callback; 579 if (options.delay) { 580 element.delay(options.delay); 581 } 582 if (hasOptions && $.effects && $.effects.effect[effectName]) { 583 element[method](options); 584 } else if (effectName !== method && element[effectName]) { 585 element[effectName](options.duration, options.easing, callback); 586 } else { 587 element.queue(function(next) { 588 $(this)[method](); 589 if (callback) { 590 callback.call(element[0]); 591 } 592 next(); 593 }); 594 } 595 }; 596 }); 597 598 return $.widget; 599 600 }));
