@converse/skeletor 0.0.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +5 -89
  2. package/dist/skeletor.d.ts +764 -0
  3. package/dist/skeletor.js +7568 -7747
  4. package/dist/skeletor.min.js +6 -11
  5. package/package.json +46 -30
  6. package/src/{collection.js → collection.ts} +232 -271
  7. package/src/drivers/{sessionStorage.js → sessionStorage.ts} +81 -93
  8. package/src/{eventemitter.js → eventemitter.ts} +44 -59
  9. package/src/{helpers.js → helpers.ts} +56 -69
  10. package/src/index.ts +84 -0
  11. package/src/{listening.js → listening.ts} +32 -27
  12. package/src/{model.js → model.ts} +143 -162
  13. package/src/{storage.js → storage.ts} +92 -49
  14. package/src/types.ts +189 -0
  15. package/src/utils/{events.js → events.ts} +98 -48
  16. package/dist/skeletor.js.map +0 -1
  17. package/dist/skeletor.min.js.map +0 -1
  18. package/src/element.js +0 -238
  19. package/src/index.js +0 -47
  20. package/src/types/collection.d.ts +0 -366
  21. package/src/types/collection.d.ts.map +0 -1
  22. package/src/types/drivers/sessionStorage.d.ts +0 -37
  23. package/src/types/drivers/sessionStorage.d.ts.map +0 -1
  24. package/src/types/element.d.ts +0 -115
  25. package/src/types/element.d.ts.map +0 -1
  26. package/src/types/eventemitter.d.ts +0 -93
  27. package/src/types/eventemitter.d.ts.map +0 -1
  28. package/src/types/helpers.d.ts +0 -62
  29. package/src/types/helpers.d.ts.map +0 -1
  30. package/src/types/index.d.ts +0 -19
  31. package/src/types/index.d.ts.map +0 -1
  32. package/src/types/listening.d.ts +0 -40
  33. package/src/types/listening.d.ts.map +0 -1
  34. package/src/types/model.d.ts +0 -230
  35. package/src/types/model.d.ts.map +0 -1
  36. package/src/types/storage.d.ts +0 -35
  37. package/src/types/storage.d.ts.map +0 -1
  38. package/src/types/utils/events.d.ts +0 -28
  39. package/src/types/utils/events.d.ts.map +0 -1
@@ -2,33 +2,71 @@
2
2
  * IndexedDB, localStorage and sessionStorage adapter
3
3
  */
4
4
  import * as memoryDriver from 'localforage-driver-memory';
5
- import cloneDeep from 'lodash-es/cloneDeep.js';
6
- import isString from 'lodash-es/isString.js';
5
+ import cloneDeep from 'lodash-es/cloneDeep';
6
+ import isString from 'lodash-es/isString';
7
7
  import localForage from 'localforage';
8
8
  import mergebounce from 'mergebounce';
9
- import sessionStorageWrapper from './drivers/sessionStorage.js';
9
+ import sessionStorageWrapper from './drivers/sessionStorage';
10
10
  import { extendPrototype as extendPrototypeWithSetItems } from 'localforage-setitems';
11
11
  import { extendPrototype as extendPrototypeWithGetItems } from '@converse/localforage-getitems/dist/localforage-getitems.es6';
12
- import { guid } from './helpers.js';
12
+ import { guid } from './helpers';
13
+ import type { Model } from './model';
14
+ import type { Collection } from './collection';
15
+ import type { SyncOptions, SyncOperation } from './types';
13
16
 
14
17
  const IN_MEMORY = memoryDriver._driver;
15
18
  localForage.defineDriver(memoryDriver);
16
19
  extendPrototypeWithSetItems(localForage);
17
20
  extendPrototypeWithGetItems(localForage);
18
21
 
19
- class Storage {
20
- constructor(id, type, batchedWrites = false) {
22
+ /**
23
+ * @public
24
+ */
25
+ export interface LocalForageWithExtensions {
26
+ setItem(key: string, value: any): Promise<any>;
27
+ getItem(key: string): Promise<any>;
28
+ removeItem(key: string): Promise<void>;
29
+ clear(): Promise<void>;
30
+ length(): Promise<number>;
31
+ key(keyIndex: number): Promise<string>;
32
+ keys(): Promise<string[]>;
33
+ setItems?(items: Record<string, any>): Promise<void>;
34
+ getItems?(keys: string[]): Promise<Record<string, any>>;
35
+ debouncedSetItems?: {
36
+ (items: Record<string, any>): Promise<void>;
37
+ flush?: () => void;
38
+ };
39
+ }
40
+
41
+ /**
42
+ * @public
43
+ */
44
+ class BrowserStorage {
45
+ storeInitialized: Promise<void>;
46
+ store: LocalForageWithExtensions;
47
+ name: string;
48
+
49
+ static sessionStorageInitialized: Promise<void>;
50
+ static localForage: typeof localForage;
51
+
52
+ constructor(
53
+ id: string,
54
+ type: 'local' | 'session' | 'indexed' | 'in_memory' | LocalForageWithExtensions,
55
+ batchedWrites = false
56
+ ) {
21
57
  if (type === 'local' && !window.localStorage) {
22
58
  throw new Error('Skeletor.storage: Environment does not support localStorage.');
23
59
  } else if (type === 'session' && !window.sessionStorage) {
24
60
  throw new Error('Skeletor.storage: Environment does not support sessionStorage.');
25
61
  }
26
62
  if (isString(type)) {
27
- this.storeInitialized = this.initStore(type, batchedWrites);
63
+ this.storeInitialized = this.initStore(type as 'local' | 'session' | 'indexed' | 'in_memory', batchedWrites);
28
64
  } else {
29
65
  this.store = type;
30
66
  if (batchedWrites) {
31
- this.store.debouncedSetItems = mergebounce((items) => this.store.setItems(items), 50, { 'promise': true });
67
+ this.store.debouncedSetItems = mergebounce((items: Record<string, any>) => this.store.setItems!(items), 50, {
68
+ 'promise': true,
69
+ });
32
70
  }
33
71
  this.storeInitialized = Promise.resolve();
34
72
  }
@@ -36,10 +74,10 @@ class Storage {
36
74
  }
37
75
 
38
76
  /**
39
- * @param {'local'|'session'|'indexed'|'in_memory'} type
40
- * @param {boolean} batchedWrites
77
+ * @param type - The storage type: 'local', 'session', 'indexed', or 'in_memory'
78
+ * @param batchedWrites - Whether to enable batched writes
41
79
  */
42
- async initStore(type, batchedWrites) {
80
+ async initStore(type: 'local' | 'session' | 'indexed' | 'in_memory', batchedWrites: boolean): Promise<void> {
43
81
  if (type === 'session') {
44
82
  await localForage.setDriver(sessionStorageWrapper._driver);
45
83
  } else if (type === 'local') {
@@ -49,17 +87,21 @@ class Storage {
49
87
  } else if (type !== 'indexed') {
50
88
  throw new Error('Skeletor.storage: No storage type was specified');
51
89
  }
52
- this.store = localForage;
90
+ this.store = localForage as LocalForageWithExtensions;
53
91
  if (batchedWrites) {
54
- this.store.debouncedSetItems = mergebounce((items) => this.store.setItems(items), 50, { 'promise': true });
92
+ this.store.debouncedSetItems = mergebounce((items: Record<string, any>) => this.store.setItems!(items), 50, {
93
+ 'promise': true,
94
+ });
55
95
  }
56
96
  }
57
97
 
58
- flush() {
59
- return this.store.debouncedSetItems?.flush();
98
+ flush(): void {
99
+ if (this.store.debouncedSetItems && typeof this.store.debouncedSetItems.flush === 'function') {
100
+ this.store.debouncedSetItems.flush();
101
+ }
60
102
  }
61
103
 
62
- async clear() {
104
+ async clear(): Promise<void> {
63
105
  await this.store.removeItem(this.name).catch((e) => console.error(e));
64
106
  const re = new RegExp(`^${this.name}-`);
65
107
  const keys = await this.store.keys();
@@ -71,8 +113,8 @@ class Storage {
71
113
  // eslint-disable-next-line @typescript-eslint/no-this-alias
72
114
  const that = this;
73
115
 
74
- async function localSync(method, model, options) {
75
- let resp, errorMessage, promise, new_attributes;
116
+ async function localSync(method: SyncOperation, model: Model, options: SyncOptions) {
117
+ let resp: any, errorMessage: string | undefined, promise: Promise<any>, new_attributes: any;
76
118
 
77
119
  // We get the collection (and if necessary the model attribute.
78
120
  // Waiting for storeInitialized will cause another iteration of
@@ -121,8 +163,9 @@ class Storage {
121
163
  resp = await that.destroy(model, collection);
122
164
  break;
123
165
  }
124
- } catch (error) {
125
- if (error.code === 22 && that.getStorageSize() === 0) {
166
+ } catch (error: any) {
167
+ const storageSize = await that.getStorageSize();
168
+ if (error.code === 22 && storageSize === 0) {
126
169
  errorMessage = 'Private browsing is unsupported';
127
170
  } else {
128
171
  errorMessage = error.message;
@@ -150,56 +193,56 @@ class Storage {
150
193
  return localSync;
151
194
  }
152
195
 
153
- removeCollectionReference(model, collection) {
196
+ removeCollectionReference(model: Model, collection: Collection | undefined): Promise<any> | undefined {
154
197
  if (!collection) {
155
198
  return;
156
199
  }
157
- const ids = collection.filter((m) => m.id !== model.id).map((m) => this.getItemName(m.id));
200
+ const ids = collection.filter((m) => m.id !== model.id).map((m) => this.getItemName(m.id!));
158
201
 
159
202
  return this.store.setItem(this.name, ids);
160
203
  }
161
204
 
162
- addCollectionReference(model, collection) {
205
+ addCollectionReference(model: Model, collection: Collection | undefined): Promise<any> | undefined {
163
206
  if (!collection) {
164
207
  return;
165
208
  }
166
- const ids = collection.map((m) => this.getItemName(m.id));
167
- const new_id = this.getItemName(model.id);
209
+ const ids = collection.map((m) => this.getItemName(m.id!));
210
+ const new_id = this.getItemName(model.id!);
168
211
  if (!ids.includes(new_id)) {
169
212
  ids.push(new_id);
170
213
  }
171
214
  return this.store.setItem(this.name, ids);
172
215
  }
173
216
 
174
- getCollectionReferenceData(model) {
217
+ getCollectionReferenceData(model: Model): Record<string, string[]> {
175
218
  if (!model.collection) {
176
219
  return {};
177
220
  }
178
- const ids = model.collection.map((m) => this.getItemName(m.id));
179
- const new_id = this.getItemName(model.id);
221
+ const ids = model.collection.map((m) => this.getItemName(m.id!));
222
+ const new_id = this.getItemName(model.id!);
180
223
  if (!ids.includes(new_id)) {
181
224
  ids.push(new_id);
182
225
  }
183
- const result = {};
226
+ const result: Record<string, string[]> = {};
184
227
  result[this.name] = ids;
185
228
  return result;
186
229
  }
187
230
 
188
- async save(model) {
231
+ async save(model: Model): Promise<any> {
189
232
  if (this.store.setItems) {
190
- const items = {};
191
- items[this.getItemName(model.id)] = model.toJSON();
233
+ const items: Record<string, any> = {};
234
+ items[this.getItemName(model.id!)] = model.toJSON();
192
235
  Object.assign(items, this.getCollectionReferenceData(model));
193
- return this.store.debouncedSetItems ? this.store.debouncedSetItems(items) : this.store.setItems(items);
236
+ return this.store.debouncedSetItems ? this.store.debouncedSetItems(items) : this.store.setItems!(items);
194
237
  } else {
195
- const key = this.getItemName(model.id);
238
+ const key = this.getItemName(model.id!);
196
239
  const data = await this.store.setItem(key, model.toJSON());
197
240
  await this.addCollectionReference(model, model.collection);
198
241
  return data;
199
242
  }
200
243
  }
201
244
 
202
- create(model, options) {
245
+ create(model: Model, options: SyncOptions): Promise<any> {
203
246
  /* Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
204
247
  * have an id of it's own.
205
248
  */
@@ -210,41 +253,41 @@ class Storage {
210
253
  return this.save(model);
211
254
  }
212
255
 
213
- update(model) {
256
+ update(model: Model): Promise<any> {
214
257
  return this.save(model);
215
258
  }
216
259
 
217
- find(model) {
218
- return this.store.getItem(this.getItemName(model.id));
260
+ find(model: Model): Promise<any> {
261
+ return this.store.getItem(this.getItemName(model.id!));
219
262
  }
220
263
 
221
- async findAll() {
264
+ async findAll(): Promise<any[]> {
222
265
  /* Return the array of all models currently in storage.
223
266
  */
224
- const keys = await this.store.getItem(this.name);
267
+ const keys = (await this.store.getItem(this.name)) as string[] | null;
225
268
  if (keys?.length) {
226
- const items = await this.store.getItems(keys);
269
+ const items = await this.store.getItems!(keys);
227
270
  return Object.values(items);
228
271
  }
229
272
  return [];
230
273
  }
231
274
 
232
- async destroy(model, collection) {
275
+ async destroy(model: Model, collection: Collection | undefined): Promise<Model> {
233
276
  await this.flush();
234
- await this.store.removeItem(this.getItemName(model.id));
277
+ await this.store.removeItem(this.getItemName(model.id!));
235
278
  await this.removeCollectionReference(model, collection);
236
279
  return model;
237
280
  }
238
281
 
239
- getStorageSize() {
240
- return this.store.length;
282
+ async getStorageSize(): Promise<number> {
283
+ return await this.store.length();
241
284
  }
242
285
 
243
- getItemName(id) {
286
+ getItemName(id: string | number): string {
244
287
  return this.name + '-' + id;
245
288
  }
246
289
  }
247
290
 
248
- Storage.sessionStorageInitialized = localForage.defineDriver(sessionStorageWrapper);
249
- Storage.localForage = localForage;
250
- export default Storage;
291
+ BrowserStorage.sessionStorageInitialized = localForage.defineDriver(sessionStorageWrapper);
292
+ BrowserStorage.localForage = localForage;
293
+ export default BrowserStorage;
package/src/types.ts ADDED
@@ -0,0 +1,189 @@
1
+ import {Collection} from './collection';
2
+ import {Model} from './model';
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ export type SyncOperation = 'create' | 'update' | 'patch' | 'delete' | 'read';
8
+
9
+ /**
10
+ * @public
11
+ */
12
+ export type ObjectListenedTo = object & {_listenId?: string};
13
+
14
+ /**
15
+ * @public
16
+ */
17
+ export interface SyncOptions {
18
+ url?: string;
19
+ data?: any;
20
+ attrs?: any;
21
+ success?: (data?: any, options?: SyncOptions) => void;
22
+ error?: (error: any) => void;
23
+ xhr?: any;
24
+ wait?: boolean;
25
+ }
26
+
27
+ /**
28
+ * @public
29
+ */
30
+ export type ClassConstructor<T = any> = new (...args: any[]) => T;
31
+
32
+ /**
33
+ * @public
34
+ */
35
+ export type EventContext = unknown;
36
+
37
+ /**
38
+ * @public
39
+ */
40
+ export type EventCallback = (...args: any[]) => void;
41
+
42
+ /**
43
+ * @public
44
+ */
45
+ export type EventCallbackMap = Record<string, EventCallback>;
46
+
47
+ /**
48
+ * @public
49
+ */
50
+ export type EventCallbacksMap = Record<string, EventCallback[]>;
51
+
52
+ /**
53
+ * @public
54
+ */
55
+ export type EventListenerMap = Record<string, ListeningType>;
56
+
57
+ /**
58
+ * @public
59
+ */
60
+ export interface EventHandlersMap {
61
+ all?: EventHandler[];
62
+ [name: string]: EventHandler[];
63
+ }
64
+
65
+ /**
66
+ * @public
67
+ */
68
+ export type ListeningMap = Record<string, ListeningType>;
69
+
70
+ /**
71
+ * @public
72
+ */
73
+ export type ObjectWithId = Record<string, any> & {id: string | number};
74
+
75
+ /**
76
+ * @public
77
+ */
78
+ export interface IEventEmitter {
79
+ _events?: Record<string, any>;
80
+ _listeners?: ListeningMap;
81
+ _listeningTo?: ListeningMap;
82
+ _listenId?: string;
83
+
84
+ on(name: string | EventCallbackMap, callback?: EventCallback | EventContext, context?: EventContext): this;
85
+ off(
86
+ name?: string | EventCallbackMap | null,
87
+ callback?: EventCallback | EventContext | null,
88
+ context?: EventContext
89
+ ): this;
90
+ trigger(name: string, ...args: any[]): this;
91
+ stopListening(obj?: any, name?: string | EventCallbackMap, callback?: EventCallback): this;
92
+ once(name: string | EventCallbackMap, callback?: EventCallback | EventContext, context?: EventContext): this;
93
+ listenTo(obj: ObjectListenedTo, name: string | EventCallbackMap, callback?: EventCallback): this;
94
+ listenToOnce(obj: any, name: string | EventCallbackMap, callback?: EventCallback): this;
95
+ }
96
+
97
+ /**
98
+ * @public
99
+ */
100
+ export interface EventHandler {
101
+ callback: EventCallback;
102
+ context: any;
103
+ ctx: any;
104
+ listening?: ListeningType | null;
105
+ }
106
+
107
+ /**
108
+ * @public
109
+ */
110
+ export interface ListeningType {
111
+ _events?: EventCallbackMap;
112
+ cleanup(): void;
113
+ count: number;
114
+ id: string;
115
+ interop: boolean;
116
+ listener: IEventEmitter;
117
+ obj: any;
118
+
119
+ start(name: string | EventCallbackMap, callback: EventCallback, context: any, _listening: ListeningType): this;
120
+ stop(name: string | EventCallbackMap, callback: EventCallback): void;
121
+ }
122
+
123
+ /**
124
+ * @public
125
+ */
126
+ export interface EventsApiOptions {
127
+ context?: any;
128
+ ctx?: any;
129
+ listening?: ListeningType;
130
+ listeners?: ListeningMap;
131
+ }
132
+
133
+ /**
134
+ * @public
135
+ */
136
+ export interface OffApiOptions {
137
+ context?: any;
138
+ listeners?: {[key: string]: ListeningType};
139
+ }
140
+
141
+ /**
142
+ * @public
143
+ */
144
+ export type Comparator<T extends Model = Model> = string | boolean | ((a: T, b: T) => number) | ((a?: T) => string);
145
+
146
+ /**
147
+ * @public
148
+ */
149
+ export type OfferFunction = (name: string, callback: EventCallback) => void;
150
+
151
+ /**
152
+ * @public
153
+ */
154
+ export type IterateeFunction = (
155
+ eventsOrMap: EventCallbackMap | EventHandlersMap,
156
+ name: string | null,
157
+ callback: EventCallback | EventHandler | null,
158
+ options: EventsApiOptions | OffApiOptions | OfferFunction | any[]
159
+ ) => EventCallbackMap | EventHandlersMap | void;
160
+
161
+ /**
162
+ * @public
163
+ */
164
+ export type ModelAttributes = Record<string | number, any> & {id?: string | number};
165
+
166
+ /**
167
+ * @public
168
+ */
169
+ export type Options = Record<string, any>;
170
+
171
+ /**
172
+ * @public
173
+ */
174
+ export type CollectionOptions<T extends Model = Model> = Options & {
175
+ model?: new (attributes?: Partial<ModelAttributes>, options?: ModelOptions) => T;
176
+ comparator?: Comparator<T>;
177
+ previousModels?: Model[];
178
+ };
179
+
180
+ /**
181
+ * @public
182
+ */
183
+ export type ModelOptions = Options & {
184
+ collection?: Collection;
185
+ parse?: boolean;
186
+ unset?: boolean;
187
+ silent?: boolean;
188
+ validate?: boolean;
189
+ };
@@ -1,5 +1,15 @@
1
- import once from 'lodash-es/once.js';
2
- import keys from 'lodash-es/keys.js';
1
+ import once from 'lodash-es/once';
2
+ import type {
3
+ EventCallback,
4
+ EventHandler,
5
+ EventCallbackMap,
6
+ EventsApiOptions,
7
+ IterateeFunction,
8
+ OffApiOptions,
9
+ OfferFunction,
10
+ EventHandlersMap,
11
+ EventContext,
12
+ } from '../types';
3
13
 
4
14
  // Regular expression used to split event strings.
5
15
  const eventSplitter = /\s+/;
@@ -9,34 +19,55 @@ const eventSplitter = /\s+/;
9
19
  * space-separated events `"change blur", callback` and jQuery-style event
10
20
  * maps `{event: callback}`).
11
21
  */
12
- export function eventsApi(iteratee, events, name, callback, opts) {
13
- let i = 0,
14
- names;
22
+ export function eventsApi(
23
+ iteratee: IterateeFunction,
24
+ events: EventHandlersMap | EventCallbackMap,
25
+ name: string | EventCallbackMap | null,
26
+ callback: EventCallback | EventHandler | EventContext | null,
27
+ opts: EventsApiOptions | OffApiOptions | any[]
28
+ ): EventHandlersMap | EventCallbackMap {
29
+ let i = 0;
30
+ let names: string[];
31
+
15
32
  if (name && typeof name === 'object') {
16
33
  // Handle event maps.
17
- if (callback !== undefined && 'context' in opts && opts.context === undefined) opts.context = callback;
18
- for (names = keys(name); i < names.length; i++) {
34
+ if (callback !== undefined && 'context' in opts && opts.context === undefined) {
35
+ opts.context = callback;
36
+ }
37
+ for (names = Object.keys(name); i < names.length; i++) {
19
38
  events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
20
39
  }
21
- } else if (name && eventSplitter.test(name)) {
40
+ } else if (name && typeof name === 'string' && eventSplitter.test(name)) {
22
41
  // Handle space-separated event names by delegating them individually.
23
42
  for (names = name.split(eventSplitter); i < names.length; i++) {
24
- events = iteratee(events, names[i], callback, opts);
43
+ const result = iteratee(events, names[i], callback as EventCallback | EventHandler, opts);
44
+ if (result !== undefined) {
45
+ events = result as EventHandlersMap;
46
+ }
25
47
  }
26
48
  } else {
27
49
  // Finally, standard events.
28
- events = iteratee(events, name, callback, opts);
50
+ events = iteratee(events, name as string | null, callback as EventCallback | EventHandler, opts) as
51
+ | EventHandlersMap
52
+ | EventCallbackMap;
29
53
  }
30
54
  return events;
31
55
  }
32
56
 
33
- // The reducing API that adds a callback to the `events` object.
34
- export function onApi(events, name, callback, options) {
57
+ /**
58
+ * A reducer that adds a callback to the `events` object.
59
+ */
60
+ export function onApi(
61
+ events: EventHandlersMap,
62
+ name: string,
63
+ callback: EventCallback | null,
64
+ options: EventsApiOptions
65
+ ): EventHandlersMap {
35
66
  if (callback) {
36
67
  const handlers = events[name] || (events[name] = []);
37
- const context = options.context,
38
- ctx = options.ctx,
39
- listening = options.listening;
68
+ const context = options.context;
69
+ const ctx = options.ctx;
70
+ const listening = options.listening;
40
71
  if (listening) listening.count++;
41
72
 
42
73
  handlers.push({ callback, context, ctx: context || ctx, listening });
@@ -47,12 +78,13 @@ export function onApi(events, name, callback, options) {
47
78
  /**
48
79
  * An try-catch guarded #on function, to prevent poisoning the global
49
80
  * `_listening` variable.
50
- * @param {any} obj
51
- * @param {string} name
52
- * @param {Function} callback
53
- * @param {any} context
54
81
  */
55
- export function tryCatchOn(obj, name, callback, context) {
82
+ export function tryCatchOn(
83
+ obj: any,
84
+ name: string | EventCallbackMap,
85
+ callback: EventCallback | EventContext,
86
+ context: EventContext
87
+ ): any {
56
88
  try {
57
89
  obj.on(name, callback, context);
58
90
  } catch (e) {
@@ -63,52 +95,59 @@ export function tryCatchOn(obj, name, callback, context) {
63
95
  /**
64
96
  * The reducing API that removes a callback from the `events` object.
65
97
  */
66
- export function offApi(events, name, callback, options) {
98
+ export function offApi(
99
+ events: EventHandlersMap,
100
+ name: string | null,
101
+ callback: EventCallback | null,
102
+ options: OffApiOptions
103
+ ): EventHandlersMap | void {
67
104
  if (!events) return;
68
105
 
69
- const context = options.context,
70
- listeners = options.listeners;
71
- let i = 0,
72
- names;
106
+ const context = options.context;
107
+ const listeners = options.listeners;
108
+ let i = 0;
109
+ let names: string[];
73
110
 
74
111
  // Delete all event listeners and "drop" events.
75
112
  if (!name && !context && !callback) {
76
- for (names = keys(listeners); i < names.length; i++) {
77
- listeners[names[i]].cleanup();
113
+ if (listeners) {
114
+ for (names = Object.keys(listeners); i < names.length; i++) {
115
+ listeners[names[i]].cleanup();
116
+ }
78
117
  }
79
118
  return;
80
119
  }
81
120
 
82
- names = name ? [name] : keys(events);
121
+ names = name ? [name] : Object.keys(events);
83
122
  for (; i < names.length; i++) {
84
- name = names[i];
85
- const handlers = events[name];
123
+ const currentName = names[i];
124
+ const handlers = events[currentName];
86
125
 
87
126
  // Bail out if there are no events stored.
88
127
  if (!handlers) {
89
- break;
128
+ continue;
90
129
  }
91
130
 
92
131
  // Find any remaining events.
93
- const remaining = [];
132
+ const remaining: EventHandler[] = [];
94
133
  for (let j = 0; j < handlers.length; j++) {
95
134
  const handler = handlers[j];
96
135
  if (
97
- (callback && callback !== handler.callback && callback !== handler.callback._callback) ||
136
+ (callback && callback !== handler.callback && callback !== (handler.callback as any)._callback) ||
98
137
  (context && context !== handler.context)
99
138
  ) {
100
139
  remaining.push(handler);
101
140
  } else {
102
141
  const listening = handler.listening;
103
- if (listening) listening.stop(name, callback);
142
+ if (listening) listening.stop(currentName, callback!);
104
143
  }
105
144
  }
106
145
 
107
146
  // Replace events if there are any remaining. Otherwise, clean up.
108
147
  if (remaining.length) {
109
- events[name] = remaining;
148
+ events[currentName] = remaining;
110
149
  } else {
111
- delete events[name];
150
+ delete events[currentName];
112
151
  }
113
152
  }
114
153
 
@@ -119,19 +158,30 @@ export function offApi(events, name, callback, options) {
119
158
  * Reduces the event callbacks into a map of `{event: onceWrapper}`.
120
159
  * `offer` unbinds the `onceWrapper` after it has been called.
121
160
  */
122
- export function onceMap(map, name, callback, offer) {
161
+ export function onceMap(
162
+ map: EventCallbackMap,
163
+ name: string,
164
+ callback: EventCallback | null,
165
+ offer: OfferFunction
166
+ ): { [name: string]: EventCallback } {
123
167
  if (callback) {
124
- const _once = (map[name] = once(function () {
168
+ const _once = once(function (this: any, ...args: any[]) {
125
169
  offer(name, _once);
126
- callback.apply(this, arguments);
127
- }));
170
+ callback.apply(this, args);
171
+ }) as EventCallback & { _callback?: EventCallback };
172
+ map[name] = _once;
128
173
  _once._callback = callback;
129
174
  }
130
175
  return map;
131
176
  }
132
177
 
133
178
  /** Handles triggering the appropriate event callbacks. */
134
- export function triggerApi(objEvents, name, callback, args) {
179
+ export function triggerApi(
180
+ objEvents: EventHandlersMap | null,
181
+ name: string,
182
+ _callback: EventCallback | null,
183
+ args: any[]
184
+ ): EventHandlersMap | null {
135
185
  if (objEvents) {
136
186
  const events = objEvents[name];
137
187
  let allEvents = objEvents.all;
@@ -147,13 +197,13 @@ export function triggerApi(objEvents, name, callback, args) {
147
197
  * triggering events. Tries to keep the usual cases speedy (most internal
148
198
  * Backbone events have 3 arguments).
149
199
  */
150
- function triggerEvents(events, args) {
151
- let ev,
152
- i = -1;
153
- const l = events.length,
154
- a1 = args[0],
155
- a2 = args[1],
156
- a3 = args[2];
200
+ function triggerEvents(events: EventHandler[], args: any[]): void {
201
+ let ev: EventHandler;
202
+ let i = -1;
203
+ const l = events.length;
204
+ const a1 = args[0];
205
+ const a2 = args[1];
206
+ const a3 = args[2];
157
207
  switch (args.length) {
158
208
  case 0:
159
209
  while (++i < l) (ev = events[i]).callback.call(ev.ctx);