diff --git a/MIT-LICENSE.md b/MIT-LICENSE.md index 8d0ec44..2eaa50c 100644 --- a/MIT-LICENSE.md +++ b/MIT-LICENSE.md @@ -1,6 +1,6 @@ ### The MIT License -Copyright (c) 2012 Ben Tadiar +Copyright (c) 2012, 2014 Vas G�bor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/grunt/saucelabs-jasmine.js b/grunt/saucelabs-jasmine.js index 4fd6b39..9f8c982 100644 --- a/grunt/saucelabs-jasmine.js +++ b/grunt/saucelabs-jasmine.js @@ -116,6 +116,7 @@ module.exports = function (grunt, options) { browsers: browsers, tunneled: false, maxRetries: 1, + maxPollRetries: 5, onTestComplete: tagJob, sauceConfig: { 'video-upload-on-pass': false, diff --git a/package.json b/package.json index f8d1f96..17d7f88 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "knockout-jqueryui", "description": "knockout binding handlers for jQuery UI", "homepage": "https://github.com/gvas/knockout-jqueryui", - "version": "2.1.0", + "version": "2.2.0", "author": { "name": "Vas Gabor " }, @@ -17,20 +17,20 @@ "license": "MIT", "dependencies": {}, "devDependencies": { - "change-case": "~2.1.1", - "grunt": "~0.4.0", - "grunt-cli": "~0.1.6", - "grunt-contrib-compress": "~0.10.0", + "change-case": "~2.1.6", + "grunt": "~0.4.5", + "grunt-cli": "~0.1.13", + "grunt-contrib-compress": "~0.12.0", "grunt-contrib-concat": "~0.5.0", - "grunt-contrib-connect": "~0.8.0", + "grunt-contrib-connect": "~0.9.0", "grunt-contrib-jshint": "~0.10.0", - "grunt-contrib-uglify": "~0.5.0", + "grunt-contrib-uglify": "~0.6.0", "grunt-contrib-watch": "~0.6.1", "grunt-sauce-tunnel": "~0.2.1", - "grunt-saucelabs": "~8.2.1", - "load-grunt-config": "~0.13.0", - "q": "~1.0.1", - "request": "~2.40.0" + "grunt-saucelabs": "~8.4.1", + "load-grunt-config": "~0.16.0", + "q": "~1.1.2", + "request": "~2.51.0" }, "scripts": { "test": "grunt test" diff --git a/spec/bindingHandler.spec.js b/spec/bindingHandler.spec.js index aef8e82..c1afc64 100644 --- a/spec/bindingHandler.spec.js +++ b/spec/bindingHandler.spec.js @@ -5,7 +5,7 @@ 'use strict'; describe('The binding handler', function () { - var createBindingHandler, $element; + var createBindingHandler, $element, match; afterEach(function () { $element.remove(); @@ -58,6 +58,37 @@ delete ko.bindingHandlers.descendantBindingHandler; }); + it('should not throw any exception when a foreach binding is applied to the same element', function () { + + var $element = $('
').prependTo('body'); + + $.fn.test = function () { }; + + createBindingHandler(); + + ko.applyBindings({}, $element[0]); + }); + + match = ko.version.match(/^(\d)\.(\d+)/); + if (match && parseInt(match[1], 10) >= 3) { + it('should instantiate the widget after the standard foreach binding is processed', function () { + + var $element; + + $element = $('
').prependTo('body'); + + spyOn(ko.bindingHandlers.foreach, 'init').andCallThrough(); + + $.fn.test = function () { + expect(ko.bindingHandlers.foreach.init).toHaveBeenCalled(); + }; + + createBindingHandler(); + + ko.applyBindings({}, $element[0]); + }); + } + it('should set the options specified in the binding on the widget', function () { var vm; @@ -235,4 +266,4 @@ expect($.fn.test).toHaveBeenCalledWith('destroy'); }); }); -}()); +} ()); diff --git a/spec/selectmenu.spec.js b/spec/selectmenu.spec.js index a7953d2..bacc53b 100644 --- a/spec/selectmenu.spec.js +++ b/spec/selectmenu.spec.js @@ -60,5 +60,34 @@ ko.removeNode($element[0]); }); + + it('should synchronize with knockout\'s value binding', function () { + var $element, vm, autoOpen; + + $element = $([ + '' + ].join('')).prependTo('body'); + vm = { valueObservable: ko.observable() }; + ko.applyBindings(vm, $element[0]); + + expect(vm.valueObservable()).toBe('1'); + + // selectmenu -> value + $('.ui-selectmenu-button').click(); + $('.ui-selectmenu-menu .ui-menu-item:nth-child(2)').trigger('mouseenter'); + $('.ui-selectmenu-menu .ui-menu-item:nth-child(2)').click(); + + expect(vm.valueObservable()).toBe('2'); + + // value -> selectmenu + vm.valueObservable('1'); + + expect($('.ui-selectmenu-text').text()).toBe('One'); + + ko.removeNode($element[0]); + }); }); }()); diff --git a/src/accordion.js b/src/accordion.js index c9649f1..a27a6e4 100644 --- a/src/accordion.js +++ b/src/accordion.js @@ -44,12 +44,12 @@ define( /// /// - var widgetName, value; + var widgetName, value, result; widgetName = this.widgetName; value = valueAccessor(); - BindingHandler.prototype.init.apply(this, arguments); + result = BindingHandler.prototype.init.apply(this, arguments); if (ko.isWriteableObservable(value.active)) { this.on(element, this.eventToWatch, function () { @@ -57,8 +57,7 @@ define( }); } - // the inner elements have already been taken care of - return { controlsDescendantBindings: true }; + return result; }; utils.register(Accordion); diff --git a/src/bindingHandler.js b/src/bindingHandler.js index 205fbdf..c1d82d6 100644 --- a/src/bindingHandler.js +++ b/src/bindingHandler.js @@ -4,10 +4,11 @@ define( [ 'jquery', 'knockout', + './utils', 'jquery-ui/widget' ], - function ($, ko) { + function ($, ko, utils) { 'use strict'; @@ -59,6 +60,7 @@ define( this.widgetEventPrefix = widgetName; this.options = []; this.events = []; + this.after = []; this.hasRefresh = false; }; @@ -66,15 +68,29 @@ define( BindingHandler.prototype.init = function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - var widgetName, value, unwrappedOptions, unwrappedEvents; + var widgetName, value, unwrappedOptions, unwrappedEvents, + shouldApplyBindingsToDescendants; widgetName = this.widgetName; value = valueAccessor(); unwrappedOptions = filterAndUnwrapProperties(value, this.options); unwrappedEvents = filterAndUnwrapProperties(value, this.events); - // allow inner elements' bindings to finish before initializing the widget - ko.applyBindingsToDescendants(bindingContext, element); + // There can be control flow- or other bindings on some of the descendant + // elements which affect the shape of the element-rooted DOM subtree. These + // should be processed before instantiating the jQuery UI widget, because they + // can add pages to the tabs widget, menu items to the menu widget, etc. + shouldApplyBindingsToDescendants = !ko.utils.arrayFirst( + utils.descendantControllingBindings, + function (bindingName) { + return this.hasOwnProperty(bindingName); + }, + allBindingsAccessor() + ); + if (shouldApplyBindingsToDescendants) { + // process descendant bindings + ko.applyBindingsToDescendants(bindingContext, element); + } // store the options' values so they can be checked for changes in the // update() method @@ -102,8 +118,7 @@ define( $(element)[widgetName]('destroy'); }); - // the inner elements have already been taken care of - return { controlsDescendantBindings: true }; + return { controlsDescendantBindings: shouldApplyBindingsToDescendants }; }; /*jslint unparam:false*/ diff --git a/src/datepicker.js b/src/datepicker.js index 162d16c..77d0c58 100644 --- a/src/datepicker.js +++ b/src/datepicker.js @@ -43,9 +43,9 @@ define( /// /// - var widgetName, options, value, subscription, origOnSelect; + var result, widgetName, options, value, subscription, origOnSelect; - BindingHandler.prototype.init.apply(this, arguments); + result = BindingHandler.prototype.init.apply(this, arguments); widgetName = this.widgetName; options = valueAccessor(); @@ -80,8 +80,7 @@ define( }); } - // the inner elements have already been taken care of - return { controlsDescendantBindings: true }; + return result; }; utils.register(Datepicker); diff --git a/src/dialog.js b/src/dialog.js index 1f6d212..cfaa583 100644 --- a/src/dialog.js +++ b/src/dialog.js @@ -55,7 +55,7 @@ define( /// /// - var marker, value; + var marker, result, value; /// sets up the correct disposal marker = document.createElement('DIV'); @@ -67,7 +67,7 @@ define( }); /// invokes the prototype's init() method - BindingHandler.prototype.init.apply(this, arguments); + result = BindingHandler.prototype.init.apply(this, arguments); /// sets up handling of the isOpen option value = valueAccessor(); @@ -112,8 +112,7 @@ define( /*jslint unparam:false*/ } - // the inner elements have already been taken care of - return { controlsDescendantBindings: true }; + return result; }; utils.register(Dialog); diff --git a/src/selectmenu.js b/src/selectmenu.js index b8828ad..912e12b 100644 --- a/src/selectmenu.js +++ b/src/selectmenu.js @@ -18,6 +18,7 @@ define( BindingHandler.call(this, 'selectmenu'); + this.after = ['value']; this.options = ['appendTo', 'disabled', 'icons', 'position', 'width']; this.events = ['change', 'close', 'create', 'focus', 'open', 'select']; this.hasRefresh = true; @@ -26,19 +27,24 @@ define( Selectmenu.prototype = utils.createObject(BindingHandler.prototype); Selectmenu.prototype.constructor = Selectmenu; - Selectmenu.prototype.init = function (element, valueAccessor) { + Selectmenu.prototype.init = function (element, valueAccessor, + allBindingsAccessor) { /// Connects the view model and the widget via the isOpen property. // /// /// + /// /// - var value = valueAccessor(); + var value, result; + + value = valueAccessor(); /// invokes the prototype's init() method - BindingHandler.prototype.init.apply(this, arguments); + result = BindingHandler.prototype.init.apply(this, arguments); - if (value.isOpen) { + // maintain the isOpen option + if (value.hasOwnProperty('isOpen')) { ko.computed({ read: function () { if (ko.utils.unwrapObservable(value.isOpen)) { @@ -60,8 +66,24 @@ define( }); } - // the inner elements have already been taken care of - return { controlsDescendantBindings: true }; + // synchronize the selected option with knockout's standard value binding + if (allBindingsAccessor().hasOwnProperty('value')) { + ko.computed({ + read: function () { + ko.utils.unwrapObservable(allBindingsAccessor().value); + $(element).selectmenu('refresh'); + }, + disposeWhenNodeIsRemoved: element + }); + } + + // Notify knockout's value- and selectedOptions bindings that the selected + // option has been changed. + this.on(element, 'change', function () { + $(element).trigger('change'); + }); + + return result; }; utils.register(Selectmenu); diff --git a/src/slider.js b/src/slider.js index 99274aa..fb573a7 100644 --- a/src/slider.js +++ b/src/slider.js @@ -37,9 +37,9 @@ define( /// /// - var value, changeEvent; + var result, value, changeEvent; - BindingHandler.prototype.init.apply(this, arguments); + result = BindingHandler.prototype.init.apply(this, arguments); value = valueAccessor(); changeEvent = value.realtime ? 'slide' : 'change'; @@ -73,8 +73,7 @@ define( /*jslint unparam:false*/ } - // the inner elements have already been taken care of - return { controlsDescendantBindings: true }; + return result; }; utils.register(Slider); diff --git a/src/spinner.js b/src/spinner.js index 8b85f05..9ff42fa 100644 --- a/src/spinner.js +++ b/src/spinner.js @@ -34,9 +34,9 @@ define( /// /// - var widgetName, value; + var result, widgetName, value; - BindingHandler.prototype.init.apply(this, arguments); + result = BindingHandler.prototype.init.apply(this, arguments); widgetName = this.widgetName; value = valueAccessor(); @@ -78,8 +78,7 @@ define( } } - // the inner elements have already been taken care of - return { controlsDescendantBindings: true }; + return result; }; utils.register(Spinner); diff --git a/src/tabs.js b/src/tabs.js index 814a39e..114114c 100644 --- a/src/tabs.js +++ b/src/tabs.js @@ -87,7 +87,7 @@ define( /// /// - BindingHandler.prototype.init.apply(this, arguments); + var result = BindingHandler.prototype.init.apply(this, arguments); if (this.version.major === 1 && this.version.minor === 8) { postInitHandler18.call(this, element, valueAccessor); @@ -95,8 +95,7 @@ define( postInitHandler.call(this, element, valueAccessor); } - // the inner elements have already been taken care of - return { controlsDescendantBindings: true }; + return result; }; utils.register(Tabs); diff --git a/src/tooltip.js b/src/tooltip.js index 4fd5405..4a3b415 100644 --- a/src/tooltip.js +++ b/src/tooltip.js @@ -33,9 +33,11 @@ define( /// /// - var value = valueAccessor(); + var value, result; - BindingHandler.prototype.init.apply(this, arguments); + value = valueAccessor(); + + result = BindingHandler.prototype.init.apply(this, arguments); if (value.isOpen) { ko.computed({ @@ -59,8 +61,7 @@ define( }); } - // the inner elements have already been taken care of - return { controlsDescendantBindings: true }; + return result; }; utils.register(Tooltip); diff --git a/src/utils.js b/src/utils.js index a8ff6e2..6bfaa15 100644 --- a/src/utils.js +++ b/src/utils.js @@ -11,7 +11,7 @@ define( 'use strict'; - var match, uiVersion, createObject, register; + var match, uiVersion, descendantControllingBindings, createObject, register; /*jslint regexp:true*/ match = ($.ui.version || '').match(/^(\d)\.(\d+)/); @@ -26,6 +26,9 @@ define( }; } + descendantControllingBindings = ['foreach', 'if', 'ifnot', 'with', 'html', 'text', + 'options']; + createObject = Object.create || function (prototype) { /// Simple (incomplete) shim for Object.create(). /// @@ -44,6 +47,9 @@ define( var handler = new Constructor(); ko.bindingHandlers[handler.widgetName] = { + after: ko.utils.arrayGetDistinctValues( + descendantControllingBindings.concat(handler.after || []) + ), init: handler.init.bind(handler), update: handler.update.bind(handler) }; @@ -51,6 +57,7 @@ define( return { uiVersion: uiVersion, + descendantControllingBindings: descendantControllingBindings, createObject: createObject, register: register };