###** Events ====== Most Unpoly interactions emit DOM events that are prefixed with `up:`. document.addEventListener('up:modal:opened', (event) => { console.log('A new modal has just opened!') }) Events often have both present and past forms. For example, `up:modal:open` is emitted before a modal starts to open. `up:modal:opened` is emitted when the modal has finished its opening animation. \#\#\# Preventing events You can prevent most present form events by calling `preventDefault()`: document.addEventListener('up:modal:open', (event) => { if (event.url == '/evil') { // Prevent the modal from opening event.preventDefault() } }) \#\#\# A better way to bind event listeners Instead of using [`Element#addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener), you may find it convenient to use [`up.on()`](/up.on) instead: up.on('click', 'button', function(event, button, data) { // button is the clicked element // data is the parsed [`up-data`](/up-data) attribute }) There are some advantages to using `up.on()`: - You may pass a selector for [event delegation](https://davidwalsh.name/event-delegate). - The event target is automatically passed as a second argument. - You may register a listener to multiple events by passing a space-separated list of event name (e.g. `"click mousedown"`). - You may register a listener to multiple elements in a single `up.on()` call, by passing a [list](/up.util.isList) of elements. - You may use an [`[up-data]`](/up-data) attribute to [attach structured data](/up.on#attaching-structured-data) to observed elements. If an `[up-data]` attribute is set, its value will automatically be parsed as JSON and passed as a third argument. - Event listeners on [unsupported browsers](/up.browser.isSupported) are silently discarded, leaving you with an application without JavaScript. This is typically preferable to a soup of randomly broken JavaScript in ancient browsers. @module up.event ### up.event = do -> u = up.util e = up.element reset = -> # Resets the list of registered event listeners to the # moment when the framework was booted. for element in [window, document, document.documentElement, document.body] up.EventListener.unbindNonDefault(element) ###** Listens to a [DOM event](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Events) on `document` or a given element. `up.on()` has some quality of life improvements over [`Element#addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener): - You may pass a selector for [event delegation](https://davidwalsh.name/event-delegate). - The event target is automatically passed as a second argument. - You may register a listener to multiple events by passing a space-separated list of event name (e.g. `"click mousedown"`) - You may register a listener to multiple elements in a single `up.on()` call, by passing a [list](/up.util.isList) of elements. - You use an [`[up-data]`](/up-data) attribute to [attach structured data](/up.on#attaching-structured-data) to observed elements. If an `[up-data]` attribute is set, its value will automatically be parsed as JSON and passed as a third argument. - Event listeners on [unsupported browsers](/up.browser.isSupported) are silently discarded, leaving you with an application without JavaScript. This is typically preferable to a soup of randomly broken JavaScript in ancient browsers. \#\#\# Examples The code below will call the listener when a `` is clicked anywhere in the `document`: up.on('click', 'a', function(event, element) { console.log("Click on a link %o", element) }) You may also bind the listener to a given element instead of `document`: var form = document.querySelector('form') up.on(form, 'click', function(event, form) { console.log("Click within %o", form) }) You may also pass both an element and a selector for [event delegation](https://davidwalsh.name/event-delegate): var form = document.querySelector('form') up.on(form, 'click', 'a', function(event, link) { console.log("Click on a link %o within %o", link, form) }) \#\#\# Attaching structured data In case you want to attach structured data to the event you're observing, you can serialize the data to JSON and put it into an `[up-data]` attribute: Bob Jim The JSON will be parsed and handed to your event handler as a third argument: up.on('click', '.person', function(event, element, data) { console.log("This is %o who is %o years old", data.name, data.age) }) \#\#\# Unbinding an event listener `up.on()` returns a function that unbinds the event listeners when called: // Define the listener var listener = function(event) { ... } // Binding the listener returns an unbind function var unbind = up.on('click', listener) // Unbind the listener unbind() There is also a function [`up.off()`](/up.off) which you can use for the same purpose: // Define the listener var listener = function(event) { ... } // Bind the listener up.on('click', listener) // Unbind the listener up.off('click', listener) @function up.on @param {Element|jQuery} [element=document] The element on which to register the event listener. If no element is given, the listener is registered on the `document`. @param {string} events A space-separated list of event names to bind to. @param {string} [selector] The selector of an element on which the event must be triggered. Omit the selector to listen to all events with that name, regardless of the event target. @param {Function(event, [element], [data])} listener The listener function that should be called. The function takes the affected element as the first argument). If the element has an [`up-data`](/up-data) attribute, its value is parsed as JSON and passed as a second argument. @return {Function()} A function that unbinds the event listeners when called. @stable ### bind = (args...) -> bindNow(args) ###** Listens to an event on `document` or a given element. The event handler is called with the event target as a [jQuery collection](https://learn.jquery.com/using-jquery-core/jquery-object/). If you're not using jQuery, use `up.on()` instead, which calls event handlers with a native element. \#\#\# Example ``` up.$on('click', 'a', function(event, $link) { console.log("Click on a link with destination %s", $element.attr('href')) }) ``` @function up.$on @param {Element|jQuery} [element=document] The element on which to register the event listener. If no element is given, the listener is registered on the `document`. @param {string} events A space-separated list of event names to bind to. @param {string} [selector] The selector of an element on which the event must be triggered. Omit the selector to listen to all events with that name, regardless of the event target. @param {Function(event, [element], [data])} listener The listener function that should be called. The function takes the affected element as the first argument). If the element has an [`up-data`](/up-data) attribute, its value is parsed as JSON and passed as a second argument. @return {Function()} A function that unbinds the event listeners when called. @stable ### $bind = (args...) -> bindNow(args, jQuery: true) bindNow = (args, options) -> # Silently discard any event handlers that are registered on unsupported # browsers and return a no-op destructor return (->) unless up.browser.isSupported() up.EventListener.bind(args, options) ###** Unbinds an event listener previously bound with [`up.on()`](/up.on). \#\#\# Example Let's say you are listing to clicks on `.button` elements: var listener = function() { ... } up.on('click', '.button', listener) You can stop listening to these events like this: up.off('click', '.button', listener) Note that you need to pass `up.off()` a reference to the same listener function that was passed to `up.on()` earlier. @function up.off @stable ### unbind = (args...) -> up.EventListener.unbind(args) ###** Emits a event with the given name and properties. The event will be triggered as an event on `document` or on the given element. Other code can subscribe to events with that name using [`Element#addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) or [`up.on()`](/up.on). \#\#\# Example up.on('my:event', function(event) { console.log(event.foo) }) up.emit('my:event', { foo: 'bar' }) // Prints "bar" to the console @function up.emit @param {Element|jQuery} [target=document] The element on which the event is triggered. If omitted, the event will be emitted on the `document`. @param {string} eventName The name of the event. @param {Object} [eventProps={}] A list of properties to become part of the event object that will be passed to listeners. Note that the event object will by default include properties like `preventDefault()` or `stopPropagation()`. @param {string|Array} [eventProps.log=false] A message to print to the console when the event is emitted. Pass `true` to print a default message @param {Element|jQuery} [eventProps.target=document] The element on which the event is triggered. @stable ### emit = (args...) -> if args[0].addEventListener target = args.shift() else if u.isJQuery(args[0]) target = e.get(args.shift()) eventName = args[0] eventProps = args[1] || {} if targetFromProps = u.pluckKey(eventProps, 'target') target = targetFromProps target ?= document logEmission(eventName, eventProps) event = buildEvent(eventName, eventProps) target.dispatchEvent(event) return event buildEvent = (name, props) -> event = document.createEvent('Event') event.initEvent(name, true, true) # name, bubbles, cancelable u.assign(event, props) # IE11 does not set { defaultPrevented: true } after #preventDefault() # was called on a custom event. # See discussion here: https://stackoverflow.com/questions/23349191 if up.browser.isIE11() event.preventDefault = -> Object.defineProperty(event, 'defaultPrevented', get: -> true) event logEmission = (eventName, eventProps) -> return unless up.log.isEnabled() message = u.pluckKey(eventProps, 'log') if u.isArray(message) [message, messageArgs...] = message else messageArgs = [] if u.isString(message) if u.isPresent(eventProps) up.puts "#{message} (%s (%o))", messageArgs..., eventName, eventProps else up.puts "#{message} (%s)", messageArgs..., eventName else if message == true if u.isPresent(eventProps) up.puts 'Event %s (%o)', eventName, eventProps else up.puts 'Event %s', eventName ###** [Emits an event](/up.emit) and returns whether no listener has prevented the default action. @function up.event.nobodyPrevents @param {string} eventName @param {Object} eventProps @param {string|Array} [eventProps.log] @return {boolean} whether no listener has prevented the default action @experimental ### nobodyPrevents = (args...) -> event = emit(args...) not event.defaultPrevented ###** [Emits](/up.emit) the given event and returns a promise that will be fulfilled if no listener has prevented the default action. If any listener prevented the default listener the returned promise will never be resolved. @function up.event.whenEmitted @param {string} eventName @param {Object} eventProps @param {string|Array} [eventProps.message] @return {Promise} @internal ### whenEmitted = (args...) -> new Promise (resolve, reject) -> if nobodyPrevents(args...) resolve() else reject(new Error("Event #{args[0]} was prevented")) ###** Registers an event listener to be called when the user presses the `Escape` key. @function up.event.onEscape @param {Function(event)} listener The listener function to register. @return {Function()} A function that unbinds the event listeners when called. @experimental ### onEscape = (listener) -> bind('keydown', 'body', (event) -> if u.escapePressed(event) listener(event) ) ###** Prevents the event from bubbling up the DOM. Also prevents other event handlers bound on the same element. Also prevents the event's default action. \#\#\# Example up.on('click', 'link.disabled', function(event) { up.event.halt(event) }) @function up.event.halt @param {Event} event @experimental ### halt = (event) -> event.stopImmediatePropagation() event.preventDefault() ###** @function up.event.consumeAction @internal ### consumeAction = (event) -> # Halt the event chain to stop duplicate processing of this user interaction. halt(event) unless event.type == 'up:action:consumed' # Although we have consumed this action and halted the event chain, # other components might still need to react. E.g. a popup needs to close when # an outside link consumes the user click. So we emit another event for that. emit(event.target, 'up:action:consumed', log: false) onReady = (callback) -> # Values are "loading", "interactive" and "completed". # https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState if document.readyState != 'loading' callback() else document.addEventListener('DOMContentLoaded', callback) bind 'up:framework:reset', reset <% if ENV['JS_KNIFE'] %>knife: eval(Knife.point)<% end %> on: bind # can't name symbols `on` in Coffeescript $on: $bind off: unbind # can't name symbols `off` in Coffeescript emit: emit nobodyPrevents: nobodyPrevents whenEmitted: whenEmitted onEscape: onEscape halt: halt consumeAction: consumeAction onReady: onReady up.on = up.event.on up.$on = up.event.$on up.off = up.event.off up.$off = up.event.off # it's the same as up.off() up.emit = up.event.emit up.legacy.renamedModule 'bus', 'event'