|
| 1 | +# Public: Delegator is the base class that all of Annotators objects inherit |
| 2 | +# from. It provides basic functionality such as instance options, event |
| 3 | +# delegation and pub/sub methods. |
| 4 | +class Delegator |
| 5 | + # Public: Events object. This contains a key/pair hash of events/methods that |
| 6 | + # should be bound. See Delegator#addEvents() for usage. |
| 7 | + events: {} |
| 8 | + |
| 9 | + # Public: Options object. Extended on initialisation. |
| 10 | + options: {} |
| 11 | + |
| 12 | + # A jQuery object wrapping the DOM Element provided on initialisation. |
| 13 | + element: null |
| 14 | + |
| 15 | + # Public: Constructor function that sets up the instance. Binds the @events |
| 16 | + # hash and extends the @options object. |
| 17 | + # |
| 18 | + # element - The DOM element that this intance represents. |
| 19 | + # options - An Object literal of options. |
| 20 | + # |
| 21 | + # Examples |
| 22 | + # |
| 23 | + # element = document.getElementById('my-element') |
| 24 | + # instance = new Delegator(element, { |
| 25 | + # option: 'my-option' |
| 26 | + # }) |
| 27 | + # |
| 28 | + # Returns a new instance of Delegator. |
| 29 | + constructor: (element, options) -> |
| 30 | + @options = $.extend(true, {}, @options, options) |
| 31 | + @element = $(element) |
| 32 | + |
| 33 | + # Delegator creates closures for each event it binds. This is a private |
| 34 | + # registry of created closures, used to enable event unbinding. |
| 35 | + @_closures = {} |
| 36 | + |
| 37 | + this.on = this.subscribe |
| 38 | + this.addEvents() |
| 39 | + |
| 40 | + # Public: Destroy the instance, unbinding all events. |
| 41 | + # |
| 42 | + # Returns nothing. |
| 43 | + destroy: -> |
| 44 | + this.removeEvents() |
| 45 | + |
| 46 | + # Public: binds the function names in the @events Object to their events. |
| 47 | + # |
| 48 | + # The @events Object should be a set of key/value pairs where the key is the |
| 49 | + # event name with optional CSS selector. The value should be a String method |
| 50 | + # name on the current class. |
| 51 | + # |
| 52 | + # This is called by the default Delegator constructor and so shouldn't usually |
| 53 | + # need to be called by the user. |
| 54 | + # |
| 55 | + # Examples |
| 56 | + # |
| 57 | + # # This will bind the clickedElement() method to the click event on @element. |
| 58 | + # @options = {"click": "clickedElement"} |
| 59 | + # |
| 60 | + # # This will delegate the submitForm() method to the submit event on the |
| 61 | + # # form within the @element. |
| 62 | + # @options = {"form submit": "submitForm"} |
| 63 | + # |
| 64 | + # # This will bind the updateAnnotationStore() method to the custom |
| 65 | + # # annotation:save event. NOTE: Because this is a custom event the |
| 66 | + # # Delegator#subscribe() method will be used and updateAnnotationStore() |
| 67 | + # # will not recieve an event parameter like the previous two examples. |
| 68 | + # @options = {"annotation:save": "updateAnnotationStore"} |
| 69 | + # |
| 70 | + # Returns nothing. |
| 71 | + addEvents: -> |
| 72 | + for event in Delegator._parseEvents(@events) |
| 73 | + this._addEvent event.selector, event.event, event.functionName |
| 74 | + |
| 75 | + # Public: unbinds functions previously bound to events by addEvents(). |
| 76 | + # |
| 77 | + # The @events Object should be a set of key/value pairs where the key is the |
| 78 | + # event name with optional CSS selector. The value should be a String method |
| 79 | + # name on the current class. |
| 80 | + # |
| 81 | + # Returns nothing. |
| 82 | + removeEvents: -> |
| 83 | + for event in Delegator._parseEvents(@events) |
| 84 | + this._removeEvent event.selector, event.event, event.functionName |
| 85 | + |
| 86 | + # Binds an event to a callback function represented by a String. A selector |
| 87 | + # can be provided in order to watch for events on a child element. |
| 88 | + # |
| 89 | + # The event can be any standard event supported by jQuery or a custom String. |
| 90 | + # If a custom string is used the callback function will not recieve an |
| 91 | + # event object as it's first parameter. |
| 92 | + # |
| 93 | + # selector - Selector String matching child elements. (default: '') |
| 94 | + # event - The event to listen for. |
| 95 | + # functionName - A String function name to bind to the event. |
| 96 | + # |
| 97 | + # Examples |
| 98 | + # |
| 99 | + # # Listens for all click events on instance.element. |
| 100 | + # instance._addEvent('', 'click', 'onClick') |
| 101 | + # |
| 102 | + # # Delegates the instance.onInputFocus() method to focus events on all |
| 103 | + # # form inputs within instance.element. |
| 104 | + # instance._addEvent('form :input', 'focus', 'onInputFocus') |
| 105 | + # |
| 106 | + # Returns itself. |
| 107 | + _addEvent: (selector, event, functionName) -> |
| 108 | + closure = => this[functionName].apply(this, arguments) |
| 109 | + |
| 110 | + if selector == '' and Delegator._isCustomEvent(event) |
| 111 | + this.subscribe(event, closure) |
| 112 | + else |
| 113 | + @element.delegate(selector, event, closure) |
| 114 | + |
| 115 | + @_closures["#{selector}/#{event}/#{functionName}"] = closure |
| 116 | + |
| 117 | + this |
| 118 | + |
| 119 | + # Unbinds a function previously bound to an event by the _addEvent method. |
| 120 | + # |
| 121 | + # Takes the same arguments as _addEvent(), and an event will only be |
| 122 | + # successfully unbound if the arguments to removeEvent() are exactly the same |
| 123 | + # as the original arguments to _addEvent(). This would usually be called by |
| 124 | + # _removeEvents(). |
| 125 | + # |
| 126 | + # selector - Selector String matching child elements. (default: '') |
| 127 | + # event - The event to listen for. |
| 128 | + # functionName - A String function name to bind to the event. |
| 129 | + # |
| 130 | + # Returns itself. |
| 131 | + _removeEvent: (selector, event, functionName) -> |
| 132 | + closure = @_closures["#{selector}/#{event}/#{functionName}"] |
| 133 | + |
| 134 | + if selector == '' and Delegator._isCustomEvent(event) |
| 135 | + this.unsubscribe(event, closure) |
| 136 | + else |
| 137 | + @element.undelegate(selector, event, closure) |
| 138 | + |
| 139 | + delete @_closures["#{selector}/#{event}/#{functionName}"] |
| 140 | + |
| 141 | + this |
| 142 | + |
| 143 | + |
| 144 | + # Public: Fires an event and calls all subscribed callbacks with any parameters |
| 145 | + # provided. This is essentially an alias of @element.triggerHandler() but |
| 146 | + # should be used to fire custom events. |
| 147 | + # |
| 148 | + # NOTE: Events fired using .publish() will not bubble up the DOM. |
| 149 | + # |
| 150 | + # event - A String event name. |
| 151 | + # params - An Array of parameters to provide to callbacks. |
| 152 | + # |
| 153 | + # Examples |
| 154 | + # |
| 155 | + # instance.subscribe('annotation:save', (msg) -> console.log(msg)) |
| 156 | + # instance.publish('annotation:save', ['Hello World']) |
| 157 | + # # => Outputs "Hello World" |
| 158 | + # |
| 159 | + # Returns itself. |
| 160 | + publish: () -> |
| 161 | + @element.triggerHandler.apply @element, arguments |
| 162 | + this |
| 163 | + |
| 164 | + # Public: Listens for custom event which when published will call the provided |
| 165 | + # callback. This is essentially a wrapper around @element.bind() but removes |
| 166 | + # the event parameter that jQuery event callbacks always recieve. These |
| 167 | + # parameters are unnessecary for custom events. |
| 168 | + # |
| 169 | + # event - A String event name. |
| 170 | + # callback - A callback function called when the event is published. |
| 171 | + # |
| 172 | + # Examples |
| 173 | + # |
| 174 | + # instance.subscribe('annotation:save', (msg) -> console.log(msg)) |
| 175 | + # instance.publish('annotation:save', ['Hello World']) |
| 176 | + # # => Outputs "Hello World" |
| 177 | + # |
| 178 | + # Returns itself. |
| 179 | + subscribe: (event, callback) -> |
| 180 | + closure = -> callback.apply(this, [].slice.call(arguments, 1)) |
| 181 | + |
| 182 | + # Ensure both functions have the same unique id so that jQuery will accept |
| 183 | + # callback when unbinding closure. |
| 184 | + closure.guid = callback.guid = ($.guid += 1) |
| 185 | + |
| 186 | + @element.bind event, closure |
| 187 | + this |
| 188 | + |
| 189 | + # Public: Unsubscribes a callback from an event. The callback will no longer |
| 190 | + # be called when the event is published. |
| 191 | + # |
| 192 | + # event - A String event name. |
| 193 | + # callback - A callback function to be removed. |
| 194 | + # |
| 195 | + # Examples |
| 196 | + # |
| 197 | + # callback = (msg) -> console.log(msg) |
| 198 | + # instance.subscribe('annotation:save', callback) |
| 199 | + # instance.publish('annotation:save', ['Hello World']) |
| 200 | + # # => Outputs "Hello World" |
| 201 | + # |
| 202 | + # instance.unsubscribe('annotation:save', callback) |
| 203 | + # instance.publish('annotation:save', ['Hello Again']) |
| 204 | + # # => No output. |
| 205 | + # |
| 206 | + # Returns itself. |
| 207 | + unsubscribe: -> |
| 208 | + @element.unbind.apply @element, arguments |
| 209 | + this |
| 210 | + |
| 211 | + |
| 212 | +# Parse the @events object of a Delegator into an array of objects containing |
| 213 | +# string-valued "selector", "event", and "func" keys. |
| 214 | +Delegator._parseEvents = (eventsObj) -> |
| 215 | + events = [] |
| 216 | + for sel, functionName of eventsObj |
| 217 | + [selector..., event] = sel.split ' ' |
| 218 | + events.push({ |
| 219 | + selector: selector.join(' '), |
| 220 | + event: event, |
| 221 | + functionName: functionName |
| 222 | + }) |
| 223 | + return events |
| 224 | + |
| 225 | + |
| 226 | +# Native jQuery events that should recieve an event object. Plugins can |
| 227 | +# add their own methods to this if required. |
| 228 | +Delegator.natives = do -> |
| 229 | + specials = (key for own key, val of jQuery.event.special) |
| 230 | + """ |
| 231 | + blur focus focusin focusout load resize scroll unload click dblclick |
| 232 | + mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave |
| 233 | + change select submit keydown keypress keyup error |
| 234 | + """.split(/[^a-z]+/).concat(specials) |
| 235 | + |
| 236 | + |
| 237 | +# Checks to see if the provided event is a DOM event supported by jQuery or |
| 238 | +# a custom user event. |
| 239 | +# |
| 240 | +# event - String event name. |
| 241 | +# |
| 242 | +# Examples |
| 243 | +# |
| 244 | +# Delegator._isCustomEvent('click') # => false |
| 245 | +# Delegator._isCustomEvent('mousedown') # => false |
| 246 | +# Delegator._isCustomEvent('annotation:created') # => true |
| 247 | +# |
| 248 | +# Returns true if event is a custom user event. |
| 249 | +Delegator._isCustomEvent = (event) -> |
| 250 | + [event] = event.split('.') |
| 251 | + $.inArray(event, Delegator.natives) == -1 |
0 commit comments