├── .gitignore ├── .gitmodules ├── Docs ├── Behavior.Events.md ├── Behavior.Startup.md ├── Behavior.Trigger.md ├── Behavior.md ├── BehaviorAPI.md ├── Delegator.md └── Element.Data.md ├── Gruntfile.js ├── README.md ├── Source ├── Behavior.Events.js ├── Behavior.Startup.js ├── Behavior.Trigger.js ├── Behavior.js ├── BehaviorAPI.js ├── Delegator.js ├── Element.Data.js └── Event.Mock.js ├── Tests ├── Specs │ ├── Behavior │ │ ├── Behavior.Benchmarks.js │ │ ├── Behavior.Events.Specs.js │ │ ├── Behavior.Specs.js │ │ ├── Behavior.SpecsHelpers.js │ │ ├── Behavior.Startup.Specs.js │ │ ├── Behavior.Trigger.Specs.js │ │ ├── BehaviorAPI.Specs.js │ │ ├── Delegator.Specs.js │ │ └── Element.Data.Specs.js │ ├── Benchmarks.js │ ├── Configuration.js │ ├── Syn.js │ └── package.yml └── gruntfile-options.js ├── bower.json ├── layers.png ├── license.txt ├── package.json └── package.yml /.gitignore: -------------------------------------------------------------------------------- 1 | art.js 2 | node_modules 3 | behavior-specs.js 4 | behavior.js 5 | .bower.json 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Specs/Runner"] 2 | path = Specs/Runner 3 | url = git://github.com/anutron/mootools-runner.git 4 | [submodule "Specs/mootools-core"] 5 | path = Specs/mootools-core 6 | url = git://github.com/mootools/mootools-core.git 7 | [submodule "Specs/mootools-more"] 8 | path = Specs/mootools-more 9 | url = git://github.com/cloudera/mootools-more.git 10 | -------------------------------------------------------------------------------- /Docs/Behavior.Events.md: -------------------------------------------------------------------------------- 1 | Behavior Filter: Behavior.Events {#Behavior.Events} 2 | ==================================== 3 | 4 | Provides mechanism to invoke a Delegator trigger when an instance created by a Behavior filter fires an event. 5 | 6 | ### Note 7 | 8 | Behavior executes filters in DOM order. For this reason, the `addEvents` filter always fires after Behavior 9 | fires it's `apply` event (after it has run through the DOM). This means that a) any filter that has a delay on it 10 | (and doesn't return an instance on startup) will not work with `addEvent` and b) that any instance that `addEvent` 11 | references has already been instantiated. So if you are attaching an event to a class that fires, say, a `show` 12 | event whenever it changes something, but it does so immediately on instantiation, then that first event will have 13 | already been fired before your listener is attached. In this case, some additional startup logic is required on your 14 | part - either change your DOM accordingly to have a startup state or use `Behavior.Startup` to invoke the 15 | proper delegator based on the DOM state. 16 | 17 | ### Example 18 | 19 |
68 | 69 | ### Options 70 | 71 | * events - (*object*) a set of targets, the behavior filter that generated the instance, and events to monitor and the triggers to invoke when they are fired 72 | 73 | ### Basic Syntax 74 | 75 | data-addevent-options=" 76 | 'events': { 77 | '.foo::BehaviorName': { // .foo is the element to find relative to this one, BehaviorName is the filter that generated the instance 78 | 'show': [ // show is the event to listen for on the instance returned by BehaviorName 79 | { 80 | '.bar::addClass': { // .bar is the element to fire the addClass trigger upon 81 | 'class': 'hide', // an argument passed to that trigger 82 | 'if': { // but only if this conditional is true. 83 | 'self::hasClass': 'baz' 84 | } 85 | } 86 | } 87 | ] 88 | } 89 | } 90 | " 91 | 92 | 93 | 94 | ### Conditionals 95 | 96 | See the notes about Conditionals in the Delegator docs. Those provided by Delegator are the basic checks against element methods or element properties as with the example above. However, Behavior.Events provides three additional conditional checks: 97 | 98 | #### Checking event arguments 99 | 100 | This allows you to fire a trigger if (or unless) the event fired on the instance received an argument of your specification. Example: 101 | 102 | data-addevent-options=" 103 | 'events': { 104 | '.foo::BehaviorName': { 105 | 'show': [ 106 | { 107 | '.bar::addClass': { 108 | 'class': 'hide', 109 | 'if': { 110 | 'eventArguments[0]': 'foo', 111 | 'eventArguments[1]': 'bar' 112 | } 113 | } 114 | } 115 | ] 116 | } 117 | } 118 | " 119 | 120 | In this example, the `onShow` event must have been passed two (or more) arguments: `'foo'`, and `'bar'`. Both must match. 121 | 122 | #### Checking instance properties 123 | 124 | This allows you to fire a trigger if (or unless) a specified property on the instance matches a value 125 | 126 | data-addevent-options=" 127 | 'events': { 128 | '.foo::BehaviorName': { 129 | 'show': [ 130 | { 131 | '.bar::addClass': { 132 | 'class': 'hide', 133 | 'if': { 134 | 'instance.foo': 'bar', 135 | 'instance.baz': 'biz' 136 | } 137 | } 138 | } 139 | ] 140 | } 141 | } 142 | " 143 | 144 | In this example, the trigger will be invoked when the `onShow` event fires but only if the instance that was created by the `BehaviorName` filter on the `.foo` element has a `.foo` property equalling `'bar'` and `.baz` property equalling `'biz'`. 145 | 146 | #### Checking instance methods 147 | 148 | This allows you to fire a trigger if (or unless) a specified method on the instance returns a value 149 | 150 | data-addevent-options=" 151 | 'events': { 152 | '.foo::BehaviorName': { 153 | 'show': [ 154 | { 155 | '.bar::addClass': { 156 | 'class': 'hide', 157 | 'if': { 158 | 'instance.foo()': 'bar', 159 | 'arguments': ['an argument to pass to foo()'] 160 | } 161 | } 162 | } 163 | ] 164 | } 165 | } 166 | " 167 | 168 | In this example, the trigger will be invoked when the `onShow` event fires but only if the instance that was created by the `BehaviorName` filter on the `.foo` element has a `.foo` *method* that, when called with the argument `'an argument to pass to foo()'` returns `'bar'`. If you don't specify the `arguments` value it just invokes the method. 169 | -------------------------------------------------------------------------------- /Docs/Behavior.Startup.md: -------------------------------------------------------------------------------- 1 | Behavior Filter: Behavior.Startup {#Behavior.Startup} 2 | ==================================== 3 | 4 | Invokes delegators on startup when specified conditions are met. This allows you to check the state of elements in the DOM and invoke some action that is appropriate. It's especially useful for form inputs where the client (browser) maintains a state if the user reloads. 5 | 6 | ### Example 7 | 8 | enable 25 | 26 | ### Options 27 | 28 | * delegators - (*object*) a set of delegators to fire if their conditionals are true. 29 | 30 | ### Conditionals 31 | 32 | Each delegator listed will be invoked if their conditional is true. The delegator name is the key, and the value is an object with the following properties: 33 | 34 | * target - (*string*) a css selector *relative to the element* to find a single element to test. 35 | * targets - (*string*) a css selector *relative to the element* to find a group of elements to test. If the conditional is true for any of them, the delegator is fired. 36 | * property - (*string*) a property of the target element to evaluate. Do not use with the `method` option. 37 | * method - (*string*) a method on the target element to invoke. Passed as arguments the `arguments` array (see below). Do not use with the `property` option. 38 | * arguments - (*array* of *strings*) arguments passed to the method of the target element specified in the `method` option. Ignored if the `property` option is used. 39 | * value - (*string*) A value to compare to either the value of the `property` of the target or the result of the `method` invoked upon it. 40 | * delay - (*number*) If set, the trigger will be invoked after this many milliseconds have passed. 41 | 42 | ### Notes 43 | 44 | * delegator conditionals that do not have a `property` OR `method` setting will always be invoked. 45 | * This behavior (like all others) is only applied once (on startup or when new content is run through `Behavior.apply`). Be careful as this adds a startup cost to delegators; use wisely. 46 | -------------------------------------------------------------------------------- /Docs/Behavior.Trigger.md: -------------------------------------------------------------------------------- 1 | Behavior Filter: Behavior.Trigger {#Behavior.Trigger} 2 | ==================================== 3 | 4 | Because Delegator is inefficient for mouse over/out events, this behavior 5 | allows you to invoke delegator triggers on elements when they occur using 6 | normal event monitoring. 7 | 8 | 9 | ### Example 10 | 11 |
...
30 | 31 | 32 | ### Options 33 | 34 | * triggers - (*array*) Array of configurations for triggers (see below) 35 | 36 | ### Triggers 37 | 38 | Each trigger listed includes the events to monitor (i.e. `click`) and a list of selectors that are 39 | used to find the targets to monitor. The events are attached using traditional `addEvent` calls (instead 40 | of using event delegation) on the elements that match this selector. 41 | 42 | 43 | ### Notes 44 | 45 | * updates to the DOM that include new elements that match that selector will not have their events monitored. 46 | -------------------------------------------------------------------------------- /Docs/BehaviorAPI.md: -------------------------------------------------------------------------------- 1 | Class: BehaviorAPI {#BehaviorAPI} 2 | ========================== 3 | 4 | Provides methods to read values from annotated HTML configured for the [Behavior][] class and its associated [Filters](Behavior.md#Behavior.Filter). 5 | 6 | ### Syntax 7 | 8 | new BehaviorAPI(element[, prefix]); 9 | 10 | ### Arguments 11 | 12 | 1. element - (*element*) An element you wish to read. 13 | 2. prefix - (*string*; optional) A prefix to all the properties; a namespace. 14 | 15 | ### Notes 16 | 17 | Examples of the HTML expressions evaluated are as follows (all of the following produce the same output*): 18 | 19 | //prefered 20 | //no braces on JSON 21 | 22 | 23 | 24 | The `-options` value is parsed as JSON first (it's slightly more permissive in that you don't have to wrap it in `{}` just for convenience). Values defined here are read as defined allowing you to express arrays, numbers, booleans, etc. Functions / callbacks are generally not used by [Behavior][]. 25 | 26 | If you attempt to read a value that isn't defined in this options object, the property name is attempted to be read from the property directly (e.g. `data-behaviorname-prop`). This value is *always* a string unless you specify a type. If a type is specified the value is run through the JSON parser and validated against that type. 27 | 28 | Note that filter names that contain characters other than A-Z, 0-9, or dash are stripped and what remains is case insensitive. Dots are turned to dashes. Further, camelCase properties are hyphenated to camel-case. So, for example, you would express the following: 29 | 30 | 31 | //and - note the hyphenation 32 | 33 | 34 | BehaviorAPI Method: get {#BehaviorAPI:get} 35 | ------------------------------------------ 36 | 37 | Gets a value for the specified name. 38 | 39 | ### Syntax 40 | 41 | api.get(name[, name, name, name]) 42 | 43 | ### Arguments 44 | 45 | 1. name - (*string*) The name of the property you wish to retrieve. Pass more than one to get back multiple. 46 | 47 | ### Example 48 | 49 | var api = new BehaviorAPI(target, 'foo'); 50 | api.get('bar'); //returns the value of data-foo-bar or null 51 | api.get('bar', 'baz'); //returns {bar: 'value', baz: 'value'} 52 | 53 | ### Returns 54 | 55 | * (*mixed*) Values defined as strings will be returned as strings. Values defined in JSON will be returned as their 56 | type is evaluated. When you expect anything other than a string it's better to use [getAs](#BehaviorAPI:getAs). 57 | When more than one name is specified you'll receive an object response with key/value pairs for the name/property values. 58 | 59 | BehaviorAPI Method: getAs {#BehaviorAPI:getAs} 60 | ------------------------------------------ 61 | 62 | Gets a value for the specified name and runs it through [JSON.decode][] and verifies that the value is parsed as the specified type (specifically a MooTools Type: [String](http://mootools.net/docs/core/Types/String), [Function](http://mootools.net/docs/core/Types/Function), [Array](http://mootools.net/docs/core/Types/Array), [Date](http://mootools.net/docs/more/Types/Date), etc.). 63 | 64 | ### Syntax 65 | 66 | api.getAs(Type, name[, defaultValue]); 67 | 68 | ### Arguments 69 | 70 | 1. Type - (*Type*) A MooTools Type instance (a function) that the value, when run through [JSON.decode][], should return 71 | 2. name - (*string*) The name of the value to read. 72 | 3. defaultValue - (*mixed*) The value to set if there no value found. 73 | 74 | ### Example 75 | 76 | var api = new BehaviorAPI(target, 'foo'); 77 | api.getAs(Number, 'some-number'); 78 | 79 | ### Returns 80 | 81 | * (*mixed*) Either returns the value as the Type you specified, the default (if provided), or undefined. 82 | 83 | BehaviorAPI Method: require {#BehaviorAPI:require} 84 | ------------------------------------------ 85 | 86 | Validates that an element has a value set for a given name. Throws an error if the value is not found. 87 | 88 | ### Syntax 89 | 90 | api.require(name[, name, name]); 91 | 92 | ### Arguments 93 | 94 | 1. name - (*string*) The name of the property you wish to require. Pass more than one if needed. 95 | 96 | ### Example 97 | 98 | var api = new BehaviorAPI(target, 'foo'); 99 | api.require('foo'); //throws an error if data-foo-foo is not set 100 | api.require('foo', 'bar'); //throws an error if data-foo-foo or data-foo-bar are not set 101 | 102 | ### Returns 103 | 104 | * *object* - the instance of BehaviorAPI. 105 | 106 | BehaviorAPI Method: requireAs {#BehaviorAPI:requireAs} 107 | ------------------------------------------ 108 | 109 | Requires that an element has a value set for a given name that can be parsed into a given type (using [JSON.decode][]). If a value is not present or does not parse to the specified Type an error is thrown. 110 | 111 | ### Syntax 112 | 113 | api.requireAs(obj); 114 | 115 | ### Arguments 116 | 117 | 1. obj - (*object*) a set of name/Type pairs to require. 118 | 119 | ### Example 120 | 121 | api.requireAs({ 122 | option1: Number, 123 | option2: Boolean 124 | }); 125 | 126 | ### Returns 127 | 128 | * *object* - the instance of BehaviorAPI. 129 | 130 | BehaviorAPI Method: setDefault {#BehaviorAPI:setDefault} 131 | ------------------------------------------ 132 | 133 | Sets the default values. Note that setting defaults for required properties is not useful. 134 | 135 | ### Syntax 136 | 137 | api.setDefault(name, value); 138 | api.setDefault(obj); 139 | 140 | ### Arguments 141 | 142 | 1. name - (*string*) The name of the property you wish to set. 143 | 2. value - (*mixed*) The default value for the given name. 144 | 145 | OR 146 | 147 | 1. obj - (*object*) a set of name/value pairs to use if the element doesn't have values present. 148 | 149 | ### Example 150 | 151 | api.setDefault('duration', 1000); 152 | api.setDefault({ 153 | duration: 1000, 154 | link: 'chain' 155 | }); 156 | 157 | ### Returns 158 | 159 | * *object* - the instance of BehaviorAPI. 160 | 161 | BehaviorAPI Method: refreshAPI {#BehaviorAPI:refreshAPI} 162 | ------------------------------------------ 163 | 164 | The API class caches values read from the element to avoid the cost of DOM interaction. Once you read a value, it is never read from the element again. If you wish to refresh this to re-read the element properties, invoke this method. Note that default values are maintained. 165 | 166 | ### Syntax 167 | 168 | api.refreshAPI(); 169 | 170 | ### Returns 171 | 172 | * *object* - the instance of BehaviorAPI. 173 | 174 | [Behavior]: Behavior.md 175 | [JSON.decode]: http://mootools.net/docs/core/Utilities/JSON#JSON:decode 176 | -------------------------------------------------------------------------------- /Docs/Delegator.md: -------------------------------------------------------------------------------- 1 | Class: Delegator {#Delegator} 2 | ==================================== 3 | 4 | Manager for generic (DOM) event handlers. 5 | 6 | ### Implements 7 | 8 | * [Options][], [Events][] 9 | 10 | ### Syntax 11 | 12 | new Delegator([options]); 13 | 14 | ### Arguments 15 | 16 | 1. options - (*object*; optional) a key/value set of options 17 | 18 | ### Options 19 | 20 | * breakOnErrors - (*boolean*) By default, errors thrown by triggers are caught; the onError event is fired. Set this to `true` to NOT catch these errors to allow them to be handled by the browser. 21 | * verbose - (*boolean*) If *true*, Delegator logs its activity to the console. This can create a lot of output. Defaults to *false*. 22 | * getBehavior - (*function*) Returns an instance of [Behavior](Behavior.md) so that triggers can integrate with it. 23 | 24 | ### Events 25 | 26 | * error - function invoked when a trigger is not found. Defaults to console errors if console.error is available. Also able to be invoked by triggers as `api.error`. 27 | * warn - function invoked when a trigger calls `api.warn`. Defaults to `console.warn` if present. 28 | * destroyDom - function invoked when a trigger destroys a portion of the DOM. Automatically integrated w/ Behavior's `cleanup` method if you set one in the options. Passed the element destroyed as an argument. 29 | * ammendDom - function invoked when a trigger ammends a portion of the DOM. Automatically integrated w/ Behavior's `apply` method if you set one in the options. Passed two arguments: the parent node that contains all the updated elements and an array of those elements updated. 30 | * trigger - function invoked whenever a trigger is called. Passed four arguments: `trigger` (the name of the trigger invoked), `element` (the element on which it was invoked), `event` (the event object), `result` (anything returned by the trigger's handler). 31 | * updateHistory - function invoked when a behavior changes the state of the page and wishes to change the url to match it using `history.pushState`. Passed a single argument, the new url (e.g. `api.fireEvent('updateHistory', someURL)`). 32 | 33 | ### Usage 34 | 35 | Delegator implements [Event Delegation](http://mootools.net/docs/more/Element/Element.Delegation) for reusable behaviors. Conceptually its similar to [Behavior][] in that you declare which behavior you want an element to have, but unlike Behavior's filters which are run at startup and instantiate widgets and the like, Delegator is designed to run its registered functions (which we call "triggers") at event time (such as click). 36 | 37 | This should not be confused with deferred Behavior filters (which can be run at event time, too). Behavior filters deferred to an event (such as click) are only run once and are used to instantiate something. Delegator's triggers are event handlers to be used repeatedly (a trigger might, for example, hide its parent or remove itself from the DOM or load some content via AJAX). 38 | 39 | ### Example Usage 40 | 41 | var myDelegator = new Delegator(); 42 | Delegator.attach(myContainerElement); 43 | Delegator.register('click', 'hide', function(event, element, api){ 44 | event.preventDefault(); 45 | api.getElement('target').hide(); 46 | }); 47 | 48 | ### Example HTML 49 | 50 | click me to hide foo 51 |
I hide when you click the link above me!
52 | 53 | ### HTML properties 54 | 55 | Delegator uses a clearly defined API to read HTML properties off the elements it configures. See [BehaviorAPI][] for details. 56 | 57 | ### Using Multiple Triggers Together 58 | 59 | It's possible to declare more than one data trigger property for a single element (`data-trigger="disableMe submitParentForm"`) 60 | 61 | ### Integrating with Behavior 62 | 63 | If you're using [Behavior](Behavior.md) you should connect the two so that links that Delegator uses to update the DOM can have their response run through your Behavior instance's `apply` method. Example: 64 | 65 | var myBehavior = new Behavior().apply(document.body); 66 | var myDelegator = new Delegator({ 67 | getBehavior: function(){ return myBehavior; } 68 | }).attach(document.body); 69 | myBehavior.setDelegator(myDelegator); 70 | 71 | ### Conventions 72 | 73 | * MooTools has the convention that classes are upper case and methods and functions are not. Becuase Delegator triggers do not necessarily instantiate classes as Behavior filters do, they are usually registered with lower case names. 74 | * Whenever a delegator trigger references another element (or elements) with its options, by convention the selector given always is relative to the element with the trigger. In the example above, the `data-hide-target` value is `!body #foo` (instead of just `#foo`). This convention is codified in the passed api methods `getElement` and `getElements` (detailed below) which will get the element(s) referenced by that selector for you, optionally throwing warnings when they aren't found. 75 | 76 | ### Conditionals 77 | 78 | Delegator also allows any trigger's options to include conditionals that will prevent or allow the trigger from being invoked if they match. These conditionals are run at event time, allowing you to program your UI for different states. Delegator provides two conditionals - `if` and `unless`. In theory this could be extended to include others like less than or greater than at some point. 79 | 80 | #### Examples 81 | 82 | ... 87 | 88 | ... 93 | 94 | Both the examples above reference the `foo` trigger and specify conditionals. The first one uses the special `if` conditional and requires that the element itself has the class "foo" (which it doesn't), and thus the trigger will not fire. The second one is nearly identical but uses the special `unless` conditional, which does the same check but verifies that it's NOT true, so that one will. 95 | 96 | There's a more verbose version of these conditionals that looks like this: 97 | 98 | ... 106 | 107 | Here we explicitly name the target (`self`), the method invoked on that element (`hasClass`) - this can be any element method, the arguments passed to that method, and the value we expect it to return. The previous examples are just shorthands that are parsed into this more verbose format. 108 | 109 | ### Multiple Triggers 110 | 111 | Delegator also provides a custom trigger called "multi" which allows a single element to invoke triggers on OTHER elements each with its own options. This allows you to have a single element that the user clicks and then it hides some elements, adds classes to others, etc. 112 | 113 | #### Example 114 | 115 | 134 | 135 | Here we have 3 different Delegator triggers we are invoking when the user clicks our link. 136 | 137 | * The first is the "someTrigger" trigger which is invoked on any child element with the "foo" class. This one has a configuration - the options for the trigger. See important note below about how these are used. 138 | * The second one just invokes the "someOtherTrigger" on any child element with the "bar" class. 139 | * The third invokes the "yetAnotherTrigger" on any element with the "baz" class *provided that it also has the "foo" class*. 140 | 141 | #### Important Notes 142 | 143 | * The configuration specified this way are passed to [BehaviorAPI][]'s `setDefault` method. This means that if the target element has its own configuration for these triggers that the configuration options specified on the element win. 144 | * Conditionals evaluated in these triggers are evaluated on the targets, not the element where they are specified. In other words, in the last example above where there's a check to see if `'self::hasClass': 'foo'"`, `self` will reference each matched element for `.baz`. These options specified here are projected on each matched element as if the trigger were there. 145 | * If you express more than one conditional statement in your `if` or `unless` object, each is evaluated and the condition **fails** if *any* of them do. In this example, each statement in the `unless` object is evaluated. If *any* are `true` (because it's an `unless` statement) the trigger is not invoked. 146 | 147 | ... 153 | 154 | * You can, if you like, still specify a conditional for the "multi" trigger. This means the entire list of triggers will be ignored if your condition fails. Example: 155 | 156 | 165 | 166 | ### Switches 167 | 168 | Finally, Delegator offers a special trigger called a `switch`. These allow you to define multiple sets of triggers to execute when a condition is met. If the user clicks this button and some radio button is selected, execute these triggers on THAT group of elements, else execute these OTHER triggers on these OTHER elements. 169 | 170 | There are two types of switches: `first` and `any`. The `first` switch iterates over your switch groups (in the order they are declared) and executes the first group whose condition is `true`, while the `any` switch iterates over all the trigger groups, executing any whose condition is true. As with all triggers, a group with no condition is treated as one that is `true` and executed. 171 | 172 | #### Examples 173 | 174 | ... 197 | 198 | In the above example, Delegator iterates over the array of trigger groups defined in the `first` switches. When it finds one that is valid, it runs the triggers defined within it. The last group in the example has no condition and is essentially treated as the default case in the eventuality that none of the previous are `true`. (Note that in this example, because the first two in the group are the same condition with one an `if` and the other an `unless`, one of them *has* to be true.) 199 | 200 | ... 223 | 224 | In this example, which is nearly identical to the first one except the switch type is `any` instead of `first`, Delegator iterates over all of the trigger groups and executes each if their conditional is `true`. While the `first` example would execute one of the first two in the group (because they are opposites in the example) and stop, the `any` example will execute one of the first two (whichever is `true`) AND the last one. 225 | 226 | #### Conditionals with Switches 227 | 228 | Note that, as with any trigger, you can have a conditional for the switch itself: 229 | 230 | ... 250 | 251 | 252 | Delegator Method: passMethod {#Delegator:passMethod} 253 | -------------------------------------------------- 254 | 255 | Defines a method that will be passed to triggers. Delegator allows you to create a well defined API for triggers to reference which increases their reusability. You define this API by explicitly passing named functions to them through the Delegator instance. 256 | 257 | ### Syntax 258 | 259 | myDelegatorInstance.passMethod(name, function); 260 | 261 | ### Returns 262 | 263 | * (*object*) this instance of Delegator 264 | 265 | ### Notes 266 | 267 | By default, Delegator passes the following methods to triggers in addition to the methods defined in the [BehaviorAPI][] 268 | 269 | * addEvent - the addEvent on the behavior instance method provided by the [Events][] class. 270 | * removeEvent - the removeEvent on the behavior instance method provided by the [Events][] class. 271 | * addEvents - the addEvents on the behavior instance method provided by the [Events][] class. 272 | * removeEvents - the removeEvents on the behavior instance method provided by the [Events][] class. 273 | * fireEvents - the fireEvents on the behavior instance method provided by the [Events][] class. 274 | * attach - the [attach](#Delegator:attach) method provided by this Delegator instance. 275 | * trigger - the [trigger](#Delegator:trigger) method provided by this Delegator instance. 276 | * error - fires the Delegator instance's `error` event with the arguments passed. 277 | * fail - stops the trigger and passes a message through to the error logger. Takes a string for the message as its only argument. 278 | * getBehavior - returns the behavior instance defined in the `getBehavior` options object. 279 | * getElement - see note on getElement below. 280 | * getElements - see note on getElements below. 281 | * See the [BehaviorAPI][] for additional methods passed by default. 282 | 283 | You can add any other methods that your triggers require. In general, your filters shouldn't reference anything in your environment except these methods. 284 | 285 | ### api.getElement and api.getElements 286 | 287 | Like Behavior, Delegator provides two methods to help you get elements relative to the one with the trigger on it: `getElement` and `getElements`. These methods, given an option key, look up the key's value and find the first element using that value as a selector. This search is relative to the api's element (so, for example, to find an element by ID anywhere on the page, you'd pass "`!body #the-id`"). Returns the first element found or `null`. `getElements` returns an `Elements` instance with the result while `getElement` just returns the first. 288 | 289 | By default, these methods will throw an error (quietly, in the console, unless the `breakOnErrors` option on the Delegator instance is `true`) if the option key is not defined or no element is found, stopping execution of the trigger. Pass in an optional second argument to have it only throw the warning in the console but continue execution. 290 | 291 | #### Examples 292 | 293 | some stuff 296 | 297 | 306 | 307 | If the user did not configure a target in the options or if the selector specified in that option were to fail to find a result, execution would be stopped and an error logged to console (or thrown if `breakOnErrors` is true). 308 | 309 | some stuff 312 | 313 | 325 | 326 | `getElements` works the same way, but instead returns an array-like `Elements` object with all elements that match the selector. 327 | 328 | #### Special selectors `self` and `window` 329 | 330 | For convenience, Delegator provides two special selectors: `self` and `window`. `self` returns the element itself, while `window` returns the window. Unlike regular selectors which can contain pseudo-selectors and commas (i.e. `.foo:focused, .bar`), the `self` and `window` selectors must be on their own with no adornment. The reason for this is that some triggers (like the first example above) require that there be a selector given for an option. If the user wants to invoke the triggers's action on the element clicked, they need a way to reference it. Likewise, if they want reference the window (like scrolling it for example) they need a way to reference it. 331 | 332 | 333 | Delegator Method: passMethods {#Delegator:passMethods} 334 | -------------------------------------------------- 335 | 336 | Iterates over an object of key/values passing them to the [passMethod](#Delegator:passMethod) method. 337 | 338 | ### Syntax 339 | 340 | myDelegatorInstance.passMethods(obj); 341 | 342 | ### Arguments 343 | 344 | 1. obj - (*object*) a set of name/function pairs to pass to the passMethod method. 345 | 346 | ### Returns 347 | 348 | * (*object*) this instance of Delegator 349 | 350 | Delegator Method: register {#Delegator:register} 351 | -------------------------------------------------- 352 | 353 | This is both a static method and an instance method. Using the static method (`Delegator.register(...)`) will register a *global* trigger. Using the instance method will register a *local* trigger. The local trigger is used whenever both exist. 354 | 355 | ### Syntax 356 | 357 | Delegator.register(eventTypes, name, handler, overwrite); 358 | myDelegator.register(eventTypes, name, handler, overwrite); 359 | //also 360 | Delegator.register(eventTypes, object, overwrite); 361 | myDelegator.register(eventTypes, object, overwrite); 362 | 363 | ### Arguments 364 | 365 | 1. eventTypes - (*string* or *array*) The event type this trigger monitors. *It is not advised to ever use mouseout or mouseover*. 366 | 2. name - (*string*) The name of this trigger. 367 | 3. handler - (*function* or *object*) The event handler for this trigger. Passed the event, the element, and an instance of [BehaviorAPI][]. See Note about extended declaration for this argument. 368 | 4. overwrite - (*boolean*) If *true* and a trigger by this name already exists, it will be overwritten. Defaults to *false*. 369 | 370 | ### Alternate Arguments 371 | 372 | 1. eventTypes - same as above. 373 | 2. object - (*object*) a set of name/handler values to add. 374 | 3. overwrite - same as above. 375 | 376 | ### Examples 377 | 378 | //this is the same example as the one at the top of the page 379 | var myDelegator = new Delegator(); 380 | myDelegator.attach(myContainerElement); 381 | //this adds a global trigger 382 | Delegator.register('click', 'hide', function(event, element, api){ 383 | event.preventDefault(); 384 | var target = element.getElement(api.get('target')); 385 | if (target) target.hide(); 386 | }); 387 | 388 | //also 389 | Delegator.register(['click', 'submit'], { 390 | Foo: function(){...}, 391 | Bar: { 392 | handler: function(){...}, 393 | requires: [...] 394 | } 395 | }); 396 | 397 | Delegator Method: setTriggerDefaults {#Behavior:setTriggerDefaults} 398 | -------------------------------------------------- 399 | 400 | Sets the default values for a trigger, overriding any defaults previously defined. 401 | 402 | ### Syntax 403 | 404 | myDelegator.setTriggerDefaults(name, defaults); 405 | 406 | ### Arguments 407 | 408 | 1. name - (*string*) The registered name of a trigger. 409 | 2. defaults - (*object*) A key/value pair of defaults. 410 | 411 | Delegator Method: cloneTrigger {#Behavior:cloneTrigger} 412 | -------------------------------------------------- 413 | 414 | Clones a pre-existing trigger and sets new specified defaults. This is a great way 415 | to pre-package often-reused configurations. 416 | 417 | ### Syntax 418 | 419 | myDelegator.cloneTrigger(name, newName, defaults); 420 | 421 | ### Arguments 422 | 423 | 1. name - (*string*) The registered name of a trigger. 424 | 2. newName - (*string*) The name of the new trigger. 425 | 3. defaults - (*object*) A key/value pair of defaults. 426 | 427 | Delegator Method: getTrigger {#Delegator:getTrigger} 428 | -------------------------------------------------- 429 | 430 | This is both a static method and an instance method. Using the static method (`Delegator.getTrigger(...)`) will return a *global* trigger. Using the instance method will return a *local* trigger or, if not found, the global one. 431 | 432 | ### Syntax 433 | 434 | Delegator.getTrigger(name); 435 | myDelegator.getTrigger(name); 436 | 437 | ### Arguments 438 | 439 | 1. name - (*string*) the name of the trigger to retrieve. 440 | 441 | ### Returns 442 | 443 | * trigger - (*object* or *null*) the trigger instance if found. 444 | 445 | ### Examples 446 | 447 | //this is the same example as the one at the top of the page 448 | var myDelegator = new Delegator(); 449 | myDelegator.attach(myContainerElement); 450 | //this adds a global trigger 451 | Delegator.register('click', 'hide', function(event, element, api){ 452 | //... 453 | }); 454 | 455 | Delegator.getTrigger('hide'); //returns the GLOBAL trigger instance 456 | myDelegator.getTrigger('hide'); //returns the GLOBAL trigger instance 457 | 458 | //but if we add a local one 459 | myDelegator.register('click', 'hide', function(event, element, api){ 460 | //... local version by the same name 461 | }); 462 | 463 | Delegator.getTrigger('hide'); //returns the GLOBAL trigger instance 464 | myDelegator.getTrigger('hide'); //returns the LOCAL trigger instance 465 | 466 | ### Extended handlers 467 | 468 | Handlers, much like Behavior's filter declaration, are passed an instance of [BehaviorAPI][] as they often have additional configuration properties (for example, a selector to find *which* form to submit or hide or what-have-you). You can declare a handler in object notation with values for defaults and required properties. Example: 469 | 470 | myDelegator.register('click', 'hide', { 471 | require: ['target'], 472 | requireAs: { 473 | count: Number, 474 | whatever: Array 475 | }, 476 | defaults: { 477 | someSelector: '#foo' 478 | }, 479 | handler: function(event, element, api){...} 480 | }); 481 | 482 | Elements that fail to provide the required attributes will have these filters ignored. These triggers throw errors but by default these are caught unless you set `options.breakOnErrors` to `true`. 483 | 484 | ### Included handlers 485 | 486 | Delegator includes five handlers: 487 | 488 | * `Stop` - calls `event.stop()` on the event for you; this is typically done in the registered trigger, but can be done at the element level if you include this trigger in your HTML declaration. 489 | * `PreventDefault` - similar to `Stop`, this calls `event.preventDefault()`. 490 | * `multi` - allows you to define numerous triggers to invoke on other elements (see section above on the `multi` trigger) 491 | * `first` and `any` - special types of `multi`-filter groups. See section above on "switch" filters. 492 | 493 | ### Events of note 494 | 495 | Triggers can fire events on the instance of Delegator that invokes them. See the Events section above regarding the events supported by default. In particular, if you're using this class with Behavior you should take care to connect the two and to use the `destroyDom` and `ammendDom` events. 496 | 497 | You can also have your triggers fire any other arbitrary event that you like to facilitate integration with other triggers or external objects that attach to Delegator's event model. 498 | 499 | Delegator Method: addEventTypes {#Delegator:addEventTypes} 500 | ---------------------------------------------------------- 501 | 502 | Adds event types to a registered trigger. 503 | 504 | ### Syntax 505 | 506 | myDelegator.addEventTypes(triggerName, types); 507 | 508 | ### Arguments 509 | 510 | 1. triggerName - (*string*) the name of the trigger. 511 | 2. types - (*array*) the event types to add (`blur`, `click`, etc). 512 | 513 | Delegator Method: attach {#Delegator:attach} 514 | -------------------------------------------------- 515 | 516 | Attaches the appropriate event listeners to the provided container. 517 | 518 | ### Syntax 519 | 520 | myDelegator.attach(container); 521 | 522 | ### Returns 523 | 524 | * (*object*) this instance of Delegator 525 | 526 | ### Notes 527 | 528 | * Attaching the event listeners to nested elements is highly discouraged. 529 | 530 | Delegator Method: detach {#Delegator:detach} 531 | -------------------------------------------------- 532 | 533 | Detaches the appropriate event listeners from the provided container or, if none is provided, all of them that have previously been attached. 534 | 535 | ### Syntax 536 | 537 | myDelegator.detach([container]); 538 | 539 | ### Arguments 540 | 541 | 1. container - (*element*; optional) A DOM element (or its ID) to attach delegated events. If none is specified all previously attached elements are detached. 542 | 543 | ### Returns 544 | 545 | * (*object*) this instance of Delegator 546 | 547 | Delegator Method: fireEventForElement {#Delegator:fireEventForElement} 548 | ---------------------------------------------------------------------- 549 | Fires the provided element's triggers that match the provided event type. 550 | 551 | ### Syntax 552 | myDelegator.fireEventForElement(element, eventType, [force]) 553 | 554 | ### Arguments 555 | 556 | 1. element - (*element*) A DOM element which has triggers that respond to the given event 557 | 2. eventType - (*string*) the name of an event to fire 558 | 3. force - (*boolean*; optional) force the element to fire its triggers, even if they don't respond to the given event. 559 | 560 | Delegator Method: trigger {#Delegator:trigger} 561 | -------------------------------------------------- 562 | 563 | Invokes a specific trigger manually. 564 | 565 | ### Syntax 566 | 567 | myDelegator.trigger(trigger, element[, event, ignoreTypes]); 568 | 569 | ### Example 570 | 571 | myDelegator.trigger('UpdateOnSubmit', myForm, 'submit'); //creates a mock "submit" event 572 | 573 | ### Arguments 574 | 575 | 1. trigger - (*string*) The name of the registered trigger to invoke. 576 | 2. element - (*element*) A DOM element (or its ID) for the trigger's target. 577 | 3. event - (*event* or *string*; optional) An optional event to pass to the trigger. If you pass in a string, a mock event will be created for that type. If none is provided a mock event is created as a "click" event. 578 | 4. ignoreTypes - (*boolean*) if `true` does not check the event type to see if it matches the trigger's specified supported methods. 579 | 580 | ### Returns 581 | 582 | * (*mixed*) - Whatever the trigger invoked returns. 583 | 584 | Static Methods 585 | ============== 586 | 587 | In addition to those listed above that are both static and instance methods... 588 | 589 | Delegator Method: debug {#Delegator:debug} 590 | -------------------------------------------------- 591 | 592 | Will invoke `debugger` before executing any trigger that matches that name, allowing you to walk through that filter's invocation. 593 | 594 | ### Syntax 595 | 596 | Delegator.debug(pluginName); 597 | 598 | ### Arguments 599 | 600 | 1. pluginName - (*string*) The name of the plugin. 601 | 602 | 603 | Element Methods 604 | =============== 605 | 606 | Delegator implements the following helper methods on the Element prototype. 607 | 608 | Element Method: addTrigger {#Element:addTrigger} 609 | ------------------------------------------------------ 610 | 611 | Adds a trigger to the element. 612 | 613 | ### Syntax 614 | 615 | myElement.addTrigger(name); 616 | 617 | ### Arguments 618 | 619 | 1. name - (*string*) The name of the trigger to add. 620 | 621 | ### Returns 622 | 623 | * (*element*) This element. 624 | 625 | Element Method: removeTrigger {#Element:removeTrigger} 626 | ------------------------------------------------------ 627 | 628 | Removes a trigger to the element. 629 | 630 | ### Syntax 631 | 632 | myElement.removeTrigger(name); 633 | 634 | ### Arguments 635 | 636 | 1. name - (*string*) The name of the trigger to remove. 637 | 638 | ### Returns 639 | 640 | * (*element*) This element. 641 | 642 | 643 | Element Method: getTriggers {#Element:getTriggers} 644 | ------------------------------------------------------ 645 | 646 | Gets an array of triggers specified on an element. 647 | 648 | ### Syntax 649 | 650 | myElement.getTriggers(); 651 | 652 | ### Returns 653 | 654 | * (*array*) A list of trigger names. 655 | 656 | Element Method: hasTrigger {#Element:hasTrigger} 657 | ------------------------------------------------------ 658 | 659 | Returns `true` if the element has the specified trigger. 660 | 661 | ### Syntax 662 | 663 | myElement.hasTrigger(name); 664 | 665 | ### Arguments 666 | 667 | 1. name - (*string*) The name of the trigger to check for. 668 | 669 | ### Returns 670 | 671 | * (*boolean*) Returns `true` if the element has the specified trigger. 672 | 673 | [Options]: http://mootools.net/docs/core/Class/Class.Extras#Options 674 | [Events]: http://mootools.net/docs/core/Class/Class.Extras#Events 675 | [Behavior]: Behavior 676 | [BehaviorAPI]: BehaviorAPI 677 | -------------------------------------------------------------------------------- /Docs/Element.Data.md: -------------------------------------------------------------------------------- 1 | Native: Element {#Element} 2 | ========================== 3 | 4 | Extends the [Element][] native object with methods useful for working with HTML5 data properties. 5 | 6 | Element Method: setData {#Element:setData} 7 | ------------------------------------------ 8 | 9 | Sets a value for a given data property. 10 | 11 | ### Syntax 12 | 13 | myDiv.setData(name, value) 14 | 15 | ### Example 16 | 17 | myDiv.setData('foo-bar', 'baz'); 18 | //result:
19 | 20 | ### Returns 21 | 22 | * (*element*) the element 23 | 24 | Element Method: getData {#Element:getData} 25 | ------------------------------------------ 26 | 27 | Gets a value for a given data property. 28 | 29 | ### Syntax 30 | 31 | myDiv.getData(name, defaultValue) 32 | 33 | ### Arguments 34 | 35 | 1. name - (*string*) the data property to get; this is prepended with "data-". 36 | 2. defaultValue - (*string, number*) the value to assign if none is set. 37 | 38 | ### Example 39 | 40 | myDiv.getData('foo-bar'); 41 | //returns "baz" from:
42 | 43 | ### Returns 44 | 45 | * (*string*) the value if found, otherwise *null*. 46 | 47 | Element Method: setJSONData {#Element:setJSONData} 48 | ------------------------------------------ 49 | 50 | Sets a value for a given data property, encoding it into JSON. 51 | 52 | ### Syntax 53 | 54 | myDiv.setJSONData(name, object) 55 | 56 | ### Example 57 | 58 | myDiv.setJSONData('foo-bar', [1, 2, 'foo','bar']); 59 | //result:
60 | 61 | ### Returns 62 | 63 | * (*element*) the element 64 | 65 | Element Method: getJSONData {#Element:getJSONData} 66 | ------------------------------------------ 67 | 68 | Gets a value for a given data property, parsing it from JSON. 69 | 70 | ### Syntax 71 | 72 | myDiv.getJSONData(name, strict, defaultValue) 73 | 74 | ### Arguments 75 | 76 | 1. name - (*string*) the data property to get; this is prepended with "data-". 77 | 2. strict - (*boolean*) if *true* (default), will set the *JSON.decode*'s secure flag to *true*; otherwise the value is still tested but allows single quoted attributes. 78 | 3. defaultValue - (*string, number*) the value to assign if none is set. 79 | 80 | ### Example 81 | 82 | myDiv.getData('foo-bar'); 83 | //returns [1, 2, 'foo','bar'] from:
84 | 85 | ### Returns 86 | 87 | * (*object*) the value if found, otherwise *null*. 88 | 89 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function(grunt) { 4 | 5 | require('load-grunt-tasks')(grunt); 6 | var options = require('./Tests/gruntfile-options'); 7 | 8 | grunt.initConfig({ 9 | 'connect': options.grunt, 10 | 'packager': { 11 | source: { 12 | options: { 13 | name: { 14 | Behavior: 'Source/', 15 | More: 'node_modules/mootools-more/', 16 | Core: 'node_modules/mootools-core/' 17 | }, 18 | /* 19 | only: [ 20 | 'Behavior/Behavior', 21 | 'Behavior/Element.Data', 22 | 'Behavior/BehaviorAPI' 23 | ] 24 | */ 25 | }, 26 | src: [ 27 | 'node_modules/mootools-core/Source/**/*.js', 28 | 'node_modules/mootools-more/Source/**/*.js', 29 | 'Source/**/*.js' 30 | ], 31 | dest: 'behavior.js' 32 | }, 33 | specs: { 34 | options: { 35 | name: 'Specs', 36 | ignoreYAMLheader: true 37 | }, 38 | src: 'Tests/Specs/Behavior/*.js', 39 | dest: 'behavior-specs.js' 40 | } 41 | }, 42 | 'karma': { 43 | options: options.karma, 44 | continuous: { 45 | browsers: ['PhantomJS'] 46 | } 47 | }, 48 | 'clean': { 49 | all: {src: 'behavior*.js'} 50 | } 51 | }); 52 | 53 | grunt.registerTask('default', ['clean', 'packager:source', 'packager:specs', 'karma:continuous']); 54 | 55 | }; 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Behavior 2 | 3 | Auto-instantiates widgets/classes based on parsed, declarative HTML. 4 | 5 | ### Purpose 6 | 7 | All well-written web sites / apps that are interactive have the same basic pattern: 8 | 9 | ![Web app layers](https://github.com/anutron/behavior/raw/master/layers.png) 10 | 11 | Each page of the site or app is esoteric. It may have any combination of interactive elements, some of which interact with each other (for example, a form validation controller might interact with an ajax controller to prevent it sending a form that isn't valid). Typically this "glue" code exists in a domready statement. It says, get *this* form and instantiate *that* class with *these* options. This code is brittle; if you change either the DOM or the code the state breaks easily. It's not reusable, it only works for a specific page state. It can easily get out of hand. 12 | 13 | Behavior attempts to abstract that domready code into something you only write once and use often. It's fast and easily customized and extended. Instead of having a domready block that, say, finds all the images on a page and turns them into a gallery, and another block that searches the page for all the links on the page and turns them into tool tips, Behavior does a single search for all the elements you've marked. Each element is passed through the filter it names, where a filter is a function (and perhaps some configuration) that you've named. Each of these functions takes that element, reads properties defined on it in a prescribed manner and invokes the appropriate UI component. 14 | 15 | ## Documentation 16 | 17 | See markdown files in the *Docs* directory. 18 | 19 | * [Behavior](Docs/Behavior.md) 20 | * [BehaviorAPI](Docs/BehaviorAPI.md) 21 | * [Element.Data](Docs/Element.Data.md) 22 | 23 | ### Why? 24 | 25 | The nutshell is that instead of having a domready function that finds the stuff in your DOM and sets up instances of classes and whatnot, you put the configuration in the HTML itself and write the code that calls "new Foo(...)" only once. Example: 26 | 27 | Instead of this: 28 | 29 | $$('form').each(function(form){ 30 | new FormValidator(form, someOptions); 31 | new Form.Request(form, someOptions); 32 | }); 33 | new Tips($$('.tip')); 34 | $$('.accordion').each(function(container){ 35 | new Accordion(container.getElements('.toggler'), container.getElements('.section'), someOptions); 36 | }); 37 | etc 38 | 39 | You do this: 40 | 41 |
...
42 | blah 43 |
...
44 | 45 | Think of it as delegation (as in event delegation) for class invocation. If you use domready to do your setup and you want to swap out some HTML with XHR, you need to reapply that startup selectively to only your components that you're updating, which is often painful. Not with Behavior, you just apply the filters to the response and call it a day. 46 | 47 | You do a lot less DOM selection; you only ever run `$$('[data-behavior]')` once (though some filters may run more selectors on themselves - like Accordion finding its togglers and sections). 48 | 49 | Domready setup is always closely bound to the DOM anyway, but it's also separated from it. If you change the DOM, you might break the JS that sets it up and you always have to keep it in sync. You almost can't do that here because the DOM and its configuration is closely bound and in the same place. 50 | 51 | Developers who maybe aren't interested in writing components don't need to wade into the JS to use it. This is a big deal if you're working with a team you must support. 52 | 53 | Behavior is designed for apps that are constantly updating the UI with new data from the server. It's *not* an MVC replacement though. It's designed for web development that uses HTML fragments not JSON APIs (though it can play nicely with them). If you destroy a node that has a widget initialized it's easy to make sure that widget cleans itself up. The library also allows you to create enforcement to prevent misconfiguration and an API that makes it easy to read the values of the configuration. 54 | 55 | There are some other nifty things you get out of it; you get essentially free specs tests and benchmarks because the code to create both of them is in the Behavior filter. Here's an example of what it takes to write a spec for a widget and ALSO the benchmark for it's instantiation (this uses [Behavior.SpecsHelpers.js](https://github.com/anutron/behavior/blob/master/Tests/Specs/Behavior/Behavior.SpecsHelpers.js)). 56 | 57 | Behavior.addFilterTest({ 58 | filterName: 'OverText', 59 | desc: 'Creates an instance of OverText', 60 | content: '', 61 | returns: OverText 62 | }); 63 | 64 | This code above can be used to validate that the HTML fragment passed in does, in fact, create an OverText instance and it can also be used with [Benchmark.js](http://benchmarkjs.com/) to see which of your filters are the most expensive. More on this stuff in a minute. 65 | 66 | ### Delegator 67 | 68 | Included in the library is also a file called Delegator which is essentially the same thing except for events. For example, let's say you have a predictable UI pattern of having a link that, when clicked, it hides a parent element. Rather than writing that code each time: 69 | 70 | document.body.addEvent("click:a.hideParent", function(e, link){ 71 | e.preventDefault(); 72 | link.getParent().hide(); 73 | }); 74 | 75 | You register this pattern with Delegator and now you just do: 76 | 77 | Hide Me! 78 | 79 | It provides essentially the same value as Behavior, but at event time. The above example is pretty straight forward so, you know, why bother, right? But consider how many of these little things you write to make a web app function. If you can create them once and configure them inline, you save yourself a lot of code. 80 | 81 | ### Stock Behaviors 82 | 83 | Check out these resources of available Behavior Filters provided by the author: 84 | 85 | * [https://github.com/anutron/more-behaviors](https://github.com/anutron/more-behaviors) 86 | * [https://github.com/anutron/clientcide](https://github.com/anutron/clientcide) 87 | * [https://github.com/anutron/mootools-bootstrap](https://github.com/anutron/mootools-bootstrap) 88 | 89 | 90 | ## Notes 91 | 92 | Below are some notes regarding the implementation. The documentation should probably be read first as it gives usage examples. 93 | 94 | * Only one selector is ever run; adding 1,000 filters doesn't affect performance. 95 | * Nodes can have numerous filters. 96 | * Nodes can have an arbitrary number of supported options for each filter (`data-behaviorname-foo="bar"`). 97 | * Nodes can define options as JSON (this is actually the preferred implementation - `data-behaviorname-options=""`). 98 | * Elements can be retired w/ custom destruction; cleaning up an element also cleans up all the children of that element that have had behaviors applied. 99 | * Behaviors are only ever applied once to an element; if you call `myBehavior.apply(document.body)` a dozen times, the elements with filters will only have those filters applied once (can be forced to override and re-apply). 100 | * Filters are instances of classes that are applied to any number of elements. They are named uniquely. 101 | * Filters can be namespaced. Declare a filter called `Foo.Bar` and reference its options as `data-foo-bar-options="..."`. 102 | * There are "global" filters that are registered for all instances of behavior. 103 | * Instance filters get precedence. This allows for libraries to provide filters (like [http://github.com/anutron/more-behaviors](http://github.com/anutron/more-behaviors)) but for a specific instance to overwrite it without affecting the global state. (This pattern is in MooTools' `FormValidator` and works pretty well). 104 | * Filters have "plugins". A requirement for Filters is that they are unaware of each other. They have no guarantee that they will be invoked in any order (the developer writing the HTML expresses their order) or that they will be invoked with others or on their own. In addition to this ignorance, it's entirely likely that in specific environments a developer might want to augment a filter with additional functionality invoked whenever the filter is. This is what plugins are for. Plugins name the filter they augment but otherwise are just filters themselves. It's possible to have plugins for plugins. When you need to make two filters aware of each other (`FilterInput` + `HtmlTable.Zebra`). Note that plugins are always executed after all the filters are, so when writing a plugin that checks for a combination of two filters it is guaranteed that both filters have been applied. 105 | * Behavior defines a bottleneck for passing environment awareness to filters (`passMethod` / `behaviorAPI`). Filters should not know too explicitly about the environment they were invoked in. If the filters, for example, had to be able to call a method on some containing class - one that has created an instance of Behavior for example, the filter shouldn't have to have a pointer to that instance itself. It would make things brittle; a change in that class would break any number of unknown filters. By forcing the code that creates the Behavior instance to declare what methods filters can use it makes a more maintainable API. 106 | 107 | ## Limitations: 108 | 109 | * Due to the DOM-searching for both creation and destruction, you can't have behavior instances inside each other. 110 | -------------------------------------------------------------------------------- /Source/Behavior.Events.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Behavior.Events 4 | description: Allows for the triggering of delegators when classes instantiated by Behavior fire arbitrary events. 5 | requires: [/Behavior, /Delegator] 6 | provides: [Behavior.Events] 7 | ... 8 | */ 9 | 10 | /* 11 | 12 |
61 | 62 | */ 63 | (function(){ 64 | 65 | var reggies = { 66 | eventArguments: /^eventArguments/, 67 | eventArgumentIndex: /.*\[(.*)\]/, 68 | instanceMethod: /^instance\.([a-zA-Z].*)\(/, 69 | instanceProperty: /^instance\./ 70 | }; 71 | 72 | var parseConditional = function(element, api, conditional, instance, eventArguments){ 73 | var result = Object.every(conditional, function(value, key){ 74 | // key == "eventArguments[1]" 75 | if (key.match(reggies.eventArguments)){ 76 | var index = key.match(reggies.eventArgumentIndex)[1].toInt(); 77 | // index == 1 78 | return eventArguments[index] == value; 79 | } 80 | // key == instance.foo() 81 | if (key.match(reggies.instanceMethod)){ 82 | var method = key.match(reggies.instanceMethod)[1]; 83 | if (instance[method]){ 84 | if (conditional['arguments']) return instance[method].apply(instance, conditional['arguments']) == value; 85 | else return instance[method]() == value; 86 | } 87 | 88 | } 89 | // key == instance.foo 90 | if (key.match(reggies.instanceProperty)){ 91 | return instance[key.split('.')[1]] == value; 92 | } 93 | return Delegator.verifyTargets(element, conditional, api); 94 | }); 95 | return result; 96 | }; 97 | 98 | Behavior.addGlobalFilter('addEvent', { 99 | setup: function(element, api){ 100 | api.addEvent('apply:once', function(){ 101 | var events = api.getAs(Object, 'events'); 102 | Object.each(events, function(eventsToAdd, key){ 103 | var selector = key.split('::')[0]; 104 | var behaviorName = key.split('::')[1]; 105 | var target = Behavior.getTarget(element, selector); 106 | if (!target) return api.warn('Could not find element at ' + selector + ' to add event to ' + behaviorName); 107 | var instance = target.getBehaviorResult(behaviorName); 108 | if (!instance) return api.warn('Could not find instance of ' + behaviorName + ' for element at ' + selector); 109 | Object.each(eventsToAdd, function(triggers, eventName){ 110 | instance.addEvent(eventName, function(){ 111 | var eventArgs = arguments; 112 | triggers.each(function(trigger){ 113 | Object.each(trigger, function(options, delegatorTarget){ 114 | var valid = true; 115 | if (options['if'] && !parseConditional(element, api, options['if'], instance, eventArgs)) valid = false; 116 | if (options['unless'] && parseConditional(element, api, options['unless'], instance, eventArgs)) valid = false; 117 | 118 | if (valid){ 119 | // we've already tested these, so remove 120 | options['_if'] = options['if']; 121 | options['_unless'] = options['unless']; 122 | delete options['if']; 123 | delete options['unless']; 124 | // invoke the trigger 125 | api.getDelegator()._invokeMultiTrigger(element, null, delegatorTarget, options); 126 | // put them back 127 | options['if'] = options['_if']; 128 | options['unless'] = options['_unless']; 129 | delete options['_if']; 130 | delete options['_unless']; 131 | } 132 | }); 133 | }); 134 | }); 135 | }); 136 | }); 137 | }); 138 | return element; 139 | } 140 | }); 141 | })(); 142 | -------------------------------------------------------------------------------- /Source/Behavior.Startup.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Behavior.Startup 4 | description: Invokes delegators on startup when specified conditions are met. 5 | requires: [/Behavior, /Delegator, /Delegator.verifyTargets] 6 | provides: [Behavior.Startup] 7 | ... 8 | */ 9 | (function(){ 10 | Behavior.addGlobalFilter('Startup', { 11 | setup: function(el, api){ 12 | //get the delegators to set up 13 | var delegators = api.get('delegators'); 14 | if (delegators){ 15 | Object.each(delegators, function(conditional, delegator){ 16 | var timer =(function(){ 17 | //if any were true, fire the delegator ON THIS ELEMENT 18 | if (Delegator.verifyTargets(el, conditional, api)) { 19 | api.getDelegator().trigger(delegator, el); 20 | } 21 | }).delay(conditional.delay || 0) 22 | api.onCleanup(function(){ 23 | clearTimeout(timer); 24 | }); 25 | }); 26 | } 27 | } 28 | }); 29 | })(); 30 | -------------------------------------------------------------------------------- /Source/Behavior.Trigger.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Behavior.Trigger 4 | description: Because Delegator is inefficient for mouse over/out events, this behavior 5 | allows you to invoke delegator triggers on elements when they occur using 6 | normal event monitoring. 7 | requires: [/Behavior, /Delegator] 8 | provides: [Behavior.Trigger] 9 | ... 10 | */ 11 | 12 | /* 13 | 14 |
...
33 | 34 | on mouse over of any div.foo, the addClass trigger is invoked 35 | IF div.bar has the class .boo. 36 | 37 | */ 38 | 39 | Behavior.addGlobalFilter('Trigger', { 40 | 41 | requireAs: { 42 | triggers: Array 43 | }, 44 | 45 | setup: function(element, api){ 46 | var delegator = api.getDelegator(); 47 | if (!delegator) api.fail('MouseTrigger behavior requires that Behavior be connected to an instance of Delegator'); 48 | 49 | api.getAs(Array, 'triggers').each(function(triggerConfig){ 50 | 51 | 52 | 53 | // get the configuration for mouseover/mouseout 54 | Object.each(triggerConfig.targets, function(triggers, selector){ 55 | // get the selector for the elements to monitor 56 | var eventTargets = Behavior.getTargets(element, selector); 57 | // loop over the elements that match 58 | eventTargets.each(function(eventTarget){ 59 | // add our mouse event on each target 60 | 61 | var eventHandler = function(event){ 62 | // when the user mouses over/out, loop over the triggers 63 | Object.each(triggers, function(config, trigger){ 64 | // split the trigger name - '.foo::addClass' > {name: addClass, selector: .foo} 65 | trigger = delegator._splitTriggerName(trigger); 66 | if (!trigger) return; 67 | 68 | // iterate over the elements that match that selector using the event target as the root 69 | Behavior.getTargets(eventTarget, trigger.selector).each(function(target){ 70 | var api; 71 | // create an api for the trigger/element combo and set defaults to the config (if config present) 72 | if (config) api = delegator._getAPI(target, trigger).setDefault(config); 73 | // invoke the trigger 74 | delegator.trigger(trigger.name, target, event, true, api); 75 | }); 76 | }); 77 | }; 78 | 79 | Array.from(triggerConfig.events).each(function(eventType){ 80 | eventType = {mouseover: 'mouseenter', mouseout: 'mouseleave'}[eventType] || eventType; 81 | eventTarget.addEvent(eventType, eventHandler); 82 | }); 83 | }); 84 | }); 85 | 86 | }); 87 | } 88 | }); -------------------------------------------------------------------------------- /Source/Behavior.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Behavior 4 | description: Auto-instantiates widgets/classes based on parsed, declarative HTML. 5 | requires: [Core/Class.Extras, Core/Element.Event, Core/Selectors, More/Table, More/Events.Pseudos, /Element.Data, /BehaviorAPI] 6 | provides: [Behavior] 7 | ... 8 | */ 9 | 10 | (function(){ 11 | 12 | var getLog = function(method){ 13 | return function(){ 14 | if (window.console && console[method]){ 15 | if(console[method].apply) console[method].apply(console, arguments); 16 | else console[method](Array.from(arguments).join(' ')); 17 | } 18 | }; 19 | }; 20 | 21 | var checkOverflow = function(el) { 22 | return (el.offsetHeight < el.scrollHeight || el.offsetWidth < el.scrollWidth) && 23 | (['auto', 'scroll'].contains(el.getStyle('overflow')) || ['auto', 'scroll'].contains(el.getStyle('overflow-y'))); 24 | }; 25 | 26 | var PassMethods = new Class({ 27 | //pass a method pointer through to a filter 28 | //by default the methods for add/remove events are passed to the filter 29 | //pointed to this instance of behavior. you could use this to pass along 30 | //other methods to your filters. For example, a method to close a popup 31 | //for filters presented inside popups. 32 | passMethod: function(method, fn){ 33 | if (this.API.prototype[method]) throw new Error('Cannot overwrite API method ' + method + ' as it already exists'); 34 | this.API.implement(method, fn); 35 | return this; 36 | }, 37 | 38 | passMethods: function(methods){ 39 | for (var method in methods) this.passMethod(method, methods[method]); 40 | return this; 41 | } 42 | 43 | }); 44 | 45 | 46 | 47 | var GetAPI = new Class({ 48 | _getAPI: function(element, filter){ 49 | var api = new this.API(element, filter.name); 50 | var getElements = function(apiKey, warnOrFail, multi){ 51 | var method = warnOrFail || "fail"; 52 | var selector = api.get(apiKey); 53 | if (!selector) api[method]("Could not find selector for " + apiKey); 54 | 55 | var result = Behavior[multi ? 'getTargets' : 'getTarget'](element, selector); 56 | if (!result || (multi && !result.length)) api[method]("Could not find any elements for target '" + apiKey + "' using selector '" + selector + "'"); 57 | return result; 58 | }; 59 | api.getElement = function(apiKey, warnOrFail){ 60 | return getElements(apiKey, warnOrFail); 61 | }; 62 | api.getElements = function(apiKey, warnOrFail){ 63 | return getElements(apiKey, warnOrFail, true); 64 | }; 65 | return api; 66 | } 67 | }); 68 | 69 | var spaceOrCommaRegex = /\s*,\s*|\s+/g; 70 | 71 | BehaviorAPI.implement({ 72 | deprecate: function(deprecated, asJSON){ 73 | var set, 74 | values = {}; 75 | Object.each(deprecated, function(prop, key){ 76 | var value = this.element[ asJSON ? 'getJSONData' : 'getData'](prop, false); 77 | if (value !== undefined){ 78 | set = true; 79 | values[key] = value; 80 | } 81 | }, this); 82 | this.setDefault(values); 83 | return this; 84 | } 85 | }); 86 | 87 | this.Behavior = new Class({ 88 | 89 | Implements: [Options, Events, PassMethods, GetAPI], 90 | 91 | options: { 92 | //by default, errors thrown by filters are caught; the onError event is fired. 93 | //set this to *true* to NOT catch these errors to allow them to be handled by the browser. 94 | // breakOnErrors: false, 95 | // container: document.body, 96 | // onApply: function(elements){}, 97 | //default error behavior when a filter cannot be applied 98 | // reloadOnPopState: false, 99 | onLog: getLog('info'), 100 | onError: getLog('error'), 101 | onWarn: getLog('warn'), 102 | enableDeprecation: true, 103 | selector: '[data-behavior]' 104 | }, 105 | 106 | initialize: function(options){ 107 | this.setOptions(options); 108 | this.API = new Class({ Extends: BehaviorAPI }); 109 | this.passMethods({ 110 | getDelegator: this.getDelegator.bind(this), 111 | getBehavior: Function.from(this), 112 | addEvent: this.addEvent.bind(this), 113 | removeEvent: this.removeEvent.bind(this), 114 | addEvents: this.addEvents.bind(this), 115 | removeEvents: this.removeEvents.bind(this), 116 | fireEvent: this.fireEvent.bind(this), 117 | applyFilters: this.apply.bind(this), 118 | applyFilter: this.applyFilter.bind(this), 119 | getContentElement: this.getContentElement.bind(this), 120 | cleanup: this.cleanup.bind(this), 121 | getContainerSize: function(){ 122 | return this.getContentElement().measure(function(){ 123 | return this.getSize(); 124 | }); 125 | }.bind(this), 126 | error: function(){ this.fireEvent('error', arguments); }.bind(this), 127 | fail: function(){ 128 | var msg = Array.join(arguments, ' '); 129 | throw new Error(msg); 130 | }, 131 | warn: function(){ 132 | this.fireEvent('warn', arguments); 133 | }.bind(this) 134 | }); 135 | 136 | if (window.Fx && Fx.Scroll){ 137 | this.passMethods({ 138 | getScroller: function(el){ 139 | var par = (el || this.element).getParent(); 140 | while (par != document.body && !checkOverflow(par)){ 141 | par = par.getParent(); 142 | } 143 | var fx = par.retrieve('behaviorScroller'); 144 | if (!fx) fx = new Fx.Scroll(par); 145 | if (this.get('scrollerOptions')) fx.setOptions(this.get('scrollerOptions')); 146 | return fx; 147 | } 148 | }); 149 | } 150 | 151 | this.addEvents({ 152 | destroyDom: function(elements){ 153 | Array.from(elements).each(function(element){ 154 | this.cleanup(element); 155 | }, this); 156 | }.bind(this), 157 | ammendDom: function(container){ 158 | this.apply(container); 159 | }.bind(this) 160 | }); 161 | if (window.history && 'pushState' in history){ 162 | this.addEvent('updateHistory', function(url){ 163 | history.pushState(null, null, url); 164 | }); 165 | window.addEvent('popstate', function(){ 166 | if (this.options.reloadOnPopState && !Behavior._popping){ 167 | Behavior._popping = true; 168 | window.location.href = window.location.href; 169 | delete Behavior._popping; 170 | } 171 | }.bind(this)); 172 | } 173 | }, 174 | 175 | getDelegator: function(){ 176 | return this.delegator; 177 | }, 178 | 179 | setDelegator: function(delegator){ 180 | if (!instanceOf(delegator, Delegator)) throw new Error('Behavior.setDelegator only accepts instances of Delegator.'); 181 | this.delegator = delegator; 182 | return this; 183 | }, 184 | 185 | getContentElement: function(){ 186 | return this.options.container || document.body; 187 | }, 188 | 189 | //Applies all the behavior filters for an element. 190 | //container - (element) an element to apply the filters registered with this Behavior instance to. 191 | //force - (boolean; optional) passed through to applyFilter (see it for docs) 192 | apply: function(container, force){ 193 | var elements = this._getElements(container).each(function(element){ 194 | var plugins = []; 195 | element.getBehaviors().each(function(name){ 196 | var filter = this.getFilter(name); 197 | if (!filter){ 198 | this.fireEvent('error', ['There is no filter registered with this name: ', name, element]); 199 | } else { 200 | var config = filter.config; 201 | if (config.delay !== undefined){ 202 | this.applyFilter.delay(filter.config.delay, this, [element, filter, force]); 203 | } else if(config.delayUntil){ 204 | this._delayFilterUntil(element, filter, force); 205 | } else if(config.initializer){ 206 | this._customInit(element, filter, force); 207 | } else { 208 | plugins.append(this.applyFilter(element, filter, force, true)); 209 | } 210 | } 211 | }, this); 212 | plugins.each(function(plugin){ 213 | if (this.options.verbose) this.fireEvent('log', ['Firing plugin...']); 214 | plugin(); 215 | }, this); 216 | }, this); 217 | this.fireEvent('apply', [elements]); 218 | return this; 219 | }, 220 | 221 | _getElements: function(container){ 222 | if (typeOf(this.options.selector) == 'function') return this.options.selector(container); 223 | else return document.id(container).getElements(this.options.selector); 224 | }, 225 | 226 | //delays a filter until the event specified in filter.config.delayUntil is fired on the element 227 | _delayFilterUntil: function(element, filter, force){ 228 | var events = filter.config.delayUntil.split(','), 229 | attached = {}, 230 | inited = false; 231 | var clear = function(){ 232 | events.each(function(event){ 233 | element.removeEvent(event, attached[event]); 234 | }); 235 | clear = function(){}; 236 | }; 237 | events.each(function(event){ 238 | var init = function(e){ 239 | clear(); 240 | if (inited) return; 241 | inited = true; 242 | var setup = filter.setup; 243 | filter.setup = function(element, api, _pluginResult){ 244 | api.event = e; 245 | return setup.apply(filter, [element, api, _pluginResult]); 246 | }; 247 | this.applyFilter(element, filter, force); 248 | filter.setup = setup; 249 | }.bind(this); 250 | element.addEvent(event, init); 251 | attached[event] = init; 252 | }, this); 253 | }, 254 | 255 | //runs custom initiliazer defined in filter.config.initializer 256 | _customInit: function(element, filter, force){ 257 | var api = this._getAPI(element, filter); 258 | api.runSetup = this.applyFilter.pass([element, filter, force], this); 259 | filter.config.initializer(element, api); 260 | }, 261 | 262 | //Applies a specific behavior to a specific element. 263 | //element - the element to which to apply the behavior 264 | //filter - (object) a specific behavior filter, typically one registered with this instance or registered globally. 265 | //force - (boolean; optional) apply the behavior to each element it matches, even if it was previously applied. Defaults to *false*. 266 | //_returnPlugins - (boolean; optional; internal) if true, plugins are not rendered but instead returned as an array of functions 267 | //_pluginTargetResult - (obj; optional internal) if this filter is a plugin for another, this is whatever that target filter returned 268 | // (an instance of a class for example) 269 | applyFilter: function(element, filter, force, _returnPlugins, _pluginTargetResult){ 270 | var pluginsToReturn = []; 271 | if (this.options.breakOnErrors){ 272 | pluginsToReturn = this._applyFilter.apply(this, arguments); 273 | } else { 274 | try { 275 | pluginsToReturn = this._applyFilter.apply(this, arguments); 276 | } catch (e){ 277 | this.fireEvent('error', ['Could not apply the behavior ' + filter.name, e.message]); 278 | } 279 | } 280 | return _returnPlugins ? pluginsToReturn : this; 281 | }, 282 | 283 | //see argument list above for applyFilter 284 | _applyFilter: function(element, filter, force, _returnPlugins, _pluginTargetResult){ 285 | var pluginsToReturn = []; 286 | element = document.id(element); 287 | //get the filters already applied to this element 288 | var applied = getApplied(element); 289 | //if this filter is not yet applied to the element, or we are forcing the filter 290 | if (!applied[filter.name] || force){ 291 | if (this.options.verbose) this.fireEvent('log', ['Applying behavior: ', filter.name, element]); 292 | //if it was previously applied, garbage collect it 293 | if (applied[filter.name]) applied[filter.name].cleanup(element); 294 | var api = this._getAPI(element, filter); 295 | 296 | //deprecated 297 | api.markForCleanup = filter.markForCleanup.bind(filter); 298 | api.onCleanup = function(fn){ 299 | filter.markForCleanup(element, fn); 300 | }; 301 | 302 | if (filter.config.deprecated && this.options.enableDeprecation) api.deprecate(filter.config.deprecated); 303 | if (filter.config.deprecateAsJSON && this.options.enableDeprecation) api.deprecate(filter.config.deprecatedAsJSON, true); 304 | 305 | //deal with requirements and defaults 306 | if (filter.config.requireAs){ 307 | api.requireAs(filter.config.requireAs); 308 | } else if (filter.config.require){ 309 | api.require.apply(api, Array.from(filter.config.require)); 310 | } 311 | 312 | if (filter.config.defaults) api.setDefault(filter.config.defaults); 313 | 314 | //apply the filter 315 | if (Behavior.debugging && Behavior.debugging.contains(filter.name)) debugger; 316 | var result = filter.setup(element, api, _pluginTargetResult); 317 | if (filter.config.returns && !instanceOf(result, filter.config.returns)){ 318 | throw new Error("Filter " + filter.name + " did not return a valid instance."); 319 | } 320 | element.store('Behavior Filter result:' + filter.name, result); 321 | if (this.options.verbose){ 322 | if (result && !_pluginTargetResult) this.fireEvent('log', ['Successfully applied behavior: ', filter.name, element, result]); 323 | else this.fireEvent('warn', ['Behavior applied, but did not return result: ', filter.name, element, result]); 324 | } 325 | 326 | //and mark it as having been previously applied 327 | applied[filter.name] = filter; 328 | //apply all the plugins for this filter 329 | var plugins = this.getPlugins(filter.name); 330 | if (plugins){ 331 | for (var name in plugins){ 332 | if (_returnPlugins){ 333 | pluginsToReturn.push(this.applyFilter.pass([element, plugins[name], force, null, result], this)); 334 | } else { 335 | this.applyFilter(element, plugins[name], force, null, result); 336 | } 337 | } 338 | } 339 | } 340 | return pluginsToReturn; 341 | }, 342 | 343 | //given a name, returns a registered behavior 344 | getFilter: function(name){ 345 | return this._registered[name] || Behavior.getFilter(name); 346 | }, 347 | 348 | getPlugins: function(name){ 349 | return this._plugins[name] || Behavior._plugins[name]; 350 | }, 351 | 352 | //Garbage collects all applied filters for an element and its children. 353 | //element - (*element*) container to cleanup 354 | //ignoreChildren - (*boolean*; optional) if *true* only the element will be cleaned, otherwise the element and all the 355 | // children with filters applied will be cleaned. Defaults to *false*. 356 | cleanup: function(element, ignoreChildren){ 357 | element = document.id(element); 358 | var applied = getApplied(element); 359 | for (var filter in applied){ 360 | applied[filter].cleanup(element); 361 | element.eliminate('Behavior Filter result:' + filter); 362 | delete applied[filter]; 363 | } 364 | if (!ignoreChildren) this._getElements(element).each(this.cleanup, this); 365 | return this; 366 | } 367 | 368 | }); 369 | 370 | //Export these for use elsewhere (notabily: Delegator). 371 | Behavior.getLog = getLog; 372 | Behavior.PassMethods = PassMethods; 373 | Behavior.GetAPI = GetAPI; 374 | 375 | 376 | //Returns the applied behaviors for an element. 377 | var getApplied = function(el){ 378 | return el.retrieve('_appliedBehaviors', {}); 379 | }; 380 | 381 | //Registers a behavior filter. 382 | //name - the name of the filter 383 | //fn - a function that applies the filter to the given element 384 | //overwrite - (boolean) if true, will overwrite existing filter if one exists; defaults to false. 385 | var addFilter = function(name, fn, overwrite){ 386 | if (!this._registered[name] || overwrite) this._registered[name] = new Behavior.Filter(name, fn); 387 | else throw new Error('Could not add the Behavior filter "' + name +'" as a previous trigger by that same name exists.'); 388 | }; 389 | 390 | var addFilters = function(obj, overwrite){ 391 | for (var name in obj){ 392 | addFilter.apply(this, [name, obj[name], overwrite]); 393 | } 394 | }; 395 | 396 | //Registers a behavior plugin 397 | //filterName - (*string*) the filter (or plugin) this is a plugin for 398 | //name - (*string*) the name of this plugin 399 | //setup - a function that applies the filter to the given element 400 | var addPlugin = function(filterName, name, setup, overwrite){ 401 | if (!this._plugins[filterName]) this._plugins[filterName] = {}; 402 | if (!this._plugins[filterName][name] || overwrite) this._plugins[filterName][name] = new Behavior.Filter(name, setup); 403 | else throw new Error('Could not add the Behavior filter plugin "' + name +'" as a previous trigger by that same name exists.'); 404 | }; 405 | 406 | var addPlugins = function(obj, overwrite){ 407 | for (var name in obj){ 408 | addPlugin.apply(this, [obj[name].fitlerName, obj[name].name, obj[name].setup], overwrite); 409 | } 410 | }; 411 | 412 | var setFilterDefaults = function(name, defaults){ 413 | var filter = this.getFilter(name); 414 | if (!filter.config.defaults) filter.config.defaults = {}; 415 | Object.append(filter.config.defaults, defaults); 416 | }; 417 | 418 | var cloneFilter = function(name, newName, defaults){ 419 | var filter = Object.clone(this.getFilter(name)); 420 | addFilter.apply(this, [newName, filter.config]); 421 | this.setFilterDefaults(newName, defaults); 422 | }; 423 | 424 | //Add methods to the Behavior namespace for global registration. 425 | Object.append(Behavior, { 426 | _registered: {}, 427 | _plugins: {}, 428 | addGlobalFilter: addFilter, 429 | addGlobalFilters: addFilters, 430 | addGlobalPlugin: addPlugin, 431 | addGlobalPlugins: addPlugins, 432 | setFilterDefaults: setFilterDefaults, 433 | cloneFilter: cloneFilter, 434 | getFilter: function(name){ 435 | return this._registered[name]; 436 | } 437 | }); 438 | //Add methods to the Behavior class for instance registration. 439 | Behavior.implement({ 440 | _registered: {}, 441 | _plugins: {}, 442 | addFilter: addFilter, 443 | addFilters: addFilters, 444 | addPlugin: addPlugin, 445 | addPlugins: addPlugins, 446 | cloneFilter: cloneFilter, 447 | setFilterDefaults: setFilterDefaults 448 | }); 449 | 450 | //This class is an actual filter that, given an element, alters it with specific behaviors. 451 | Behavior.Filter = new Class({ 452 | 453 | config: { 454 | /** 455 | returns: Foo, 456 | require: ['req1', 'req2'], 457 | //or 458 | requireAs: { 459 | req1: Boolean, 460 | req2: Number, 461 | req3: String 462 | }, 463 | defaults: { 464 | opt1: false, 465 | opt2: 2 466 | }, 467 | //simple example: 468 | setup: function(element, API){ 469 | var kids = element.getElements(API.get('selector')); 470 | //some validation still has to occur here 471 | if (!kids.length) API.fail('there were no child elements found that match ', API.get('selector')); 472 | if (kids.length < 2) API.warn("there weren't more than 2 kids that match", API.get('selector')); 473 | var fooInstance = new Foo(kids, API.get('opt1', 'opt2')); 474 | API.onCleanup(function(){ 475 | fooInstance.destroy(); 476 | }); 477 | return fooInstance; 478 | }, 479 | delayUntil: 'mouseover', 480 | //OR 481 | delay: 100, 482 | //OR 483 | initializer: function(element, API){ 484 | element.addEvent('mouseover', API.runSetup); //same as specifying event 485 | //or 486 | API.runSetup.delay(100); //same as specifying delay 487 | //or something completely esoteric 488 | var timer = (function(){ 489 | if (element.hasClass('foo')){ 490 | clearInterval(timer); 491 | API.runSetup(); 492 | } 493 | }).periodical(100); 494 | //or 495 | API.addEvent('someBehaviorEvent', API.runSetup); 496 | }); 497 | */ 498 | }, 499 | 500 | //Pass in an object with the following properties: 501 | //name - the name of this filter 502 | //setup - a function that applies the filter to the given element 503 | initialize: function(name, setup){ 504 | this.name = name; 505 | if (typeOf(setup) == "function"){ 506 | this.setup = setup; 507 | } else { 508 | Object.append(this.config, setup); 509 | this.setup = this.config.setup; 510 | } 511 | this._cleanupFunctions = new Table(); 512 | }, 513 | 514 | //Stores a garbage collection pointer for a specific element. 515 | //Example: if your filter enhances all the inputs in the container 516 | //you might have a function that removes that enhancement for garbage collection. 517 | //You would mark each input matched with its own cleanup function. 518 | //NOTE: this MUST be the element passed to the filter - the element with this filters 519 | // name in its data-behavior property. I.E.: 520 | //
521 | // 522 | //
523 | //If this filter is FormValidator, you can mark the form for cleanup, but not, for example 524 | //the input. Only elements that match this filter can be marked. 525 | markForCleanup: function(element, fn){ 526 | var functions = this._cleanupFunctions.get(element); 527 | if (!functions) functions = []; 528 | functions.include(fn); 529 | this._cleanupFunctions.set(element, functions); 530 | return this; 531 | }, 532 | 533 | //Garbage collect a specific element. 534 | //NOTE: this should be an element that has a data-behavior property that matches this filter. 535 | cleanup: function(element){ 536 | var marks = this._cleanupFunctions.get(element); 537 | if (marks){ 538 | marks.each(function(fn){ fn(); }); 539 | this._cleanupFunctions.erase(element); 540 | } 541 | return this; 542 | } 543 | 544 | }); 545 | 546 | Behavior.debug = function(name){ 547 | if (!Behavior.debugging) Behavior.debugging = []; 548 | Behavior.debugging.push(name); 549 | }; 550 | 551 | Behavior.elementDataProperty = 'behavior'; 552 | 553 | // element fetching 554 | 555 | /* 556 | private method 557 | given an element and a selector, fetches elements relative to 558 | that element. boolean 'multi' determines if its getElement or getElements 559 | special cases for when the selector == 'window' (returns the window) 560 | and selector == 'self' (returns the element) 561 | - for both of those, if multi is true returns 562 | new Elements([self]) or new Elements([window]) 563 | */ 564 | var getTargets = function(element, selector, multi){ 565 | // get the targets 566 | if (selector && selector != 'self' && selector != 'window') return element[multi ? 'getElements' : 'getElement'](selector); 567 | if (selector == 'window') return multi ? new Elements([window]) : window; 568 | return multi ? new Elements([element]) : element; 569 | }; 570 | 571 | /* 572 | see above; public interface for getting a single element 573 | */ 574 | Behavior.getTarget = function(element, selector){ 575 | return getTargets(element, selector, false); 576 | }; 577 | 578 | /* 579 | see above; public interface for getting numerous elements 580 | */ 581 | Behavior.getTargets = function(element, selector){ 582 | return getTargets(element, selector, true); 583 | }; 584 | 585 | Element.implement({ 586 | 587 | addBehaviorFilter: function(name){ 588 | return this.setData(Behavior.elementDataProperty, this.getBehaviors().include(name).join(' ')); 589 | }, 590 | 591 | removeBehaviorFilter: function(name){ 592 | return this.setData(Behavior.elementDataProperty, this.getBehaviors().erase(name).join(' ')); 593 | }, 594 | 595 | getBehaviors: function(){ 596 | var filters = this.getData(Behavior.elementDataProperty); 597 | if (!filters) return []; 598 | return filters.trim().split(spaceOrCommaRegex); 599 | }, 600 | 601 | hasBehavior: function(name){ 602 | return this.getBehaviors().contains(name); 603 | }, 604 | 605 | getBehaviorResult: function(name){ 606 | return this.retrieve('Behavior Filter result:' + name); 607 | } 608 | 609 | }); 610 | 611 | 612 | })(); 613 | -------------------------------------------------------------------------------- /Source/BehaviorAPI.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: BehaviorAPI 4 | description: HTML getters for Behavior's API model. 5 | requires: [Core/Class, /Element.Data] 6 | provides: [BehaviorAPI] 7 | ... 8 | */ 9 | 10 | 11 | (function(){ 12 | //see Docs/BehaviorAPI.md for documentation of public methods. 13 | 14 | var reggy = /[^a-z0-9\-]/gi, 15 | dots = /\./g; 16 | 17 | window.BehaviorAPI = new Class({ 18 | element: null, 19 | prefix: '', 20 | defaults: {}, 21 | 22 | initialize: function(element, prefix){ 23 | this.element = element; 24 | this.prefix = prefix.toLowerCase().replace(dots, '-').replace(reggy, ''); 25 | }, 26 | 27 | /****************** 28 | * PUBLIC METHODS 29 | ******************/ 30 | 31 | get: function(/* name[, name, name, etc] */){ 32 | if (arguments.length > 1) return this._getObj(Array.from(arguments)); 33 | return this._getValue(arguments[0]); 34 | }, 35 | 36 | getAs: function(/*returnType, name, defaultValue OR {name: returnType, name: returnType, etc}*/){ 37 | if (typeOf(arguments[0]) == 'object') return this._getValuesAs.apply(this, arguments); 38 | return this._getValueAs.apply(this, arguments); 39 | }, 40 | 41 | require: function(/* name[, name, name, etc] */){ 42 | for (var i = 0; i < arguments.length; i++){ 43 | if (this._getValue(arguments[i]) == undefined) throw new Error('Could not retrieve ' + this.prefix + '-' + arguments[i] + ' option from element.'); 44 | } 45 | return this; 46 | }, 47 | 48 | requireAs: function(returnType, name /* OR {name: returnType, name: returnType, etc}*/){ 49 | var val; 50 | if (typeOf(arguments[0]) == 'object'){ 51 | for (var objName in arguments[0]){ 52 | val = this._getValueAs(arguments[0][objName], objName); 53 | if (val === undefined || val === null) throw new Error("Could not retrieve " + this.prefix + '-' + objName + " option from element."); 54 | } 55 | } else { 56 | val = this._getValueAs(returnType, name); 57 | if (val === undefined || val === null) throw new Error("Could not retrieve " + this.prefix + '-' + name + " option from element."); 58 | } 59 | return this; 60 | }, 61 | 62 | setDefault: function(name, value /* OR {name: value, name: value, etc }*/){ 63 | if (typeOf(arguments[0]) == 'object'){ 64 | for (var objName in arguments[0]){ 65 | this.setDefault(objName, arguments[0][objName]); 66 | } 67 | return this; 68 | } 69 | name = name.camelCase(); 70 | 71 | switch (typeOf(value)){ 72 | case 'object': value = Object.clone(value); break; 73 | case 'array': value = Array.clone(value); break; 74 | case 'hash': value = new Hash(value); break; 75 | } 76 | 77 | this.defaults[name] = value; 78 | var setValue = this._getValue(name); 79 | var options = this._getOptions(); 80 | if (setValue == null){ 81 | options[name] = value; 82 | } else if (typeOf(setValue) == 'object' && typeOf(value) == 'object') { 83 | options[name] = Object.merge({}, value, setValue); 84 | } 85 | return this; 86 | }, 87 | 88 | refreshAPI: function(){ 89 | delete this.options; 90 | this.setDefault(this.defaults); 91 | return; 92 | }, 93 | 94 | /****************** 95 | * PRIVATE METHODS 96 | ******************/ 97 | 98 | //given an array of names, returns an object of key/value pairs for each name 99 | _getObj: function(names){ 100 | var obj = {}; 101 | names.each(function(name){ 102 | var value = this._getValue(name); 103 | if (value !== undefined) obj[name] = value; 104 | }, this); 105 | return obj; 106 | }, 107 | //gets the data-behaviorname-options object and parses it as JSON 108 | _getOptions: function(){ 109 | try { 110 | if (!this.options){ 111 | var options = this.element.getData(this.prefix + '-options', '{}').trim(); 112 | if (options === "") return this.options = {}; 113 | if (options && options.substring(0,1) != '{') options = '{' + options + '}'; 114 | var isSecure = JSON.isSecure(options); 115 | if (!isSecure) throw new Error('warning, options value for element is not parsable, check your JSON format for quotes, etc.'); 116 | this.options = isSecure ? JSON.decode(options, false) : {}; 117 | for (option in this.options) { 118 | this.options[option.camelCase()] = this.options[option]; 119 | } 120 | } 121 | } catch (e){ 122 | throw new Error('Could not get options from element; check your syntax. ' + this.prefix + '-options: "' + this.element.getData(this.prefix + '-options', '{}') + '"'); 123 | } 124 | return this.options; 125 | }, 126 | //given a name (string) returns the value for it 127 | _getValue: function(name){ 128 | name = name.camelCase(); 129 | var options = this._getOptions(); 130 | if (!options.hasOwnProperty(name)){ 131 | var inline = this.element.getData(this.prefix + '-' + name.hyphenate()); 132 | if (inline) options[name] = inline; 133 | } 134 | return options[name]; 135 | }, 136 | //given a Type and a name (string) returns the value for it coerced to that type if possible 137 | //else returns the defaultValue or null 138 | _getValueAs: function(returnType, name, defaultValue){ 139 | var value = this._getValue(name); 140 | if (value == null || value == undefined) return defaultValue; 141 | var coerced = this._coerceFromString(returnType, value); 142 | if (coerced == null) throw new Error("Could not retrieve value '" + name + "' as the specified type. Its value is: " + value); 143 | return coerced; 144 | }, 145 | //given an object of name/Type pairs, returns those as an object of name/value (as specified Type) pairs 146 | _getValuesAs: function(obj){ 147 | var returnObj = {}; 148 | for (var name in obj){ 149 | returnObj[name] = this._getValueAs(obj[name], name); 150 | } 151 | return returnObj; 152 | }, 153 | //attempts to run a value through the JSON parser. If the result is not of that type returns null. 154 | _coerceFromString: function(toType, value){ 155 | if (typeOf(value) == 'string' && toType != String){ 156 | if (JSON.isSecure(value)) value = JSON.decode(value, false); 157 | } 158 | if (instanceOf(value, toType)) return value; 159 | return null; 160 | } 161 | }); 162 | 163 | })(); 164 | -------------------------------------------------------------------------------- /Source/Delegator.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Delegator 4 | description: Allows for the registration of delegated events on a container. 5 | requires: [Core/Element.Delegation, Core/Options, Core/Events, /Event.Mock, /Behavior] 6 | provides: [Delegator, Delegator.verifyTargets] 7 | ... 8 | */ 9 | (function(){ 10 | 11 | var spaceOrCommaRegex = /\s*,\s*|\s+/g; 12 | 13 | var checkEvent = function(trigger, element, event){ 14 | if (!event) return true; 15 | return trigger.types.some(function(type){ 16 | var elementEvent = Element.Events[type]; 17 | if (elementEvent && elementEvent.condition){ 18 | return elementEvent.condition.call(element, event, type); 19 | } else { 20 | var eventType = elementEvent && elementEvent.base ? elementEvent.base : event.type; 21 | return eventType == type; 22 | } 23 | }); 24 | }; 25 | 26 | window.Delegator = new Class({ 27 | 28 | Implements: [Options, Events, Behavior.PassMethods, Behavior.GetAPI], 29 | 30 | options: { 31 | // breakOnErrors: false, 32 | // onTrigger: function(trigger, element, event, result){}, 33 | getBehavior: function(){}, 34 | onLog: Behavior.getLog('info'), 35 | onError: Behavior.getLog('error'), 36 | onWarn: Behavior.getLog('warn') 37 | }, 38 | 39 | initialize: function(options){ 40 | this.setOptions(options); 41 | this._bound = { 42 | eventHandler: this._eventHandler.bind(this) 43 | }; 44 | Delegator._instances.push(this); 45 | Object.each(Delegator._triggers, function(trigger){ 46 | this._eventTypes.combine(trigger.types); 47 | }, this); 48 | this.API = new Class({ Extends: BehaviorAPI }); 49 | this.passMethods({ 50 | addEvent: this.addEvent.bind(this), 51 | removeEvent: this.removeEvent.bind(this), 52 | addEvents: this.addEvents.bind(this), 53 | removeEvents: this.removeEvents.bind(this), 54 | fireEvent: this.fireEvent.bind(this), 55 | attach: this.attach.bind(this), 56 | trigger: this.trigger.bind(this), 57 | error: function(){ this.fireEvent('error', arguments); }.bind(this), 58 | fail: function(){ 59 | var msg = Array.join(arguments, ' '); 60 | throw new Error(msg); 61 | }, 62 | warn: function(){ 63 | this.fireEvent('warn', arguments); 64 | }.bind(this), 65 | getBehavior: function(){ 66 | return this.options.getBehavior(); 67 | }.bind(this), 68 | getDelegator: Function.from(this) 69 | }); 70 | 71 | this.bindToBehavior(this.options.getBehavior()); 72 | }, 73 | 74 | /* 75 | given an instance of Behavior, binds this delegator instance 76 | to the behavior instance. 77 | */ 78 | bindToBehavior: function(behavior){ 79 | if (!behavior) return; 80 | this.unbindFromBehavior(); 81 | this._behavior = behavior; 82 | if (this._behavior.options.verbose) this.options.verbose = true; 83 | if (!this._behaviorEvents){ 84 | var self = this; 85 | this._behaviorEvents = { 86 | destroyDom: function(elements){ 87 | self._behavior.fireEvent('destroyDom', elements); 88 | }, 89 | ammendDom: function(container){ 90 | self._behavior.fireEvent('ammendDom', container); 91 | }, 92 | updateHistory: function(url){ 93 | self._behavior.fireEvent('updateHistory', url); 94 | } 95 | }; 96 | } 97 | this.addEvents(this._behaviorEvents); 98 | }, 99 | 100 | getBehavior: function(){ 101 | return this._behavior; 102 | }, 103 | 104 | unbindFromBehavior: function(){ 105 | if (this._behaviorEvents && this._behavior){ 106 | this._behavior.removeEvents(this._behaviorEvents); 107 | delete this._behavior; 108 | } 109 | }, 110 | 111 | /* 112 | attaches this instance to a specified DOM element to 113 | monitor events to it and its children 114 | */ 115 | attach: function(target, _method){ 116 | _method = _method || 'addEvent'; 117 | target = document.id(target); 118 | if ((_method == 'addEvent' && this._attachedTo.contains(target)) || 119 | (_method == 'removeEvent') && !this._attachedTo.contains(target)) return this; 120 | // iterate over all the event types for registered filters and attach listener for each 121 | this._eventTypes.each(function(event){ 122 | target[_method](event + ':relay([data-trigger])', this._bound.eventHandler); 123 | }, this); 124 | if (_method == 'addEvent') this._attachedTo.push(target); 125 | else this._attachedTo.erase(target); 126 | return this; 127 | }, 128 | 129 | 130 | /* 131 | detaches this instance of delegator from the target 132 | */ 133 | detach: function(target){ 134 | if (target) this.attach(target, 'removeEvent'); 135 | else this._attachedTo.each(this.detach, this); 136 | return this; 137 | }, 138 | 139 | fireEventForElement: function(element, eventType, force){ 140 | var e = new Event.Mock(element, eventType); 141 | element.getTriggers().each(function(triggerName){ 142 | var trigger = this.getTrigger(triggerName); 143 | if (force || trigger.types.contains(eventType)){ 144 | this.trigger(triggerName, element, e); 145 | } 146 | }, this); 147 | element.fireEvent(eventType, [e]); 148 | }, 149 | 150 | /* 151 | invokes a specific trigger upon an element 152 | */ 153 | trigger: function(name, element, event, ignoreTypes, _api){ 154 | var e = event; 155 | // if the event is a string, create an mock event object 156 | if (!e || typeOf(e) == "string") e = new Event.Mock(element, e); 157 | if (this.options.verbose) this.fireEvent('log', ['Applying trigger: ', name, element, event]); 158 | 159 | // if the trigger is of the special types handled by delegator itself, 160 | // run those and remove them from the list of triggers 161 | switch(name){ 162 | case 'Stop': 163 | event.stop(); 164 | return; 165 | case 'PreventDefault': 166 | event.preventDefault(); 167 | return; 168 | case 'multi': 169 | this._handleMultiple(element, event); 170 | return; 171 | case 'any': 172 | this._runSwitch('any', element, event); 173 | return; 174 | case 'first': 175 | this._runSwitch('first', element, event, 'some'); 176 | return; 177 | default: 178 | var result, 179 | trigger = this.getTrigger(name); 180 | // warn if the trigger isn't found and exit quietly 181 | if (!trigger){ 182 | this.fireEvent('warn', 'Could not find a trigger by the name of ' + name); 183 | // check that the event type matches the types registered for the filter unless specifically ignoring types 184 | } else if (ignoreTypes || checkEvent(trigger, element, e)) { 185 | // invoke the trigger 186 | if (this.options.breakOnErrors){ 187 | result = this._trigger(trigger, element, e, _api); 188 | } else { 189 | try { 190 | result = this._trigger(trigger, element, e, _api); 191 | } catch(error) { 192 | this.fireEvent('error', ['Could not apply the trigger', name, error.message]); 193 | } 194 | } 195 | } 196 | // log the event 197 | if (this.options.verbose && result) this.fireEvent('log', ['Successfully applied trigger: ', name, element, event]); 198 | else if (this.options.verbose) this.fireEvent('log', ['Trigger applied, but did not return a result: ', name, element, event]); 199 | // return the result of the trigger 200 | return result; 201 | } 202 | }, 203 | 204 | // returns the trigger object for a given trigger name 205 | getTrigger: function(triggerName){ 206 | return this._triggers[triggerName] || Delegator._triggers[triggerName]; 207 | }, 208 | 209 | // adds additional event types for a given trigger 210 | addEventTypes: function(triggerName, types){ 211 | this.getTrigger(triggerName).types.combine(Array.from(types)); 212 | return this; 213 | }, 214 | 215 | /****************** 216 | * PRIVATE METHODS 217 | ******************/ 218 | 219 | /* 220 | invokes a trigger for a specified element 221 | */ 222 | _trigger: function(trigger, element, event, _api){ 223 | // create an instance of the API if one not already passed in; atypical to specify one, 224 | // really only used for the multi trigger functionality to set defaults 225 | var api = _api || this._getAPI(element, trigger); 226 | 227 | // if we're debugging, stop 228 | if (Delegator.debugging && Delegator.debugging.contains(name)) debugger; 229 | 230 | // set defaults, check requirements 231 | if (trigger.defaults) api.setDefault(trigger.defaults); 232 | if (trigger.requireAs) api.requireAs(trigger.requireAs); 233 | if (trigger.require) api.require.apply(api, Array.from(trigger.require)); 234 | 235 | // if the element is specified, check conditionals 236 | if (element && !this._checkConditionals(element, api)) return; 237 | 238 | // invoke the trigger, return result 239 | var result = trigger.handler.apply(this, [event, element, api]); 240 | this.fireEvent('trigger', [trigger, element, event, result]); 241 | return result; 242 | }, 243 | 244 | /* 245 | checks the conditionals on a trigger. Example: 246 | 247 | // invoke the foo trigger if this link has the class "foo" 248 | // in this example, it will not 249 | ... 254 | 255 | // inverse of above; invoke the foo trigger if the link 256 | // does NOT have the class "foo", which it doesn't, so 257 | // the trigger will be invoked 258 | ... 263 | 264 | this method is passed the element, the api instance, the conditional 265 | ({ 'self::hasClass': ['foo'] }), and the type ('if' or 'unless'). 266 | 267 | See: Delegator.verifyTargets for how examples of conditionals. 268 | */ 269 | _checkConditionals: function(element, api, _conditional){ 270 | 271 | var conditionalIf, conditionalUnless, result = true; 272 | 273 | if (_conditional){ 274 | conditionalIf = _conditional['if']; 275 | conditionalUnless = _conditional['unless']; 276 | } else { 277 | conditionalIf = api.get('if') ? api.getAs(Object, 'if') : null; 278 | conditionalUnless = api.get('unless') ? api.getAs(Object, 'unless') : null; 279 | } 280 | 281 | // no element? NO SOUP FOR YOU 282 | if (!element) result = false; 283 | // if this is an if conditional, fail if we don't verify 284 | if (conditionalIf && !Delegator.verifyTargets(element, conditionalIf, api)) result = false; 285 | // if this is an unless conditional, fail if we DO verify 286 | if (conditionalUnless && Delegator.verifyTargets(element, conditionalUnless, api)) result = false; 287 | 288 | // logging 289 | if (!result && this.options.verbose){ 290 | this.fireEvent('log', ['Not executing trigger due to conditional', element, _conditional]); 291 | } 292 | 293 | return result; 294 | }, 295 | 296 | /* 297 | event handler for all events we're monitoring on any of our attached DOM elements 298 | */ 299 | _eventHandler: function(event, target){ 300 | // execute the triggers 301 | target.getTriggers().each(function(trigger){ 302 | this.trigger(trigger, target, event); 303 | }, this); 304 | }, 305 | 306 | /* 307 | iterates over the special "multi" trigger configuration and invokes them 308 | */ 309 | _handleMultiple: function(element, event){ 310 | // make an api reader for the 'multi' options 311 | var api = this._getAPI(element, { name: 'multi' }); 312 | 313 | if (!this._checkConditionals(element, api)) return; 314 | 315 | // get the triggers (required) 316 | var triggers = api.getAs(Array, 'triggers'); 317 | // if there are triggers, run them 318 | if (triggers && triggers.length) this._runMultipleTriggers(element, event, triggers); 319 | }, 320 | 321 | /* 322 | given an element, event, and an array of triggers, run them; 323 | only used by the 'multi', 'any', and 'first' special delegators 324 | */ 325 | _runMultipleTriggers: function(element, event, triggers){ 326 | // iterate over the array of triggers 327 | triggers.each(function(trigger){ 328 | // if it's a string, invoke it 329 | // example: '.selector::trigger' << finds .selector and calls 'trigger' delegator on it 330 | if (typeOf(trigger) == 'string'){ 331 | this._invokeMultiTrigger(element, event, trigger); 332 | } else if (typeOf(trigger) == 'object'){ 333 | // if it's an object, iterate over it's keys and config 334 | // example: 335 | // { '.selector::trigger': {'arg':'whatevs'} } << same as above, but passes ['arg'] as argument 336 | // to the trigger as *defaults* for the trigger 337 | Object.each(trigger, function(config, key){ 338 | this._invokeMultiTrigger(element, event, key, config); 339 | }, this); 340 | } 341 | }, this); 342 | }, 343 | 344 | /* 345 | invokes a trigger with an optional default configuration for each target 346 | found for the trigger. 347 | trigger example: '.selector::trigger' << find .selector and invoke 'trigger' delegator 348 | */ 349 | _invokeMultiTrigger: function(element, event, trigger, config){ 350 | // split the trigger name 351 | trigger = this._splitTriggerName(trigger); 352 | if (!trigger) return; //craps out if the trigger is mal-formed 353 | // get the targets specified by that trigger 354 | var targets = Behavior.getTargets(element, trigger.selector); 355 | // iterate over each target 356 | targets.each(function(target){ 357 | var api; 358 | // create an api for the trigger/element combo and set defaults to the config (if config present) 359 | if (config) api = this._getAPI(target, trigger).setDefault(config); 360 | // invoke the trigger 361 | this.trigger(trigger.name, target, event, true, api); 362 | }, this); 363 | }, 364 | 365 | /* 366 | given a trigger name string, split it on "::" and return the name and selector 367 | invokes 368 | */ 369 | _splitTriggerName: function(str){ 370 | var split = str.split('::'), 371 | selector = split[0], 372 | name = split[1]; 373 | if (!name || !selector){ 374 | this.fireEvent('error', 'could not invoke multi delegator for ' + str + 375 | '; could not split on :: to derive selector and trigger name'); 376 | return; 377 | } 378 | return { 379 | name: name, 380 | selector: selector 381 | }; 382 | }, 383 | 384 | /* 385 | Runs the custom switch triggers. Examples: 386 | 387 | the 'first' trigger runs through all the groups 388 | checking their conditions until it finds one that 389 | passes, then executes the driggers defined in it. 390 | if no conditional clause is defined, that counts 391 | as a pass. 392 | 393 | ... 420 | 421 | */ 422 | _runSwitch: function(switchName, element, event, method){ 423 | method = method || 'each'; 424 | // make an api reader for the switch options 425 | var api = this._getAPI(element, { name: switchName }), 426 | switches = api.getAs(Array, 'switches'); 427 | 428 | if (!this._checkConditionals(element, api)) return; 429 | 430 | switches[method](function(config){ 431 | if (this._checkConditionals(element, api, config)){ 432 | this._runMultipleTriggers(element, event, config.triggers, method); 433 | return true; 434 | } else { 435 | return false; 436 | } 437 | }, this); 438 | }, 439 | 440 | 441 | /* 442 | function that attaches listerners for each unique 443 | event type for filtesr as they're added (but only once) 444 | */ 445 | _onRegister: function(eventTypes){ 446 | eventTypes.each(function(eventType){ 447 | if (!this._eventTypes.contains(eventType)){ 448 | this._attachedTo.each(function(element){ 449 | element.addEvent(eventType + ':relay([data-trigger])', this._bound.eventHandler); 450 | }, this); 451 | } 452 | this._eventTypes.include(eventType); 453 | }, this); 454 | }, 455 | 456 | _attachedTo: [], 457 | _eventTypes: [], 458 | _triggers: {} 459 | 460 | }); 461 | 462 | Delegator._triggers = {}; 463 | Delegator._instances = []; 464 | Delegator._onRegister = function(eventType){ 465 | this._instances.each(function(instance){ 466 | instance._onRegister(eventType); 467 | }); 468 | }; 469 | 470 | Delegator.register = function(eventTypes, name, handler, overwrite /** or eventType, obj, overwrite */){ 471 | eventTypes = Array.from(eventTypes); 472 | if (typeOf(name) == "object"){ 473 | var obj = name; 474 | for (name in obj){ 475 | this.register.apply(this, [eventTypes, name, obj[name], handler]); 476 | } 477 | return this; 478 | } 479 | if (!this._triggers[name] || overwrite){ 480 | if (typeOf(handler) == "function"){ 481 | handler = { 482 | handler: handler 483 | }; 484 | } 485 | handler.types = eventTypes; 486 | handler.name = name; 487 | this._triggers[name] = handler; 488 | this._onRegister(eventTypes); 489 | } else { 490 | throw new Error('Could add the trigger "' + name +'" as a previous trigger by that same name exists.'); 491 | } 492 | return this; 493 | }; 494 | 495 | Delegator.getTrigger = function(name){ 496 | return this._triggers[name]; 497 | }; 498 | 499 | Delegator.addEventTypes = function(triggerName, types){ 500 | var eventTypes = Array.from(types); 501 | var trigger = this.getTrigger(triggerName); 502 | if (trigger) trigger.types.combine(eventTypes); 503 | this._onRegister(eventTypes); 504 | return this; 505 | }; 506 | 507 | Delegator.debug = function(name){ 508 | if (!Delegator.debugging) Delegator.debugging = []; 509 | Delegator.debugging.push(name); 510 | }; 511 | 512 | Delegator.setTriggerDefaults = function(name, defaults){ 513 | var trigger = this.getTrigger(name); 514 | if (!trigger.defaults) trigger.defaults = {}; 515 | Object.append(trigger.defaults, defaults); 516 | }; 517 | 518 | Delegator.cloneTrigger = function(name, newName, defaults){ 519 | var filter = Object.clone(this.getTrigger(name)); 520 | this.register(filter.types, newName, filter); 521 | this.setTriggerDefaults(newName, defaults); 522 | }; 523 | 524 | 525 | Delegator.implement('register', Delegator.register); 526 | 527 | Element.implement({ 528 | 529 | addTrigger: function(name){ 530 | return this.setData('trigger', this.getTriggers().include(name).join(' ')); 531 | }, 532 | 533 | removeTrigger: function(name){ 534 | return this.setData('trigger', this.getTriggers().erase(name).join(' ')); 535 | }, 536 | 537 | getTriggers: function(){ 538 | var triggers = this.getData('trigger'); 539 | if (!triggers) return []; 540 | return triggers.trim().split(spaceOrCommaRegex); 541 | }, 542 | 543 | hasTrigger: function(name){ 544 | return this.getTriggers().contains(name); 545 | } 546 | 547 | }); 548 | 549 | 550 | /* 551 | conditional = the parsed json conditional configuration. Examples: 552 | 553 | 558 | This passes { 'self::hasClass': ['bar'] } through this parser 559 | which interpolates the 'self::hasClass' statement into an object that 560 | has the arguments specified below for verifyTargets, returning: 561 | { 562 | targets: 'self', 563 | method: 'hasClass', 564 | arguments: ['bar'] 565 | } 566 | */ 567 | Delegator.parseConditional = function(conditional){ 568 | Object.each(conditional, function(value, key){ 569 | if (key.contains('::')){ 570 | conditional.targets = key.split('::')[0]; 571 | conditional.method = key.split('::')[1]; 572 | conditional['arguments'] = value; 573 | } 574 | }); 575 | if (conditional.value === undefined) conditional.value = true; 576 | return conditional; 577 | }; 578 | 579 | /* 580 | Conditionals have the following properties: 581 | 582 | * target - (*string*) a css selector *relative to the element* to find a single element to test. 583 | * targets - (*string*) a css selector *relative to the element* to find a group of elements to test. If the conditional is true for any of them, the delegator is fired. 584 | * property - (*string*) a property of the target element to evaluate. Do not use with the `method` option. 585 | * method - (*string*) a method on the target element to invoke. Passed as arguments the `arguments` array (see below). Do not use with the `property` option. 586 | * arguments - (*array* of *strings*) arguments passed to the method of the target element specified in the `method` option. Ignored if the `property` option is used. 587 | * value - (*string*) A value to compare to either the value of the `property` of the target or the result of the `method` invoked upon it. 588 | */ 589 | Delegator.verifyTargets = function(el, conditional, api){ 590 | conditional = Delegator.parseConditional(conditional); 591 | 592 | // get the targets 593 | var targets = Behavior.getTargets(el, conditional.targets || conditional.target); 594 | if (targets.length == 0) api.fail('could not find target(s): ', conditional.targets || conditional.target); 595 | // check the targets for the conditionals 596 | return targets.some(function(target){ 597 | if (conditional.property) return target.get(conditional.property) === conditional.value; 598 | else if (conditional.method) return target[conditional.method].apply(target, Array.from(conditional['arguments'])) === conditional.value; 599 | else return !conditional.method && !conditional.property; 600 | }); 601 | }; 602 | 603 | })(); 604 | -------------------------------------------------------------------------------- /Source/Element.Data.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Element.Data 4 | description: Stores data in HTML5 data properties 5 | requires: [Core/Element, Core/JSON] 6 | provides: [Element.Data] 7 | script: Element.Data.js 8 | ... 9 | */ 10 | (function(){ 11 | 12 | JSON.isSecure = function(string){ 13 | //this verifies that the string is parsable JSON and not malicious (borrowed from JSON.js in MooTools, which in turn borrowed it from Crockford) 14 | //this version is a little more permissive, as it allows single quoted attributes because forcing the use of double quotes 15 | //is a pain when this stuff is used as HTML properties 16 | return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(string.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '').replace(/'[^'\\\n\r]*'/g, '')); 17 | }; 18 | 19 | Element.implement({ 20 | /* 21 | sets an HTML5 data property. 22 | arguments: 23 | name - (string) the data name to store; will be automatically prefixed with 'data-'. 24 | value - (string, number) the value to store. 25 | */ 26 | setData: function(name, value){ 27 | return this.set('data-' + name.hyphenate(), value); 28 | }, 29 | 30 | getData: function(name, defaultValue){ 31 | var value = this.get('data-' + name.hyphenate()); 32 | if (value != undefined){ 33 | return value; 34 | } else if (defaultValue != undefined){ 35 | this.setData(name, defaultValue); 36 | return defaultValue; 37 | } 38 | }, 39 | 40 | /* 41 | arguments: 42 | name - (string) the data name to store; will be automatically prefixed with 'data-' 43 | value - (string, array, or object) if an object or array the object will be JSON encoded; otherwise stored as provided. 44 | */ 45 | setJSONData: function(name, value){ 46 | return this.setData(name, JSON.encode(value)); 47 | }, 48 | 49 | /* 50 | retrieves a property from HTML5 data property you specify 51 | 52 | arguments: 53 | name - (retrieve) the data name to store; will be automatically prefixed with 'data-' 54 | strict - (boolean) if true, will set the JSON.decode's secure flag to true; otherwise the value is still tested but allows single quoted attributes. 55 | defaultValue - (string, array, or object) the value to set if no value is found (see storeData above) 56 | */ 57 | getJSONData: function(name, strict, defaultValue){ 58 | strict = strict === undefined ? false : strict; 59 | var value = this.get('data-' + name); 60 | if (value != undefined){ 61 | if (value && JSON.isSecure(value)) { 62 | return JSON.decode(value, strict); 63 | } else { 64 | return value; 65 | } 66 | } else if (defaultValue != undefined){ 67 | this.setJSONData(name, defaultValue); 68 | return defaultValue; 69 | } 70 | } 71 | 72 | }); 73 | 74 | })(); 75 | -------------------------------------------------------------------------------- /Source/Event.Mock.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Event.Mock 4 | description: Supplies a Mock Event object for use on fireEvent 5 | license: MIT-style 6 | authors: 7 | - Arieh Glazer 8 | requires: [Core/Event] 9 | provides: [Event.Mock] 10 | ... 11 | */ 12 | 13 | (function(window){ 14 | window.Event = window.Event || window.DOMEvent; //for 1.4 nocompat 15 | 16 | /** 17 | * creates a Mock event to be used with fire event 18 | * @param Element target an element to set as the target of the event - not required 19 | * @param string type the type of the event to be fired. Will not be used by IE - not required. 20 | * 21 | */ 22 | Event.Mock = function(target,type){ 23 | type = type || 'click'; 24 | 25 | var e = { 26 | type: type, 27 | target: target 28 | }; 29 | 30 | if (document.createEvent){ 31 | e = document.createEvent('HTMLEvents'); 32 | e.initEvent( 33 | type //event type 34 | , false //bubbles - set to false because the event should like normal fireEvent 35 | , true //cancelable 36 | ); 37 | } 38 | 39 | e = new Event(e); 40 | 41 | e.target = target; 42 | 43 | return e; 44 | }; 45 | 46 | })(window); 47 | -------------------------------------------------------------------------------- /Tests/Specs/Behavior/Behavior.Benchmarks.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Behavior.Benchmarks 4 | description: n/a 5 | requires: [Behavior-Tests/Behavior.SpecsHelpers] 6 | provides: [Behavior.Benchmarks] 7 | ... 8 | */ 9 | (function(){ 10 | 11 | // Behavior.addGlobalFilter('BenchmarkFilter', function(){}); 12 | 13 | // Behavior.addFilterTest({ 14 | // filterName: 'BenchmarkFilter', 15 | // desc: 'Applies an empty filter', 16 | // content: '
', 17 | // specs: false 18 | // }); 19 | 20 | })(); 21 | -------------------------------------------------------------------------------- /Tests/Specs/Behavior/Behavior.Events.Specs.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Behavior.Events.Specs 4 | description: n/a 5 | requires: [Behavior-Tests/Behavior.SpecsHelpers, Behavior/Behavior.Events] 6 | provides: [Behavior.Events.Specs] 7 | ... 8 | */ 9 | if (window.describe){ 10 | (function(){ 11 | 12 | describe('Behavior.Events', function(){ 13 | 14 | var SimpleEventsClass = new Class({ 15 | 16 | Implements: [Events], 17 | 18 | get: function(what){ 19 | return what || 'nothing'; 20 | } 21 | 22 | }); 23 | 24 | Behavior.addGlobalFilter('SimpleEventsClass', { 25 | returns: SimpleEventsClass, 26 | setup: function(){ 27 | return new SimpleEventsClass(); 28 | } 29 | }); 30 | 31 | var d = new Delegator({ 32 | onLog: function(){}, 33 | onError: function(){}, 34 | onWarn: function(){} 35 | }); 36 | behaviorInstance.setDelegator(d); 37 | 38 | var container = new Element('div.container'); 39 | var target = new Element('div.simple', { 40 | 'data-behavior': 'SimpleEventsClass' 41 | }).inject(container); 42 | 43 | var simpleTriggerValue = '-'; 44 | Delegator.register('click', { 45 | simpleTrigger: function(event, target, api){ simpleTriggerValue = api.get('value'); } 46 | }); 47 | 48 | var div = new Element('div') 49 | .addBehaviorFilter('addEvent') 50 | .setJSONData('addevent-options', { 51 | events: { 52 | '!.container div.simple::SimpleEventsClass': { 53 | 'elementMethodCall': [ 54 | { 55 | '.bar::simpleTrigger': { 56 | 'value': 'methodCallWorked', 57 | 'if': { 58 | 'self::hasClass': 'baz' 59 | } 60 | } 61 | } 62 | ], 63 | 'eventArgumentCheck': [ 64 | { 65 | '.bar::simpleTrigger': { 66 | 'value': 'argumentCheckWorked', 67 | 'if': { 68 | 'eventArguments[0]': 'does', 69 | 'eventArguments[1]': 'work' 70 | } 71 | } 72 | } 73 | ], 74 | 'instancePropertyCheck': [ 75 | { 76 | '.bar::simpleTrigger': { 77 | 'value': 'instancePropertyCheckWorked', 78 | 'if': { 79 | 'instance.prop': 'it works!' 80 | } 81 | } 82 | } 83 | ], 84 | 'methodWithoutArgumentsCheck': [ 85 | { 86 | '.bar::simpleTrigger': { 87 | 'value': 'methodWithoutArgumentsCheckWorked', 88 | 'if': { 89 | 'instance.get()': 'nothing' 90 | } 91 | } 92 | } 93 | ], 94 | 'methodWithArgumentsCheck': [ 95 | { 96 | '.bar::simpleTrigger': { 97 | 'value': 'methodWithoutArgumentsCheckWorked', 98 | 'if': { 99 | 'instance.get()': 'something', 100 | 'arguments': ['something'] 101 | } 102 | } 103 | } 104 | ] 105 | } 106 | } 107 | }).inject(container); 108 | var bar = new Element('div.bar').inject(div); 109 | 110 | // adds events 111 | behaviorInstance.apply(container); 112 | 113 | 114 | it('should add a simple event monitor and check against an element method', function(){ 115 | 116 | // no change to our default value 117 | expect(simpleTriggerValue).toBe('-'); 118 | 119 | var instance = target.getBehaviorResult('SimpleEventsClass'); 120 | 121 | // now we fire our event 122 | instance.fireEvent('elementMethodCall'); 123 | 124 | // still no change, as conditional doesn't match 125 | expect(simpleTriggerValue).toBe('-'); 126 | 127 | // we add the .baz class, now our conditional matches 128 | div.addClass('baz'); 129 | 130 | // fire event again 131 | instance.fireEvent('elementMethodCall'); 132 | 133 | // should have changed 134 | expect(simpleTriggerValue).toBe('methodCallWorked'); 135 | 136 | simpleTriggerValue = '-'; 137 | 138 | }); 139 | 140 | it('should add a simple event monitor and check against an argument', function(){ 141 | 142 | // no change to our default value 143 | expect(simpleTriggerValue).toBe('-'); 144 | 145 | var instance = target.getBehaviorResult('SimpleEventsClass'); 146 | 147 | // now we fire our event 148 | instance.fireEvent('eventArgumentCheck', ['it', 'does', 'not', 'work']); 149 | 150 | // still no change, as conditional doesn't match 151 | expect(simpleTriggerValue).toBe('-'); 152 | 153 | // fire event again 154 | instance.fireEvent('eventArgumentCheck', ['does', 'work']); 155 | 156 | // should have changed 157 | expect(simpleTriggerValue).toBe('argumentCheckWorked'); 158 | 159 | simpleTriggerValue = '-'; 160 | 161 | }); 162 | 163 | it('should add a simple event monitor and check against an instance property', function(){ 164 | 165 | // no change to our default value 166 | expect(simpleTriggerValue).toBe('-'); 167 | 168 | var instance = target.getBehaviorResult('SimpleEventsClass'); 169 | 170 | // now we fire our event 171 | instance.fireEvent('instancePropertyCheck'); 172 | 173 | // still no change, as conditional doesn't match 174 | expect(simpleTriggerValue).toBe('-'); 175 | 176 | // fire event again 177 | instance.prop = 'it works!'; 178 | instance.fireEvent('instancePropertyCheck'); 179 | 180 | // should have changed 181 | expect(simpleTriggerValue).toBe('instancePropertyCheckWorked'); 182 | 183 | simpleTriggerValue = '-'; 184 | 185 | }); 186 | 187 | it('should add a simple event monitor and check against an instance method', function(){ 188 | 189 | // no change to our default value 190 | expect(simpleTriggerValue).toBe('-'); 191 | 192 | var instance = target.getBehaviorResult('SimpleEventsClass'); 193 | 194 | // now we fire our event 195 | instance.fireEvent('methodWithoutArgumentsCheck'); 196 | expect(simpleTriggerValue).toBe('methodWithoutArgumentsCheckWorked'); 197 | 198 | instance.fireEvent('methodWithArgumentsCheck'); 199 | 200 | // should have changed 201 | expect(simpleTriggerValue).toBe('methodWithoutArgumentsCheckWorked'); 202 | 203 | simpleTriggerValue = '-'; 204 | 205 | }); 206 | 207 | }); 208 | 209 | })(); 210 | } 211 | -------------------------------------------------------------------------------- /Tests/Specs/Behavior/Behavior.Specs.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Behavior.Specs 4 | description: n/a 5 | requires: [Behavior-Tests/Behavior.SpecsHelpers] 6 | provides: [Behavior.Specs] 7 | ... 8 | */ 9 | if (window.describe){ 10 | (function(){ 11 | var container = new Element('div'); 12 | var target = new Element('div', { 13 | 'data-behavior': 'Test1 Test2', 14 | 'data-defaults-options':'{"foo":"bar"}', 15 | 'data-require-options':'{"foo": "bar", "nine": 9, "arr": [1, 2, 3]}', 16 | 'data-require-number': '0', 17 | 'data-require-true': 'true', 18 | 'data-require-false': 'false' 19 | }).inject(container); 20 | 21 | var SimpleClass = new Class({ 22 | simple: 'simple' 23 | }); 24 | 25 | describe('Behavior', function(){ 26 | 27 | it('should register a delegator', function(){ 28 | var d = new Delegator({ 29 | onLog: function(){}, 30 | onError: function(){}, 31 | onWarn: function(){} 32 | }); 33 | behaviorInstance.setDelegator(d); 34 | expect(behaviorInstance.getDelegator()).toBe(d); 35 | var error; 36 | try { 37 | behaviorInstance.setDelegator({}); 38 | } catch(e){ 39 | error = true; 40 | } 41 | expect(error).toBe(true); 42 | }); 43 | 44 | it('should register a filter', function(){ 45 | var test1 = ClassAdder.makeAdder('one'); 46 | Behavior.addGlobalFilter('Test1', test1); 47 | expect(behaviorInstance.getFilter('Test1').setup).toBe(test1); 48 | 49 | var test2 = ClassAdder.makeAdder('two'); 50 | Behavior.addGlobalFilters({ 51 | Test2: test2 52 | }); 53 | 54 | expect(behaviorInstance.getFilter('Test2').setup).toBe(test2); 55 | }); 56 | 57 | it('should not overwrite a filter unless forced', function(){ 58 | var test1 = function(){}; 59 | var test2 = function(){}; 60 | Behavior.addGlobalFilter('T1', test1); 61 | try { 62 | Behavior.addGlobalFilter('T1', test2); 63 | expect(true).toBe(false); //should not get here 64 | } catch(e){ 65 | expect(e.message).toBe('Could not add the Behavior filter "T1" as a previous trigger by that same name exists.'); 66 | } 67 | expect(behaviorInstance.getFilter('T1').setup).toNotBe(test2); 68 | }); 69 | 70 | it('should overwrite a filter when forced', function(){ 71 | var test1 = function(){}; 72 | var test2 = function(){}; 73 | Behavior.addGlobalFilter('T2', test1); 74 | Behavior.addGlobalFilter('T2', test2, true); 75 | expect(behaviorInstance.getFilter('Test1').setup).toNotBe(test2); 76 | }); 77 | 78 | it('should invoke a filter', function(){ 79 | behaviorInstance.applyFilter(target, behaviorInstance.getFilter('Test1')); 80 | expect(target.getBehaviorResult('Test1')).toBeTruthy(); 81 | }); 82 | 83 | it('should store the filter result', function(){ 84 | Behavior.addGlobalFilter('SimpleClass', { 85 | returns: SimpleClass, 86 | setup: function(){ 87 | return new SimpleClass(); 88 | } 89 | }); 90 | target.addBehaviorFilter('SimpleClass'); 91 | behaviorInstance.apply(container); 92 | target.removeBehaviorFilter('SimpleClass'); 93 | expect(target.getBehaviorResult('SimpleClass')).toBeTruthy(); 94 | }); 95 | 96 | it('should fail if a filter fails to return a proper instance', function(){ 97 | Behavior.addGlobalFilter('SimpleClass2', { 98 | returns: SimpleClass, 99 | setup: function(){ 100 | //note that this does not return! 101 | new SimpleClass(); 102 | } 103 | }); 104 | target.addBehaviorFilter('SimpleClass2'); 105 | behaviorInstance.options.breakOnErrors = true; 106 | try { 107 | behaviorInstance.apply(container); 108 | expect(true).toBe(false); //this shouldn't get this far as an error should be thrown 109 | } catch(e) { 110 | expect(e.message).toBe("Filter SimpleClass2 did not return a valid instance."); 111 | } 112 | behaviorInstance.options.breakOnErrors = false; 113 | target.removeBehaviorFilter('SimpleClass2'); 114 | expect(target.getBehaviorResult('SimpleClass2')).toBeFalsy(); 115 | }); 116 | 117 | 118 | it('should create a filter that fails', function(){ 119 | Behavior.addGlobalFilter('Failure', { 120 | setup: function(element, api){ 121 | api.fail('this thing is totally broken'); 122 | } 123 | }); 124 | target.addBehaviorFilter('Failure'); 125 | behaviorInstance.options.breakOnErrors = true; 126 | try { 127 | behaviorInstance.apply(container); 128 | expect(true).toBe(false); //this shouldn't get this far as an error should be thrown 129 | } catch(e) { 130 | expect(e.message).toBe("this thing is totally broken"); 131 | } 132 | behaviorInstance.options.breakOnErrors = false; 133 | target.removeBehaviorFilter('Failure'); 134 | }); 135 | 136 | it('should create a filter that warns', function(){ 137 | var warning, 138 | warner = function(msg){ 139 | warning = msg; 140 | }; 141 | behaviorInstance.addEvent('warn', warner); 142 | Behavior.addGlobalFilter('Warn', { 143 | setup: function(element, api){ 144 | api.warn("you've been warned"); 145 | } 146 | }); 147 | target.addBehaviorFilter('Warn'); 148 | behaviorInstance.apply(container); 149 | behaviorInstance.removeEvent('warn', warner); 150 | expect(warning).toBe("you've been warned"); 151 | target.removeBehaviorFilter('Warn'); 152 | }); 153 | 154 | it('should not invoke a filter twice unless forced', function(){ 155 | behaviorInstance.applyFilter(target, behaviorInstance.getFilter('Test1')); 156 | expect(target.getBehaviorResult('Test1').getCount()).toBe(1); 157 | behaviorInstance.applyFilter(target, behaviorInstance.getFilter('Test1'), true); 158 | expect(target.getBehaviorResult('Test1').getCount()).toBe(2); 159 | }); 160 | 161 | it('should clean up a filter', function(){ 162 | behaviorInstance.cleanup(target); 163 | expect(target.getBehaviorResult('Test1')).toBeNull(); 164 | expect(target.hasClass('one')).toBe(false); 165 | }); 166 | 167 | it('should set the defaults for a filter', function(){ 168 | target.addBehaviorFilter('Defaults'); 169 | Behavior.addGlobalFilter('Defaults', { 170 | defaults: { 171 | foo: 'baz', 172 | number: 9 173 | }, 174 | setup: function(element, api){ 175 | expect(api.get('number')).toEqual(9); 176 | expect(api.get('foo')).toEqual('bar'); 177 | } 178 | }); 179 | behaviorInstance.apply(container); 180 | target.removeBehaviorFilter('Defaults'); 181 | }); 182 | 183 | it('should clone a filter', function(){ 184 | target.addBehaviorFilter('Base').addBehaviorFilter('Clone'); 185 | Behavior.addGlobalFilter('Base', { 186 | defaults: { 187 | foo: 'baz', 188 | number: 9 189 | }, 190 | setup: function(element, api){ 191 | if (api.prefix == 'base'){ 192 | expect(api.get('number')).toEqual(9); 193 | expect(api.get('foo')).toEqual('baz'); 194 | } 195 | if (api.prefix == 'clone'){ 196 | expect(api.get('number')).toEqual(10); 197 | expect(api.get('foo')).toEqual('biz'); 198 | } 199 | } 200 | }); 201 | 202 | Behavior.cloneFilter('Base', 'Clone', { 203 | foo: 'biz', 204 | number: 10 205 | }); 206 | 207 | behaviorInstance.apply(container); 208 | target.removeBehaviorFilter('Base').removeBehaviorFilter('Clone'); 209 | }); 210 | 211 | 212 | var makeRequirementTest = function(options){ 213 | return function(){ 214 | behaviorInstance.options.breakOnErrors = true; 215 | 216 | var filter = { 217 | setup: function(){ 218 | return new SimpleClass(); 219 | } 220 | }; 221 | 222 | if (options.require) filter.require = options.require; 223 | if (options.requireAs) filter.requireAs = options.requireAs; 224 | 225 | Behavior.addGlobalFilter('Require', filter, true); 226 | target.addBehaviorFilter('Require'); 227 | if (options.catcher) { 228 | try { 229 | behaviorInstance.apply(container, true); 230 | expect(true).toBe(false); //this shouldn't get this far as an error should be thrown 231 | } catch(e) { 232 | expect(e.message).toBe(options.catcher); 233 | } 234 | } else { 235 | behaviorInstance.apply(container, true); 236 | } 237 | if (options.truthy) expect(instanceOf(target.getBehaviorResult('Require'), SimpleClass)).toBeTruthy(); 238 | else expect(target.getBehaviorResult('Require')).toBeFalsy(); 239 | target.eliminate('Behavior Filter result:Require'); 240 | target.removeBehaviorFilter('Require'); 241 | 242 | behaviorInstance.options.breakOnErrors = false; 243 | }; 244 | }; 245 | 246 | it('should require a set of arguments on the target element', 247 | makeRequirementTest({ 248 | require: ['number', 'true', 'false'], 249 | truthy: true 250 | }) 251 | ); 252 | 253 | it('should require a set of arguments of a given type are present on the target element', 254 | makeRequirementTest({ 255 | requireAs: { 256 | 'number': Number, 257 | 'true': Boolean, 258 | 'false': Boolean 259 | }, 260 | truthy: true 261 | }) 262 | ); 263 | 264 | it('should fail if a required argument of a specific type is not found on an element', 265 | makeRequirementTest({ 266 | requireAs: { 267 | 'number': Number, 268 | 'true': Boolean, 269 | 'false': Boolean, 270 | 'nine': Array 271 | }, 272 | truthy: false, 273 | catcher: "Could not retrieve value \'nine\' as the specified type. Its value is: 9" 274 | }) 275 | ); 276 | 277 | it('should fail if a required argument is not found on an element', 278 | makeRequirementTest({ 279 | require: ['number', 'true', 'false', 'nope'], 280 | truthy: false, 281 | catcher: "Could not retrieve require-nope option from element." 282 | }) 283 | ); 284 | 285 | it('should apply all filters to a container\'s children', function(){ 286 | behaviorInstance.apply(container); 287 | expect(target.hasClass('one')).toBe(true); 288 | expect(target.hasClass('two')).toBe(true); 289 | }); 290 | 291 | it('should clean up all the filters applied to a container\'s children', function(){ 292 | behaviorInstance.cleanup(container); 293 | expect(target.hasClass('one')).toBe(false); 294 | expect(target.hasClass('two')).toBe(false); 295 | }); 296 | 297 | it('should overwrite a global filter with a local one', function(){ 298 | var test1 = ClassAdder.makeAdder('ONE'); 299 | behaviorInstance.addFilter('Test1', test1); 300 | expect(behaviorInstance.getFilter('Test1').setup).toBe(test1); 301 | behaviorInstance.apply(container); 302 | expect(target.hasClass('ONE')).toBe(true); 303 | behaviorInstance.cleanup(container); 304 | expect(target.hasClass('ONE')).toBe(false); 305 | runs(function(){ 306 | delete behaviorInstance._registered.Test1; 307 | }); 308 | }); 309 | 310 | it('should return the data filters on an element', function(){ 311 | expect(target.getBehaviors()).toEqual(['Test1', 'Test2']); 312 | }); 313 | 314 | it('should log an error when a filter that isn\'t defined is encountered', function(){ 315 | var logged = false, 316 | log = function(){ logged = true; }; 317 | behaviorInstance.addEvent('error', log); 318 | target.addBehaviorFilter('Test3'); 319 | behaviorInstance.apply(container); 320 | expect(logged).toBe(true); 321 | behaviorInstance.cleanup(container); 322 | behaviorInstance.removeEvent('error', log); 323 | target.removeBehaviorFilter('Test3'); 324 | }); 325 | 326 | it('should add a filter to an element', function(){ 327 | target.addBehaviorFilter('Test3'); 328 | expect(target.getBehaviors()).toEqual(['Test1', 'Test2', 'Test3']); 329 | target.removeBehaviorFilter('Test3'); 330 | }); 331 | 332 | it('should tell you if an element has a filter', function(){ 333 | target.addBehaviorFilter('Test3'); 334 | expect(target.hasBehavior('Test3')).toBe(true); 335 | target.removeBehaviorFilter('Test3'); 336 | }); 337 | 338 | it('should remove a data filter', function(){ 339 | target.addBehaviorFilter('Test3'); 340 | target.removeBehaviorFilter('Test3'); 341 | expect(target.hasBehavior('Test3')).toBe(false); 342 | }); 343 | 344 | it('should create a delayed filter', function(){ 345 | target.addBehaviorFilter('Delayed'); 346 | Behavior.addGlobalFilter('Delayed', { 347 | delay: 100, 348 | setup: function(element, API){ 349 | element.addClass('wasdelayed'); 350 | } 351 | }); 352 | behaviorInstance.apply(container); 353 | expect(target.hasClass('wasdelayed')).toBe(false); 354 | waits(200); 355 | runs(function(){ 356 | expect(target.hasClass('wasdelayed')).toBe(true); 357 | target.removeClass('wasdelayed'); 358 | target.removeBehaviorFilter('Delayed'); 359 | }); 360 | }); 361 | 362 | it('should create a filter that is run on mouseover', function(){ 363 | target.addBehaviorFilter('MouseOver'); 364 | var event; 365 | Behavior.addGlobalFilter('MouseOver', { 366 | delayUntil: 'mouseover', 367 | setup: function(element, API){ 368 | element.addClass('wasmousedover'); 369 | event = API.event; 370 | } 371 | }); 372 | behaviorInstance.apply(container); 373 | expect(target.hasClass('wasmousedover')).toBe(false); 374 | expect(event).toBeFalsy(); 375 | target.fireEvent('mouseover', true); 376 | expect(target.hasClass('wasmousedover')).toBe(true); 377 | expect(event).toBe(true); 378 | target.removeClass('wasmousedover'); 379 | target.removeBehaviorFilter('MouseOver'); 380 | }); 381 | 382 | it('should create a filter with a custom initializer', function(){ 383 | target.addBehaviorFilter('CustomInit'); 384 | Behavior.addGlobalFilter('CustomInit', { 385 | initializer: function(element, api){ 386 | var timer = (function(){ 387 | if (element.hasClass('custom_init')) { 388 | clearInterval(timer); 389 | api.runSetup(); 390 | } 391 | }).periodical(100); 392 | }, 393 | setup: function(element, API){ 394 | element.addClass('custom_init-ed'); 395 | } 396 | }); 397 | behaviorInstance.apply(container); 398 | expect(target.hasClass('custom_init-ed')).toBe(false); 399 | target.addClass('custom_init'); 400 | waits(200); 401 | runs(function(){ 402 | expect(target.hasClass('custom_init-ed')).toBe(true); 403 | target.removeClass('custom_init-ed'); 404 | target.removeClass('custom_init'); 405 | target.removeBehaviorFilter('CustomInit'); 406 | }); 407 | 408 | }); 409 | 410 | it('should pass a method to a filter via the API', function(){ 411 | var val = false; 412 | behaviorInstance.passMethod('changeVal', function(){ 413 | val = true; 414 | }); 415 | target.addBehaviorFilter('PassedMethod'); 416 | Behavior.addGlobalFilter('PassedMethod', function(el, api){ 417 | api.changeVal(); 418 | }); 419 | behaviorInstance.apply(container); 420 | expect(val).toBe(true); 421 | target.removeBehaviorFilter('PassedMethod'); 422 | }); 423 | 424 | it('should throw an error when attempting to pass a method that is already defined', function(){ 425 | try { 426 | behaviorInstance.passMethod('addEvent', function(){}); 427 | expect(true).toBe(false); //this shouldn't get this far as an error should be thrown 428 | } catch (e) { 429 | expect(e.message).toBe('Cannot overwrite API method addEvent as it already exists'); 430 | } 431 | }); 432 | 433 | it('should parse deprecated values', function(){ 434 | target.setData('some-thing', 'some value'); 435 | var depApi; 436 | behaviorInstance.addFilter('Deprecated', { 437 | deprecated: { 438 | 'test': 'some-thing' 439 | }, 440 | setup: function(element, api){ 441 | depAPI = api; 442 | } 443 | }); 444 | target.addBehaviorFilter('Deprecated'); 445 | behaviorInstance.apply(container); 446 | expect(depAPI.get('test')).toBe('some value'); 447 | behaviorInstance.options.enableDeprecation = false; 448 | behaviorInstance.apply(container, true); 449 | expect(depAPI.get('test')).toBe(undefined); 450 | behaviorInstance.options.enableDeprecation = true; 451 | target.removeBehaviorFilter('Deprecated'); 452 | }); 453 | 454 | it('should get a global filter', function(){ 455 | var global = function(){}; 456 | Behavior.addGlobalFilter('aGlobal', global); 457 | expect(Behavior.getFilter('aGlobal').setup).toBe(global); 458 | }); 459 | 460 | it('should overwrite the defaults for a filter', function(){ 461 | var before = { 462 | defaults: { 463 | letter: 'a', 464 | obj: { 465 | number: 1 466 | } 467 | } 468 | }; 469 | Behavior.addGlobalFilter('iHasDefaults', Object.clone(before)); 470 | Behavior.setFilterDefaults('iHasDefaults', { 471 | letter: 'b', 472 | obj: { 473 | number: 9 474 | } 475 | }); 476 | expect(Behavior.getFilter('iHasDefaults').config.defaults.letter).toBe('b'); 477 | expect(Behavior.getFilter('iHasDefaults').config.defaults.obj.number).toBe(9); 478 | behaviorInstance.addFilter('localDefaults', Object.clone(before)); 479 | behaviorInstance.setFilterDefaults('localDefaults', { 480 | letter: 'b', 481 | obj: { 482 | number: 9 483 | } 484 | }); 485 | expect(behaviorInstance.getFilter('localDefaults').config.defaults.letter).toBe('b'); 486 | expect(behaviorInstance.getFilter('localDefaults').config.defaults.obj.number).toBe(9); 487 | }); 488 | 489 | // plugins 490 | 491 | it('should define a global plugin', function(){ 492 | 493 | var newTest1 = function(element, API, filterResult) { 494 | //verify that BOTH filters (Test1 and Test2) have run before this plugin 495 | expect(element.hasClass('one')).toBe(true); 496 | expect(element.hasClass('two')).toBe(true); 497 | element.addClass(filterResult.className + '_plugin'); 498 | }; 499 | Behavior.addGlobalPlugin('Test1', 'Test1Plugin', newTest1); 500 | behaviorInstance.apply(container, true); 501 | expect(target.hasClass('one_plugin')).toBe(true); 502 | }); 503 | 504 | it('should get the specified element via the api key', function(){ 505 | target.addBehaviorFilter('testGetElements'); 506 | var firstSpan = new Element('span.foo#one').inject(target); 507 | var anotherSpan = new Element('span.foo#two').inject(target); 508 | 509 | target.setJSONData('testgetelements-options', { 510 | span: 'span', 511 | spanfoo: 'span.foo', 512 | self: 'self', 513 | win: 'window' 514 | }); 515 | 516 | var logged = false, 517 | log = function(){ logged = true; }; 518 | behaviorInstance.addEvent('error', log); 519 | Behavior.addGlobalFilter('testGetElements', function(element, api){ 520 | expect(api.getElement('span')).toEqual(firstSpan); 521 | expect(api.getElement('spanfoo')).toEqual(firstSpan); 522 | expect(api.getElements('span')[0]).toEqual(firstSpan); 523 | expect(api.getElements('span')[1]).toEqual(anotherSpan); 524 | expect(api.getElements('span').length).toEqual(2); 525 | expect(api.getElement('self')).toEqual(target); 526 | expect(api.getElements('self')[0]).toEqual(target); 527 | expect(api.getElements('self').length).toEqual(1); 528 | expect(api.getElement('win')).toEqual(window); 529 | expect(api.getElements('win')[0]).toEqual(window); 530 | expect(api.getElements('win').length).toEqual(1); 531 | }); 532 | behaviorInstance.apply(container); 533 | if (logged) expect("ERROR").toEqual("api.getElements failed; check console"); 534 | behaviorInstance.cleanup(container); 535 | target.removeBehaviorFilter('testGetElements'); 536 | }); 537 | 538 | }); 539 | 540 | })(); 541 | } 542 | -------------------------------------------------------------------------------- /Tests/Specs/Behavior/Behavior.SpecsHelpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Behavior.SpecsHelpers 4 | description: n/a 5 | requires: [Behavior/Behavior] 6 | provides: [Behavior.SpecsHelpers] 7 | ... 8 | */ 9 | 10 | //simple class that adds a class name and counts how many times it's been applied 11 | var ClassAdder = new Class({ 12 | initialize: function(element, className){ 13 | this.element = element.addClass(className); 14 | this.className = className; 15 | this.setCount(this.getCount() + 1); 16 | }, 17 | getCount: function(){ 18 | return (this.element.getData('class-adder-count-' + this.className) || 0).toInt(); 19 | }, 20 | setCount: function(count){ 21 | this.element.setData('class-adder-count-' + this.className, count); 22 | }, 23 | destroy: function(){ 24 | this.element.removeClass(this.className); 25 | } 26 | }); 27 | 28 | //a vanilla behavior instance 29 | var behaviorInstance = new Behavior({ 30 | onLog: function(){}, 31 | onError: function(){}, 32 | onWarn: function(){} 33 | }); 34 | //returns a Behavior.Filter setup function that creates a ClassAdder instance (see above) 35 | //for a specified CSS className 36 | ClassAdder.makeAdder = function(className){ 37 | return function(el, API){ 38 | var adder = new ClassAdder(el, className); 39 | API.markForCleanup(el, function(){ 40 | adder.destroy(); 41 | }); 42 | return adder; 43 | }; 44 | }; 45 | (function(){ 46 | //given a peice of content (either an HTML string or a DOM tree) 47 | //multiply it by a given number of times and return it 48 | var multiplyContent = function(content, times){ 49 | var combo; 50 | if (typeOf(content) == 'string'){ 51 | combo = content; 52 | (times - 1).times(function(){ 53 | combo += content; 54 | }); 55 | } else { 56 | combo = new Element('div'); 57 | times.times(function(){ 58 | combo.adopt(content.clone(true, true)); 59 | }); 60 | } 61 | return combo; 62 | }; 63 | 64 | if (window.MooBench){ 65 | /** 66 | defines a convenience method for adding benchmarks 67 | note that you should use Behavior.addFilterTest instead of this method directly 68 | options is an object with: 69 | desc - the name of the test as displayed on the screen 70 | content - an HTML string or a DOM tree to run behaviorInstance.apply against (will be injected into a common container DIV) 71 | */ 72 | 73 | MooBench.addBehaviorTest = function(options){ 74 | var name = options.desc, 75 | content = options.content; 76 | //content wrapper 77 | var tester, 78 | container = new Element('div'); 79 | if (typeOf(content) == 'string') container.set('html', content); 80 | else container.adopt(content); 81 | 82 | //cleans up any instances before each test cycle 83 | var clean = function(){ 84 | if (tester){ 85 | tester.destroy(); 86 | tester = null; 87 | } 88 | }; 89 | 90 | //add a benchmark for instantiating the widget 91 | MooBench.add(name + ': instantiation', function(){ behaviorInstance.apply(document.body); }, { 92 | // compiled/called before the test loop 93 | 'setup': function(){ 94 | tester = container.cloneNode(true); 95 | document.body.appendChild(tester); 96 | }, 97 | 98 | // compiled/called after the test loop 99 | 'teardown': function(){ 100 | behaviorInstance.cleanup(document.body); 101 | clean(); 102 | } 103 | }); 104 | 105 | //add a benchmark for destroying the widget 106 | MooBench.add(name + ': cleanup', function(){ behaviorInstance.cleanup(document.body); }, { 107 | // compiled/called before the test loop 108 | 'setup': function(){ 109 | tester = container.cloneNode(true); 110 | document.body.appendChild(tester); 111 | behaviorInstance.apply(document.body); 112 | }, 113 | 114 | // compiled/called after the test loop 115 | 'teardown': clean 116 | 117 | }); 118 | 119 | }; 120 | 121 | } 122 | /** 123 | The prefered method for adding unit tests and benchmarks for Behavior.Filters. This 124 | method will actually add both for you (unless you specify otherwise). 125 | Options object argument is: 126 | filterName: "Accordion", //the name of the filter as registered w/ Behavior 127 | desc: "Creates an Accordion with 20 sections." //a description of the test 128 | returns: Fx.Accordion, //a pointer to the class instantiated and returned; if nothing is returned, omit 129 | content: "
...
", //the HTML string or DOM tree to run behaviorInstance.apply() against 130 | //expects is a function passed the element filtered and the instance created 131 | //write any Jasmine style expectation string you like; this is run after the filter is applied 132 | expectation: function(element, instanceReturedByFilter){ expects(something).toBe(whatever); }, 133 | specs: true/false, //excludes from specs tests if false; optional 134 | benchmarks: true/false //excludes from benchmarks if false; optional 135 | */ 136 | 137 | Behavior.addFilterTest = function(options){ 138 | if (options.multiplier){ 139 | options.content = multiplyContent(options.content, options.multiplier); 140 | } 141 | //if we're in the benchmark suite, add a benchmark 142 | if (window.MooBench){ 143 | //unless benchmarks: false is specified 144 | if (options.benchmarks !== false) MooBench.addBehaviorTest(options); 145 | } else if (window.describe){ 146 | //else we're in specs; add spec test unless specs:false is specified 147 | if (options.specs !== false) Behavior.addSpecsTest(options); 148 | } 149 | }; 150 | 151 | //run any additional tests specified in the options 152 | /** 153 | options - the options object passed to addSpecsTest 154 | element - the element the filter was applied to 155 | instance - the widget instance returned by the filter (if any) 156 | */ 157 | var checkExpectations = function(options, element, instance){ 158 | if (options.expect) options.expect(element, instance); 159 | }; 160 | 161 | Behavior.addSpecsTest = function(options){ 162 | 163 | describe(options.desc, function(){ 164 | it('should run the ' + options.filterName + ' filter and return a result', function(){ 165 | //new instance of behavior for each specs test 166 | var behaviorInstance = new Behavior({ 167 | onLog: function(){}, 168 | onError: function(){}, 169 | onWarn: function(){} 170 | }); 171 | var tester, 172 | container = new Element('div').inject(document.body); 173 | //content wrapper 174 | if (typeOf(options.content) == 'string') container.set('html', options.content); 175 | else container.adopt(options.content); 176 | 177 | var created = false, 178 | filterReturned, filterElement; 179 | //a plugin to run after the filter 180 | var plugin = function(element, api, instance){ 181 | if (options.returns) created = instanceOf(instance, options.returns); 182 | else created = true; 183 | filterReturned = instance; 184 | filterElement = element; 185 | }; 186 | //add a plugin for the specified filterName 187 | behaviorInstance.addPlugin(options.filterName, options.filterName + ' test plugin', plugin); 188 | //apply the filters 189 | behaviorInstance.apply(container); 190 | //check to see if the filter was deferred; if it was, wait for it or invoke it 191 | var filter = behaviorInstance.getFilter(options.filterName); 192 | var checkCreated = function(){ 193 | expect(created).toBe(true); 194 | }; 195 | if (filter.config.delay){ 196 | waits(filter.config.delay + 50); 197 | runs(function(){ 198 | checkCreated(); 199 | checkExpectations(options, filterElement, filterReturned); 200 | runs(function(){ 201 | behaviorInstance.cleanup(container); 202 | container.dispose(); 203 | }); 204 | }); 205 | } else if (filter.config.delayUntil){ 206 | container.getElements('[data-behavior]').fireEvent(filter.config.delayUntil.split(',')[0], true); 207 | checkCreated(); 208 | checkExpectations(options, filterElement, filterReturned); 209 | runs(function(){ 210 | behaviorInstance.cleanup(container); 211 | container.dispose(); 212 | }); 213 | } else if (filter.config.initializer){ 214 | container.getElement('[data-behavior]').each(function(element){ 215 | if (element.hasBehavior(filter.name)){ 216 | behaviorInstance.applyFilter(element, filter); 217 | checkCreated(); 218 | checkExpectations(options, filterElement, filterReturned); 219 | runs(function(){ 220 | behaviorInstance.cleanup(container); 221 | container.dispose(); 222 | }); 223 | } 224 | }); 225 | } else { 226 | //not deffered 227 | checkCreated(); 228 | checkExpectations(options, filterElement, filterReturned); 229 | runs(function(){ 230 | behaviorInstance.cleanup(container); 231 | container.dispose(); 232 | }); 233 | } 234 | }); 235 | }); 236 | }; 237 | })(); 238 | -------------------------------------------------------------------------------- /Tests/Specs/Behavior/Behavior.Startup.Specs.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Behavior.Startup.Specs 4 | description: n/a 5 | requires: [Behavior/Behavior.Startup, Behavior-Tests/Behavior.SpecsHelpers] 6 | provides: [Behavior.Startup.Specs] 7 | ... 8 | */ 9 | if (window.describe){ 10 | 11 | describe('Behavior.Startup', function(){ 12 | 13 | var b = new Behavior({ 14 | onLog: function(){}, 15 | onError: function(){}, 16 | onWarn: function(){} 17 | }); 18 | var d = new Delegator({ 19 | getBehavior: function(){return b;} 20 | }); 21 | b.setDelegator(d); 22 | var test1count = 0, test2count = 0, test3count = 0, test4count = 0, test5count = 0, test6count = 0; 23 | Delegator.register('click', { 24 | StartupTest1: function(){ test1count++; }, 25 | StartupTest2: function(){ test2count++; }, 26 | StartupTest3: function(){ test3count++; }, 27 | StartupTest4: function(){ test4count++; }, 28 | StartupTest5: function(){ test5count++; }, 29 | StartupTest6: function(){ test6count++; } 30 | }); 31 | 32 | var dom = new Element('div').adopt( 33 | new Element('div', { 34 | 'data-trigger': 'StartupTest1, StartupTest2', 35 | 'data-behavior': 'Startup', 36 | 'data-startup-options': JSON.encode({ 37 | delegators: { 38 | // should fire 39 | StartupTest1: { 40 | target:'input#foo', 41 | property:'value', 42 | value:'bar' 43 | }, 44 | // should not fire 45 | StartupTest2: { 46 | target:'input#foo', 47 | property:'value', 48 | value:'baz' 49 | }, 50 | // should fire 51 | StartupTest3: { 52 | targets: 'span.yo', 53 | method:'hasClass', 54 | arguments: ['oy'], 55 | value: true 56 | }, 57 | // should not fire 58 | StartupTest4: { 59 | targets: 'span.yo', 60 | method:'get', 61 | arguments:'tag', 62 | value: 'div' 63 | }, 64 | // should fire 65 | StartupTest5: { 66 | 'span.yo::get': ['tag'], 67 | value: 'span' 68 | }, 69 | // should not fire 70 | StartupTest6: { 71 | 'span.yo::get': ['tag'], 72 | value: 'div' 73 | } 74 | } 75 | }) 76 | }) 77 | .adopt(new Element('input#foo', {name: 'foo', value: 'bar'})) 78 | .adopt(new Element('span.yo.oy')) 79 | ); 80 | 81 | b.apply(dom); 82 | 83 | it('Expects the proper startup delegators to have fired', function(){ 84 | expect(test1count).toBe(1); 85 | expect(test2count).toBe(0); 86 | expect(test3count).toBe(1); 87 | expect(test4count).toBe(0); 88 | expect(test5count).toBe(1); 89 | expect(test6count).toBe(0); 90 | }); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /Tests/Specs/Behavior/Behavior.Trigger.Specs.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Behavior.Trigger.Specs 4 | description: n/a 5 | requires: [Behavior/Behavior.Trigger, Behavior-Tests/Behavior.SpecsHelpers] 6 | provides: [Behavior.Trigger.Specs] 7 | ... 8 | */ 9 | if (window.describe){ 10 | 11 | describe('Behavior.Trigger', function(){ 12 | 13 | var b = new Behavior({ 14 | onLog: function(){}, 15 | onError: function(){}, 16 | onWarn: function(){} 17 | }); 18 | var d = new Delegator({ 19 | onLog: function(){}, 20 | onError: function(){}, 21 | onWarn: function(){}, 22 | getBehavior: function(){return b;} 23 | }); 24 | b.setDelegator(d); 25 | Delegator.register('click', { 26 | addClass: function(event, element, api){ 27 | element.addClass(api.get('class')); 28 | } 29 | }); 30 | 31 | var child = new Element('div.child'); 32 | 33 | var div = new Element('div.inner', { 34 | 'data-behavior': 'Trigger', 35 | 'data-trigger-options': 36 | JSON.encode({ 37 | 'triggers': [ 38 | { 39 | 'events': ['click'], //which events to monitor 40 | 'targets': { 41 | '> div': { //elements whose events we monitor 42 | 'self::addClass': { //selector for elements to invoke trigger :: trigger name 43 | 'class': 'success' //api options for trigger 44 | } 45 | } 46 | } 47 | } 48 | ] 49 | }) 50 | }).adopt(child); 51 | 52 | var dom = new Element('div').adopt(div); 53 | 54 | b.apply(dom); 55 | d.attach(dom); 56 | Syn.click({}, child); 57 | it('Expects the trigger to have invoked the delegator', function(){ 58 | expect(child.hasClass('success')).toBe(true); 59 | }); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /Tests/Specs/Behavior/BehaviorAPI.Specs.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: BehaviorAPI.Specs 4 | description: n/a 5 | requires: [Behavior/BehaviorAPI] 6 | provides: [BehaviorAPI.Specs] 7 | ... 8 | */ 9 | if (window.describe){ 10 | (function(){ 11 | var target = new Element('div', { 12 | 'data-behaviorname-options':'{"foo": "bar", "nine": 9, "arr": [1, 2, 3], "i-have-hyphens": "sweet"}', 13 | 'data-behaviorname-number': '0', 14 | 'data-behaviorname-true': 'true', 15 | 'data-behaviorname-false': 'false' 16 | }); 17 | 18 | describe('BehaviorAPI', function(){ 19 | 20 | it('should get a data properties from an element', function(){ 21 | var api = new BehaviorAPI(target, 'behaviorname'); 22 | expect(api.get('number')).toBe('0'); 23 | expect(api.get('number', 'true', 'false', 'iHaveHyphens')).toEqual({ 24 | 'number': '0', 'true': 'true', 'false': 'false', 'iHaveHyphens': 'sweet' 25 | }); 26 | expect(api.get('i-have-hyphens')).toBe('sweet'); 27 | }); 28 | 29 | it('should get a data property from an element as a number', function(){ 30 | var api = new BehaviorAPI(target, 'behaviorname'); 31 | expect(api.getAs(Number, 'number')).toBe(0); 32 | }); 33 | 34 | it('should get a data property from an element as a boolean', function(){ 35 | var api = new BehaviorAPI(target, 'behaviorname'); 36 | expect(api.getAs(Boolean, 'true')).toBe(true); 37 | expect(api.getAs(Boolean, 'false')).toBe(false); 38 | }); 39 | 40 | it('should not fail when using getAs on a property that isn\'t present', function(){ 41 | var api = new BehaviorAPI(target, 'behaviorname'); 42 | expect(api.getAs(Boolean, 'notHere')).toBe(undefined); 43 | }); 44 | 45 | it('should read JSON values', function(){ 46 | var api = new BehaviorAPI(target, 'behaviorname'); 47 | expect(api.get('foo')).toBe('bar'); 48 | expect(api.get('nine')).toBe(9); 49 | expect(api.get('arr')).toEqual([1,2,3]); 50 | 51 | 52 | var target2 = new Element('div', { 53 | 'data-behaviorname-options':'"foo": "bar", "nine": 9, "arr": [1, 2, 3]' 54 | }); 55 | var api2 = new BehaviorAPI(target2, 'behaviorname'); 56 | expect(api2.get('foo')).toBe('bar'); 57 | expect(api2.get('nine')).toBe(9); 58 | expect(api2.get('arr')).toEqual([1,2,3]); 59 | }); 60 | 61 | it('should set a default value', function(){ 62 | var api = new BehaviorAPI(target, 'behaviorname'); 63 | api.setDefault('foo', 'baz'); 64 | expect(api.get('foo')).toBe('bar'); 65 | 66 | api.setDefault('something', 'else'); 67 | expect(api.get('something')).toBe('else'); 68 | 69 | api.setDefault({ 70 | 'one': 1, 71 | 'two': 2 72 | }); 73 | expect(api.get('one')).toBe(1); 74 | expect(api.get('two')).toBe(2); 75 | }); 76 | 77 | it('should reset cached values', function(){ 78 | var clone = target.clone(true, true); 79 | var api = new BehaviorAPI(clone, 'behaviorname'); 80 | api.setDefault('fred', 'flintsone'); 81 | expect(api.get('number')).toBe('0'); 82 | clone.setData('behaviorname-number', '5'); 83 | expect(api.get('number')).toBe('0'); 84 | api.refreshAPI(); 85 | expect(api.get('number')).toBe('5'); 86 | }); 87 | 88 | 89 | it('should require an option that is present', function(){ 90 | var api = new BehaviorAPI(target, 'behaviorname'); 91 | api.require('number'); 92 | api.requireAs(Number, 'number'); 93 | 94 | api.require('number', 'true', 'false'); 95 | api.requireAs({ 96 | 'number': Number, 97 | 'true': Boolean, 98 | 'false': Boolean 99 | }); 100 | }); 101 | 102 | it('should require an option that is NOT present', function(){ 103 | var api = new BehaviorAPI(target, 'behaviorname'); 104 | try { 105 | api.require('notThere'); 106 | expect(true).toBe(false); //this shouldn't get this far as an error should be thrown 107 | } catch(e) { 108 | expect(e.message).toBe('Could not retrieve behaviorname-notThere option from element.'); 109 | } 110 | 111 | try { 112 | api.requireAs(Number, 'true'); 113 | expect(true).toBe(false); //this shouldn't get this far as an error should be thrown 114 | } catch(e) { 115 | expect(e.message).toBe('Could not retrieve value \'true\' as the specified type. Its value is: true'); 116 | } 117 | 118 | try { 119 | api.requireAs(Boolean, 'number'); 120 | expect(true).toBe(false); //this shouldn't get this far as an error should be thrown 121 | } catch(e) { 122 | expect(e.message).toBe('Could not retrieve value \'number\' as the specified type. Its value is: 0'); 123 | } 124 | 125 | try { 126 | api.requireAs({ 127 | 'true': Boolean, 128 | 'false': Boolean, 129 | 'number': Boolean 130 | }); 131 | } catch(e){ 132 | expect(e.message).toBe('Could not retrieve value \'number\' as the specified type. Its value is: 0'); 133 | } 134 | }); 135 | 136 | }); 137 | 138 | })(); 139 | } 140 | -------------------------------------------------------------------------------- /Tests/Specs/Behavior/Delegator.Specs.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Delegator.Specs 4 | description: n/a 5 | requires: [Behavior/Delegator, Behavior/Behavior, Core/DomReady] 6 | provides: [Delegator.Specs] 7 | ... 8 | */ 9 | (function(){ 10 | var container = new Element('div').inject(document.body); 11 | var target = new Element('a.some-class', { 12 | 'data-trigger': 'Test1 Test2', 13 | 'data-required-options': '"foo": "bar", "number": 9', 14 | 'data-required-true': 'true', 15 | 'data-reader-options': '"foo": "bar", "number": 9', 16 | 'data-reader-true': 'true' 17 | }) 18 | .adopt(new Element('span.foo.bar')) 19 | .inject(container); 20 | 21 | var test1count = 0; 22 | Delegator.register('click', { 23 | Test1: function(){ test1count++; }, 24 | Test2: function(){} 25 | }); 26 | var instance = new Delegator({ 27 | onLog: function(){}, 28 | onError: function(){}, 29 | onWarn: function(){} 30 | }).attach(container); 31 | 32 | describe('Delegator', function(){ 33 | 34 | it('should return the triggers on an element', function(){ 35 | expect(target.getTriggers()).toEqual(['Test1', 'Test2']); 36 | }); 37 | 38 | it('should add a trigger to an element', function(){ 39 | target.addTrigger('Test3'); 40 | expect(target.getTriggers()).toEqual(['Test1', 'Test2', 'Test3']); 41 | target.removeTrigger('Test3'); 42 | }); 43 | 44 | it('should tell you if an element has a trigger', function(){ 45 | target.addTrigger('Test3'); 46 | expect(target.hasTrigger('Test3')).toBe(true); 47 | target.removeTrigger('Test3'); 48 | }); 49 | 50 | it('should remove a trigger', function(){ 51 | target.addTrigger('Test3'); 52 | target.removeTrigger('Test3'); 53 | expect(target.hasTrigger('Test3')).toBe(false); 54 | }); 55 | 56 | it('should register a global trigger', function(){ 57 | var test3 = function(){}; 58 | Delegator.register('click', 'Test3', test3); 59 | expect(instance.getTrigger('Test3').handler).toBe(test3); 60 | expect(Delegator.getTrigger('Test3').handler).toBe(test3); 61 | }); 62 | 63 | it('should register a local trigger', function(){ 64 | var test3 = function(){}; 65 | instance.register('click', 'Test3', test3); 66 | expect(instance.getTrigger('Test3').handler).toBe(test3); 67 | expect(Delegator.getTrigger('Test3').handler).toNotBe(test3); 68 | }); 69 | 70 | it('should fail to overwrite a filter', function(){ 71 | var test3 = function(){}; 72 | try { 73 | instance.register('click', 'Test3', test3); 74 | expect(true).toBe(false); //should not get here 75 | } catch(e){ 76 | expect(e.message).toBe('Could add the trigger "Test3" as a previous trigger by that same name exists.'); 77 | } 78 | expect(instance.getTrigger('Test3').handler).toNotBe(test3); 79 | }); 80 | 81 | it('should overwrite a filter', function(){ 82 | var overwrite = function(){}; 83 | instance.register('click', 'Test2', overwrite, true); 84 | expect(instance.getTrigger('Test2').handler).toBe(overwrite); 85 | 86 | var test3 = function(){}; 87 | instance.register('click', { 88 | Test3: test3 89 | }, true); 90 | expect(instance.getTrigger('Test3').handler).toBe(test3); 91 | }); 92 | 93 | it('should bind to a behavior instance', function(){ 94 | var b = new Behavior({ 95 | onLog: function(){}, 96 | onError: function(){}, 97 | onWarn: function(){} 98 | }); 99 | var d = new Delegator({ 100 | onLog: function(){}, 101 | onError: function(){}, 102 | onWarn: function(){}, 103 | getBehavior: function(){ return b; } 104 | }); 105 | expect(d.getBehavior()).toBe(b); 106 | var b2 = new Behavior({ 107 | onLog: function(){}, 108 | onError: function(){}, 109 | onWarn: function(){} 110 | }); 111 | d.bindToBehavior(b2); 112 | expect(d.getBehavior()).toBe(b2); 113 | d.unbindFromBehavior(b2); 114 | expect(d.getBehavior()).toBeFalsy(); 115 | }); 116 | 117 | it('should return the value that the trigger returns', function(){ 118 | 119 | Delegator.register('click', { 120 | Test4: function(){ 121 | return 'test4'; 122 | }, 123 | Test5: function(){}, 124 | Test6: function(event, target, api){ 125 | return api.trigger('Test4'); 126 | } 127 | }); 128 | 129 | expect(instance.trigger('Test4')).toEqual('test4'); 130 | expect(instance.trigger('Test5')).toBe(undefined); 131 | expect(instance.trigger('Test6')).toEqual('test4'); 132 | 133 | }); 134 | 135 | // Only run this spec in browsers other than IE6-8 because they can't properly simulate bubbling events 136 | if (window.addEventListener){ 137 | 138 | it ('should set filter defaults', function(){ 139 | var fired = false; 140 | Delegator.register('click', 'Defaults', { 141 | defaults: { 142 | foo: 'bar', 143 | number: 9 144 | }, 145 | handler: function(event, target, api){ 146 | expect(api.get('foo')).toBe('baz'); 147 | expect(api.get('number')).toBe(10); 148 | fired = true; 149 | } 150 | }); 151 | Delegator.setTriggerDefaults('Defaults', { 152 | foo: 'baz', 153 | number: 10 154 | }); 155 | target.addTrigger('Defaults'); 156 | Syn.trigger('click', null, target); 157 | expect(fired).toEqual(true); 158 | target.removeTrigger('Defaults'); 159 | }); 160 | 161 | it ('should clone a filter', function(){ 162 | Delegator.register('click', 'Base', { 163 | defaults: { 164 | foo: 'bar', 165 | number: 9 166 | }, 167 | handler: function(event, target, api){ 168 | if (api.prefix == 'base'){ 169 | expect(api.get('foo')).toBe('bar'); 170 | expect(api.get('number')).toBe(9); 171 | } else { 172 | expect(api.get('foo')).toBe('baz'); 173 | expect(api.get('number')).toBe(10); 174 | } 175 | fired = true; 176 | } 177 | }); 178 | Delegator.cloneTrigger('Base', 'Clone', { 179 | foo: 'baz', 180 | number: 10 181 | }); 182 | target.addTrigger('Base').addTrigger('Clone'); 183 | Syn.trigger('click', null, target); 184 | expect(fired).toEqual(true); 185 | target.removeTrigger('Base').removeTrigger('Clone'); 186 | }); 187 | 188 | 189 | it('should capture a click and run a filter only once', function(){ 190 | var count = 0, 191 | test1current = test1count; 192 | // instance is already attached to the container 193 | // so this should be ignored 194 | instance.attach(container); 195 | instance.register('click', 'ClickTest', function(){ 196 | count++; 197 | }); 198 | target.addTrigger('ClickTest'); 199 | Syn.trigger('click', null, target); 200 | expect(count).toBe(1); 201 | expect(test1count).toBe(test1current + 1); 202 | target.removeTrigger('ClickTest'); 203 | }); 204 | 205 | it('should fire event for element', function(){ 206 | var testElement = new Element('a.some-class', { 207 | 'data-trigger': 'ElementTest' 208 | }); 209 | Delegator.register('madeUpEvent', { 210 | ElementTest: function(){ testElement.setData('success', true) } 211 | }); 212 | 213 | instance.fireEventForElement(testElement, 'madeUpEvent'); 214 | expect(testElement.getData('success')).toEqual('true'); 215 | }); 216 | 217 | 218 | it('should use BehaviorAPI to read element properties', function(){ 219 | var readerAPI; 220 | target.addTrigger('Reader'); 221 | instance.register('click', 'Reader', function(event, target, api){ 222 | readerAPI = api; 223 | }); 224 | Syn.trigger('click', null, target); 225 | expect(readerAPI.get('foo')).toBe('bar'); 226 | expect(readerAPI.getAs(Number, 'number')).toBe(9); 227 | expect(readerAPI.get('nope')).toBe(undefined); 228 | target.removeTrigger('Reader'); 229 | }); 230 | 231 | it('should define a trigger with required and default values', function(){ 232 | var reqAPI; 233 | target.addTrigger('Required'); 234 | instance.register('click', 'Required', { 235 | handler: function(event, target, api){ reqAPI = api; }, 236 | defaults: { 237 | 'foo': 'baz', 238 | 'ten': 10 239 | }, 240 | require: ['foo'], 241 | requireAs: { 242 | 'true': Boolean, 243 | 'number': Number 244 | } 245 | }); 246 | Syn.trigger('click', null, target); 247 | expect(reqAPI).toBeTruthy(); 248 | expect(reqAPI.get('foo')).toBe('bar'); 249 | expect(reqAPI.getAs(Number, 'ten')).toBe(10); 250 | target.removeTrigger('Required'); 251 | }); 252 | 253 | it('should not fail when breakOnErrors is false', function(){ 254 | target.addTrigger('Required'); 255 | var success, msg; 256 | instance.addEvent('error', function(){ 257 | msg = Array.join(arguments, ' '); 258 | }); 259 | instance.register('click', 'Required', { 260 | handler: function(event, target, api){ success = true; }, 261 | require: ['missing'] 262 | }, true); 263 | Syn.trigger('click', null, target); 264 | expect(success).toBeFalsy(); 265 | expect(msg).toBe('Could not apply the trigger Required Could not retrieve required-missing option from element.'); 266 | target.removeTrigger('Required'); 267 | }); 268 | 269 | it('should fail when breakOnErrors is true', function(){ 270 | target.addTrigger('Required'); 271 | instance.options.breakOnErrors = true; 272 | instance.register('click', 'Required', { 273 | handler: function(event, target, api){}, 274 | require: ['missing'] 275 | }, true); 276 | try{ 277 | instance.trigger('Required', target); 278 | expect(true).toBe(false); 279 | } catch(e){ 280 | expect(e.message).toBe('Could not retrieve required-missing option from element.'); 281 | } 282 | target.removeTrigger('Required'); 283 | }); 284 | 285 | it('should capture a click and ignore a filter that isn\'t named', function(){ 286 | var clicked; 287 | instance.register('click', 'Ignored', function(){ 288 | clicked = true; 289 | }); 290 | Syn.trigger('click', null, target); 291 | expect(clicked).toBe(undefined); 292 | }); 293 | 294 | // Hmmm. For some reason Syn.trigger breaks this test, but I have to use it with the latest 295 | // test runner... commenting it out for now. Anecdotally the thing this test tests does, in fact 296 | // work... 297 | // it('should detach from a previously attached container and re-attach to it', function(){ 298 | // instance.detach(container); 299 | // var test1current = test1count; 300 | // Syn.trigger('click', null, target); 301 | // expect(test1count).toBe(test1current); 302 | // instance.attach(container); 303 | // Syn.trigger('click', null, target); 304 | // expect(test1count).toBe(test1current + 1); 305 | // }); 306 | 307 | 308 | it('should obey conditionals', function(){ 309 | var test7count = 0, test8count = 0, test9count = 0, test10count = 0, test11count = 0; 310 | instance.register('click', { 311 | Test7: function(){ test7count++; }, 312 | Test8: function(){ test8count++; }, 313 | Test9: function(){ test9count++; }, 314 | Test10: function(){ test10count++; }, 315 | Test11: function(){ test11count++; } 316 | }); 317 | target.set({ 318 | // should fire 319 | 'data-test7-options': JSON.encode({ 320 | 'if': { 321 | 'self::hasClass': ['some-class'] 322 | } 323 | }), 324 | // should not fire 325 | 'data-test8-options': JSON.encode({ 326 | 'unless': { 327 | 'self::hasClass': ['some-class'] 328 | } 329 | }), 330 | // should fire 331 | 'data-test9-options': JSON.encode({ 332 | 'if': { 333 | 'target': 'span.foo', 334 | 'method': 'hasClass', 335 | 'arguments': ['bar'] 336 | } 337 | }), 338 | // should not fire 339 | 'data-test10-options': JSON.encode({ 340 | 'if': { 341 | 'target': 'span.foo', 342 | 'method': 'get', 343 | 'arguments': ['tag'], 344 | 'value': 'div' 345 | } 346 | }), 347 | // should fire 348 | 'data-test11-options': JSON.encode({ 349 | 'unless': { 350 | 'target': 'span.foo', 351 | 'method': 'get', 352 | 'arguments': ['tag'], 353 | 'value': 'div' 354 | } 355 | }) 356 | }); 357 | target.addTrigger('Test7') 358 | .addTrigger('Test8') 359 | .addTrigger('Test9') 360 | .addTrigger('Test10') 361 | .addTrigger('Test11'); 362 | Syn.trigger('click', null, target); 363 | expect(test7count).toEqual(1); 364 | expect(test8count).toEqual(0); 365 | expect(test9count).toEqual(1); 366 | expect(test10count).toEqual(0); 367 | expect(test11count).toEqual(1); 368 | }); 369 | 370 | it('should handle multi-triggers', function(){ 371 | 372 | var foo = new Element('div.foo'); 373 | var bar = new Element('div.bar'); 374 | 375 | var multiTester = new Element('a', { 376 | 'data-trigger': 'multi', 377 | 'data-multi-triggers': JSON.encode( 378 | [ 379 | { 380 | '.foo::multi1': { 381 | 'arg':'blah' 382 | } 383 | }, 384 | '.bar::multi2', 385 | { 386 | '.foo::multi3': { 387 | 'if':{ 388 | 'self::hasClass':'foo' 389 | } 390 | } 391 | }, 392 | { 393 | '.foo::multi4': { 394 | 'unless':{ 395 | 'self::hasClass':'foo' 396 | } 397 | } 398 | } 399 | ] 400 | ) 401 | }) 402 | .adopt(foo) 403 | .adopt(bar) 404 | .inject(container); 405 | 406 | var multi1 = 0, multi2 = 0, multi3 = 0, multi4 = 0; 407 | instance.register('click', { 408 | multi1: function(event, el, api){ 409 | expect(api.get('arg')).toEqual('blah'); 410 | expect(el).toEqual(foo); 411 | multi1++; 412 | }, 413 | multi2: function(event, el, api){ 414 | expect(el).toEqual(bar); 415 | multi2++; 416 | }, 417 | multi3: function(event, el, api){ 418 | expect(el).toEqual(foo); 419 | multi3++; 420 | }, 421 | // shouldn't get called 422 | multi4: function(){ 423 | multi4++; 424 | } 425 | }); 426 | 427 | Syn.trigger('click', null, multiTester); 428 | expect(multi1).toEqual(1); 429 | expect(multi2).toEqual(1); 430 | expect(multi3).toEqual(1); 431 | expect(multi4).toEqual(0); 432 | 433 | instance.trigger('multi', multiTester); 434 | expect(multi1).toEqual(2); 435 | expect(multi2).toEqual(2); 436 | expect(multi3).toEqual(2); 437 | expect(multi4).toEqual(0); 438 | 439 | }); 440 | 441 | 442 | it('handle multi-trigger switches', function(){ 443 | 444 | var foo = new Element('div.foo'); 445 | var bar = new Element('div.bar'); 446 | 447 | var multiTester = new Element('a', { 448 | 'data-trigger': 'first any', 449 | 'data-first-switches': JSON.encode([ 450 | // should NOT fire 451 | { 452 | 'if': { 453 | 'div.foo::hasClass':['baz'] 454 | }, 455 | triggers: [ 456 | '.foo::switch1' 457 | ] 458 | }, 459 | // should fire 460 | { 461 | 'unless': { 462 | 'div.foo::hasClass':['baz'] 463 | }, 464 | triggers: [ 465 | '.foo::switch2' 466 | ] 467 | }, 468 | // should NOT fire 469 | { 470 | triggers: [ 471 | '.foo::switch3' 472 | ] 473 | } 474 | ]), 475 | 'data-any-switches': JSON.encode([ 476 | // should NOT fire 477 | { 478 | 'if': { 479 | 'div.foo::hasClass':['baz'] 480 | }, 481 | triggers: [ 482 | '.foo::switch4' 483 | ] 484 | }, 485 | // should fire 486 | { 487 | 'unless': { 488 | 'div.foo::hasClass':['baz'] 489 | }, 490 | triggers: [ 491 | '.foo::switch5' 492 | ] 493 | }, 494 | // should fire; no conditional 495 | { 496 | triggers: [ 497 | '.foo::switch6' 498 | ] 499 | } 500 | ]) 501 | }) 502 | .adopt(foo) 503 | .adopt(bar) 504 | .inject(container); 505 | 506 | var switch1 = 0, switch2 = 0, switch3 = 0, switch4 = 0, switch5 = 0, switch6 = 0; 507 | instance.register('click', { 508 | switch1: function(event, el, api){ 509 | // shouldn't fire 510 | switch1++; 511 | }, 512 | switch2: function(event, el, api){ 513 | expect(el).toEqual(foo); 514 | switch2++; 515 | }, 516 | switch3: function(event, el, api){ 517 | // shouldn't fire 518 | switch3++; 519 | }, 520 | switch4: function(event, el, api){ 521 | // shouldn't fire 522 | switch4++; 523 | }, 524 | switch5: function(event, el, api){ 525 | expect(el).toEqual(foo); 526 | switch5++; 527 | }, 528 | switch6: function(event, el, api){ 529 | expect(el).toEqual(foo); 530 | switch6++; 531 | } 532 | }); 533 | 534 | Syn.trigger('click', null, multiTester); 535 | expect(switch1).toEqual(0); 536 | expect(switch2).toEqual(1); 537 | expect(switch3).toEqual(0); 538 | expect(switch4).toEqual(0); 539 | expect(switch5).toEqual(1); 540 | expect(switch6).toEqual(1); 541 | 542 | instance.trigger('first', multiTester); 543 | instance.trigger('any', multiTester); 544 | expect(switch1).toEqual(0); 545 | expect(switch2).toEqual(2); 546 | expect(switch3).toEqual(0); 547 | expect(switch4).toEqual(0); 548 | expect(switch5).toEqual(2); 549 | expect(switch6).toEqual(2); 550 | 551 | }); 552 | 553 | } 554 | 555 | }); 556 | 557 | })(); 558 | -------------------------------------------------------------------------------- /Tests/Specs/Behavior/Element.Data.Specs.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: Element.Data.Specs 4 | description: n/a 5 | requires: [Behavior/Element.Data] 6 | provides: [Element.Data.Specs] 7 | ... 8 | */ 9 | 10 | (function(){ 11 | var target = new Element('div', { 12 | 'data-behavior': 'Test1 Test2', 13 | 'data-i-have-hyphens': 'sweet', 14 | 'data-json':'{"foo": "bar", "nine": 9, "arr": [1, 2, 3]}' 15 | }); 16 | 17 | 18 | describe('Element.Data', function(){ 19 | 20 | it('should get a data property from an element', function(){ 21 | expect(target.getData('behavior')).toBe('Test1 Test2'); 22 | }); 23 | 24 | it('should get a data property from an element using hyphens or camelcase', function(){ 25 | expect(target.getData('i-have-hyphens')).toBe('sweet'); 26 | expect(target.getData('iHaveHyphens')).toBe('sweet'); 27 | }); 28 | 29 | 30 | it('should set a data property on an element', function(){ 31 | target.setData('foo', 'bar'); 32 | expect(target.getData('foo')).toBe('bar'); 33 | }); 34 | 35 | it('should read a property as JSON', function(){ 36 | var json = target.getJSONData('json'); 37 | expect(json.foo).toBe('bar'); 38 | expect(json.nine).toBe(9); 39 | expect(json.arr).toEqual([1,2,3]); 40 | }); 41 | 42 | it('should set a property as JSON', function(){ 43 | target.setJSONData('json2', { 44 | foo: 'bar', nine: 9, arr: [1,2,3] 45 | }); 46 | var json = target.getJSONData('json2'); 47 | expect(json.foo).toBe('bar'); 48 | expect(json.nine).toBe(9); 49 | expect(json.arr).toEqual([1,2,3]); 50 | }); 51 | 52 | it('should return undefined for a non-defined property', function(){ 53 | expect(target.getData('baz')).toBeUndefined(); 54 | }); 55 | 56 | }); 57 | 58 | })(); 59 | -------------------------------------------------------------------------------- /Tests/Specs/Benchmarks.js: -------------------------------------------------------------------------------- 1 | // Put this file in the parent directory of the runner folder. Also rename the file to Configuration.js 2 | 3 | (function(context){ 4 | 5 | var Configuration = context.Configuration = {}; 6 | 7 | // Runner name 8 | Configuration.name = 'Behavior'; 9 | 10 | 11 | // Presets - combine the sets and the source to a preset to easily run a test 12 | Configuration.presets = { 13 | 14 | 'Behavior': { 15 | sets: ['Behavior'], 16 | source: ['Behavior'] 17 | } 18 | 19 | }; 20 | 21 | // An object with default presets 22 | Configuration.defaultPresets = { 23 | browser: 'Behavior', 24 | nodejs: 'Behavior', 25 | jstd: 'Behavior' 26 | }; 27 | 28 | 29 | /* 30 | * An object with sets. Each item in the object should have an path key', ' 31 | * that specifies where the spec files are and an array with all the files 32 | * without the .js extension relative to the given path 33 | */ 34 | Configuration.sets = { 35 | 36 | 'Behavior': { 37 | path: 'Behavior/', 38 | files: ['Behavior.SpecsHelpers', 'Behavior.Benchmarks'] 39 | } 40 | 41 | }; 42 | 43 | 44 | /* 45 | * An object with the source files. Each item should have an path key, 46 | * that specifies where the source files are and an array with all the files 47 | * without the .js extension relative to the given path 48 | */ 49 | 50 | Configuration.source = { 51 | 52 | 'Behavior': { 53 | path: '', 54 | files: [ 55 | 'mootools-core/Source/Core/Core', 56 | 'mootools-core/Source/Types/Array', 57 | 'mootools-core/Source/Types/String', 58 | 'mootools-core/Source/Types/Function', 59 | 'mootools-core/Source/Types/Number', 60 | 'mootools-core/Source/Types/Object', 61 | 'mootools-core/Source/Class/Class', 62 | 'mootools-core/Source/Class/Class.Extras', 63 | 'mootools-core/Source/Browser/Browser', 64 | 'mootools-core/Source/Slick/Slick.Parser', 65 | 'mootools-core/Source/Slick/Slick.Finder', 66 | 'mootools-core/Source/Element/Element', 67 | 'mootools-core/Source/Element/Element.Event', 68 | 'mootools-core/Source/Element/Element.Dimensions', 69 | 'mootools-core/Source/Utilities/JSON', 70 | 'mootools-more/Source/Core/More', 71 | 'mootools-more/Source/Utilities/Table', 72 | '../Source/Behavior', 73 | '../Source/BehaviorAPI', 74 | '../Source/Element.Data' 75 | ] 76 | } 77 | 78 | }; 79 | 80 | })(typeof exports != 'undefined' ? exports : this); 81 | -------------------------------------------------------------------------------- /Tests/Specs/Configuration.js: -------------------------------------------------------------------------------- 1 | // Put this file in the parent directory of the runner folder. Also rename the file to Configuration.js 2 | 3 | (function(context){ 4 | 5 | var Configuration = context.Configuration = {}; 6 | 7 | // Runner name 8 | Configuration.name = 'Behavior'; 9 | 10 | 11 | // Presets - combine the sets and the source to a preset to easily run a test 12 | Configuration.presets = { 13 | 14 | 'Behavior': { 15 | sets: ['Behavior'], 16 | source: ['Behavior'] 17 | } 18 | 19 | }; 20 | 21 | // An object with default presets 22 | Configuration.defaultPresets = { 23 | browser: 'Behavior', 24 | nodejs: 'Behavior', 25 | jstd: 'Behavior' 26 | }; 27 | 28 | 29 | /* 30 | * An object with sets. Each item in the object should have an path key', ' 31 | * that specifies where the spec files are and an array with all the files 32 | * without the .js extension relative to the given path 33 | */ 34 | Configuration.sets = { 35 | 36 | 'Behavior': { 37 | path: 'Behavior/', 38 | files: [ 39 | 'Behavior.SpecsHelpers', 40 | 'Behavior.Specs', 41 | 'Element.Data.Specs', 42 | 'BehaviorAPI.Specs' 43 | ] 44 | } 45 | 46 | }; 47 | 48 | 49 | /* 50 | * An object with the source files. Each item should have an path key, 51 | * that specifies where the source files are and an array with all the files 52 | * without the .js extension relative to the given path 53 | */ 54 | 55 | Configuration.source = { 56 | 57 | 'Behavior': { 58 | path: '', 59 | files: [ 60 | 'mootools-core/Source/Core/Core', 61 | 'mootools-core/Source/Types/Array', 62 | 'mootools-core/Source/Types/String', 63 | 'mootools-core/Source/Types/Function', 64 | 'mootools-core/Source/Types/Number', 65 | 'mootools-core/Source/Types/Object', 66 | 'mootools-core/Source/Class/Class', 67 | 'mootools-core/Source/Class/Class.Extras', 68 | 'mootools-core/Source/Browser/Browser', 69 | 'mootools-core/Source/Slick/Slick.Parser', 70 | 'mootools-core/Source/Slick/Slick.Finder', 71 | 'mootools-core/Source/Element/Element', 72 | 'mootools-core/Source/Element/Element.Event', 73 | 'mootools-core/Source/Element/Element.Dimensions', 74 | 'mootools-core/Source/Utilities/JSON', 75 | 'mootools-more/Source/Core/More', 76 | 'mootools-more/Source/Utilities/Table', 77 | '../Source/Behavior', 78 | '../Source/Element.Data', 79 | '../Source/BehaviorAPI' 80 | ] 81 | } 82 | 83 | }; 84 | 85 | })(typeof exports != 'undefined' ? exports : this); 86 | -------------------------------------------------------------------------------- /Tests/Specs/package.yml: -------------------------------------------------------------------------------- 1 | name: "Behavior-Tests" 2 | 3 | web: "[clientcide.com](http://clientcide.com)" 4 | 5 | description: "Behavior filters for MooTools More" 6 | 7 | license: "MIT License" 8 | 9 | copyright: "© [Aaron Newton](http://clientcide.com)" 10 | 11 | authors: "[Aaron Newton](http://clientcide.com)" 12 | 13 | sources: 14 | - "Behavior/BehaviorAPI.Specs.js" 15 | - "Behavior/Behavior.Benchmarks.js" 16 | - "Behavior/Behavior.Specs.js" 17 | - "Behavior/Behavior.SpecsHelpers.js" 18 | - "Behavior/Behavior.Trigger.Specs.js" 19 | - "Behavior/Delegator.Specs.js" 20 | - "Behavior/Element.Data.Specs.js" 21 | - "Behavior/Behavior.Events.Specs.js" 22 | - "Behavior/Behavior.Startup.Specs.js" 23 | -------------------------------------------------------------------------------- /Tests/gruntfile-options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var gruntOptions = { 4 | testserver: { 5 | options: { 6 | // We use end2end task (which does not start the webserver) 7 | // and start the webserver as a separate process 8 | // to avoid https://github.com/joyent/libuv/issues/826 9 | port: 8000, 10 | hostname: '0.0.0.0', 11 | middleware: function(connect, options){ 12 | return [ 13 | function(req, resp, next){ 14 | // cache get requests to speed up tests on travis 15 | if (req.method === 'GET'){ 16 | resp.setHeader('Cache-control', 'public, max-age=3600'); 17 | } 18 | next(); 19 | }, 20 | connect.static(options.base)]; 21 | } 22 | } 23 | } 24 | } 25 | 26 | var karmaOptions = { 27 | captureTimeout: 60000 * 2, 28 | singleRun: true, 29 | frameworks: ['jasmine'/*, 'sinon'*/], 30 | files: [ 31 | 'behavior.js', 32 | 'Tests/Specs/Behavior/Behavior.SpecsHelpers.js', 33 | 'Tests/Specs/Behavior/Behavior.Benchmarks.js', 34 | 'Tests/Specs/Syn.js', 35 | 'behavior-specs.js' 36 | ], 37 | reporters: ['progress'], 38 | } 39 | 40 | exports.grunt = gruntOptions; 41 | exports.karma = karmaOptions; 42 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "behavior", 3 | "dependencies": { 4 | "mootools-core": "https://github.com/mootools/mootools-core.git#1.5.2", 5 | "mootools-more": "https://github.com/mootools/mootools-more.git#a007edb07e7d776a1e31f06d8df15abcb04af69f" 6 | }, 7 | "ignore": [ 8 | "Tests/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anutron/behavior/9b7af63afd8b2250f965b6087f5aa0e64189f376/layers.png -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Aaron Newton, 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "behavior", 3 | "version": "1.5.9", 4 | "description": "Auto-instantiat widgets/classes based on parsed, declarative HTML with MooTools.", 5 | "main": "behavior.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/anutron/behavior.git" 9 | }, 10 | "keywords": [ 11 | "mootools", 12 | "behavior", 13 | "ui" 14 | ], 15 | "author": "Aaron Newton", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/anutron/behavior/issues" 19 | }, 20 | "homepage": "http://http://www.behaviorui.com/", 21 | "devDependencies": { 22 | "mootools-core": "https://github.com/mootools/mootools-core/tarball/master", 23 | "mootools-more": "https://github.com/mootools/mootools-more/tarball/master", 24 | "grunt": "~0.4.2", 25 | "grunt-cli": "~0.1.13", 26 | "grunt-mootools-packager": "~0.3.0", 27 | "grunt-contrib-clean": "~0.5.0", 28 | "grunt-contrib-connect": "~0.7.0", 29 | "load-grunt-tasks": "~0.4.0", 30 | "grunt-karma": "~0.8.0", 31 | "karma": "~0.12.0", 32 | "karma-jasmine": "~0.1.5", 33 | "karma-phantomjs-launcher": "~0.1.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.yml: -------------------------------------------------------------------------------- 1 | name: "Behavior" 2 | 3 | web: "[clientcide.com](http://clientcide.com)" 4 | 5 | description: "Auto-instantiat widgets/classes based on parsed, declarative HTML with MooTools." 6 | 7 | license: "MIT License" 8 | 9 | copyright: "© [Aaron Newton](http://clientcide.com)" 10 | 11 | authors: "[Aaron Newton](http://clientcide.com)" 12 | 13 | version: "1.4.0" 14 | 15 | sources: 16 | - "Source/Delegator.js" 17 | - "Source/Element.Data.js" 18 | - "Source/Event.Mock.js" 19 | - "Source/Behavior.js" 20 | - "Source/BehaviorAPI.js" 21 | - "Source/Behavior.Events.js" 22 | - "Source/Behavior.Startup.js" 23 | - "Source/Behavior.Trigger.js" 24 | --------------------------------------------------------------------------------