1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2024 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15let listeners = [];
16
17/**
18 * Provides simplified event handling for DOM nodes. Used as a
19 * mixin into RapidContext.Widget instances and similar, but also
20 * provides static versions of the same functions.
21 *
22 * @class RapidContext.UI.Event
23 */
24export class Event {
25
26 /**
27 * Dispatches a single event from this DOM node. Also creates
28 * a new CustomEvent instance if needed.
29 *
30 * @method emit
31 * @memberof RapidContext.UI.Event.prototype
32 * @param {string/Event} event the event type name or instance
33 * @param {Object} [opts] the event options
34 * @param {boolean} [opts.async=true] the async dispatch flag
35 * @param {boolean} [opts.bubbles=false] the event bubbles flag
36 * @param {boolean} [opts.cancelable=false] the cancellable event flag
37 * @param {Object} [opts.detail] the additional event details
38 * @return {boolean} `true` if event was async or not cancelled
39 */
40 emit(event, opts) {
41 return emit(this, event, opts);
42 }
43
44 /**
45 * Attaches a listener function for one or more events.
46 *
47 * @method on
48 * @memberof RapidContext.UI.Event.prototype
49 * @param {string} event the event type name (or space separated names)
50 * @param {string} [selector] the CSS selector to match for event target
51 * @param {function} listener the event handler function (or `false`)
52 * @param {Object} [opts] the event listener options (see addEventListener)
53 * @param {number} [opts.delay] an inactivity delay before calling listener
54 * @return {Node} the input DOM node (for chaining calls)
55 */
56 on(event, selector, listener, opts) {
57 return on(this, event, selector, listener, opts);
58 }
59
60 /**
61 * Attaches a single event listener function. The listener will
62 * be removed the first time an event is triggered.
63 *
64 * @method once
65 * @memberof RapidContext.UI.Event.prototype
66 * @param {string} event the event type name (or space separated names)
67 * @param {string} [selector] the CSS selector to match for event target
68 * @param {function} listener the event handler function (or `false`)
69 * @param {Object} [opts] the event listener options (see addEventListener)
70 * @param {number} [opts.delay] an inactivity delay before calling listener
71 * @return {Node} the input DOM node (for chaining calls)
72 */
73 once(event, selector, listener, opts) {
74 return once(this, event, selector, listener, opts);
75 }
76
77 /**
78 * Removes one or more previously attached event listeners. If
79 * no event details or listeners are specified, all matching
80 * handlers are removed.
81 *
82 * @method off
83 * @memberof RapidContext.UI.Event.prototype
84 * @param {string} [event] the event type name (or space separated names)
85 * @param {string} [selector] the CSS selector to match for event target
86 * @param {function} [listener] the event handler function (or `false`)
87 * @return {Node} the input DOM node (for chaining calls)
88 */
89 off(event, selector, listener) {
90 return off(this, event, selector, listener);
91 }
92}
93
94/**
95 * Dispatches a single event for a DOM node. Also creates a new
96 * CustomEvent instance if needed.
97 *
98 * @param {Node} src the DOM node emitting the event
99 * @param {string/Event} event the event type name or instance
100 * @param {Object} [opts] the event options
101 * @param {boolean} [opts.async=true] the async dispatch flag
102 * @param {boolean} [opts.bubbles=false] the event bubbles flag
103 * @param {boolean} [opts.cancelable=false] the cancellable event flag
104 * @param {Object} [opts.detail] the additional event details
105 * @return {boolean} `true` if event was async or not cancelled
106 * @function RapidContext.UI.Event.emit
107 */
108export function emit(src, event, opts) {
109 opts = Object.assign({ async: true }, opts);
110 event = (event instanceof window.Event) ? event : new CustomEvent(event, opts);
111 if (opts.async) {
112 setTimeout(() => src.dispatchEvent(event));
113 return true;
114 } else {
115 return src.dispatchEvent(event);
116 }
117}
118
119/**
120 * Attaches a listener function for one or more events.
121 *
122 * @param {Node} src the DOM node when event handler is attached
123 * @param {string} event the event type name (or space separated names)
124 * @param {string} [selector] the CSS selector to match for event target
125 * @param {function} listener the event handler function (or `false`)
126 * @param {Object} [opts] the event listener options (see addEventListener)
127 * @param {number} [opts.delay] an inactivity delay before calling listener
128 * @return {Node} the input DOM node (for chaining calls)
129 * @function RapidContext.UI.Event.on
130 */
131export function on(src, event, selector, listener, opts) {
132 if (typeof(selector) != 'string') {
133 opts = arguments[3];
134 listener = arguments[2];
135 selector = null;
136 }
137 let handler = (listener === false) ? stop : listener;
138 if (opts && opts.delay) {
139 handler = debounce(opts.delay, handler);
140 }
141 if (selector) {
142 handler = delegate.bind(null, selector, handler);
143 }
144 let arr = Array.isArray(event) ? event : event.split(/\s+/g);
145 arr.forEach((event) => {
146 src.addEventListener(event, handler, opts);
147 listeners.push({ src, event, selector, listener, handler, opts });
148 });
149 return src;
150}
151
152/**
153 * Attaches a single event listener function. The listener will
154 * be removed the first time an event is triggered.
155 *
156 * @param {Node} src the DOM node when event handler is attached
157 * @param {string} event the event type name (or space separated names)
158 * @param {string} [selector] the CSS selector to match for event target
159 * @param {function} listener the event handler function (or `false`)
160 * @param {Object} [opts] the event listener options (see addEventListener)
161 * @param {number} [opts.delay] an inactivity delay before calling listener
162 * @return {Node} the input DOM node (for chaining calls)
163 * @function RapidContext.UI.Event.once
164 */
165export function once(src, event, selector, listener, opts) {
166 function handler(evt) {
167 off(src, event, selector, handler);
168 listener.call(this, evt);
169 }
170 return on(src, event, selector, handler, opts);
171}
172
173/**
174 * Removes one or more previously attached event listeners. If
175 * no event details or listeners are specified, all matching
176 * handlers are removed.
177 *
178 * @param {Node} src the DOM node when event handler is attached
179 * @param {string} [event] the event type name (or space separated names)
180 * @param {string} [selector] the CSS selector to match for event target
181 * @param {function} [listener] the event handler function (or `false`)
182 * @return {Node} the input DOM node (for chaining calls)
183 * @function RapidContext.UI.Event.off
184 */
185export function off(src, event, selector, listener) {
186 if (typeof(selector) != 'string') {
187 listener = arguments[2];
188 selector = null;
189 }
190 let arr = (event == null || Array.isArray(event)) ? event : event.split(/\s+/g);
191 let matches = listeners.filter((l) => {
192 return src === l.src &&
193 (arr == null || arr.includes(l.event)) &&
194 (selector == null || selector === l.selector) &&
195 (listener == null || listener === l.listener);
196 });
197 matches.forEach((l) => {
198 src.removeEventListener(l.event, l.handler, l.opts);
199 listeners.splice(listeners.indexOf(l), 1);
200 });
201 return src;
202}
203
204function parents(el, ancestor) {
205 let path = [];
206 for (; el && el !== ancestor; el = el.parentElement) {
207 path.push(el);
208 }
209 el && path.push(el);
210 return path;
211}
212
213function delegate(selector, listener, evt) {
214 let isMatch = (el) => el.matches(selector);
215 let forward = (el) => {
216 try {
217 evt.delegateTarget = el;
218 listener.call(evt.currentTarget, evt);
219 } finally {
220 delete evt.delegateTarget;
221 }
222 };
223 parents(evt.target, evt.currentTarget).filter(isMatch).forEach(forward);
224}
225
226function stop(evt) {
227 evt.preventDefault();
228 evt.stopImmediatePropagation();
229}
230
231function debounce(delay, fn, thisObj) {
232 let timer;
233 return function () {
234 clearTimeout(timer);
235 timer = setTimeout(fn.bind(thisObj || this, arguments), delay);
236 };
237}
238
239export default Object.assign(Event, { on, once, off, emit });
240
RapidContext
Access · Discovery · Insight
www.rapidcontext.com