@converse/skeletor 0.0.8 → 1.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.
- package/README.md +21 -92
- package/dist/skeletor.d.ts +764 -0
- package/dist/skeletor.js +10206 -6249
- package/dist/skeletor.min.js +72 -6
- package/package.json +59 -33
- package/src/collection.ts +846 -0
- package/src/drivers/sessionStorage.ts +297 -0
- package/src/eventemitter.ts +172 -0
- package/src/helpers.ts +179 -0
- package/src/index.ts +84 -0
- package/src/listening.ts +86 -0
- package/src/model.ts +520 -0
- package/src/storage.ts +293 -0
- package/src/types.ts +189 -0
- package/src/utils/events.ts +224 -0
- package/dist/skeletor.js.map +0 -1
- package/dist/skeletor.min.js.map +0 -1
- package/src/collection.js +0 -740
- package/src/drivers/sessionStorage.js +0 -306
- package/src/drivers/sessionStorage.js~ +0 -307
- package/src/element.js +0 -188
- package/src/events.js +0 -328
- package/src/helpers.js +0 -167
- package/src/history.js +0 -301
- package/src/main.js +0 -43
- package/src/model.js +0 -437
- package/src/overview.js +0 -191
- package/src/router.js +0 -119
- package/src/storage.js +0 -258
- package/src/view.js +0 -267
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// Copyright 2014 Mozilla
|
|
2
|
+
// Copyright 2015 Thodoris Greasidis
|
|
3
|
+
// Copyright 2018-2025 JC Brand
|
|
4
|
+
//
|
|
5
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
// you may not use this file except in compliance with the License.
|
|
7
|
+
// You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
// See the License for the specific language governing permissions and
|
|
15
|
+
// limitations under the License.
|
|
16
|
+
|
|
17
|
+
import executeCallback from 'localforage/src/utils/executeCallback';
|
|
18
|
+
import getCallback from 'localforage/src/utils/getCallback';
|
|
19
|
+
import normalizeKey from 'localforage/src/utils/normalizeKey';
|
|
20
|
+
import serializer from 'localforage/src/utils/serializer';
|
|
21
|
+
|
|
22
|
+
const serialize = serializer['serialize'];
|
|
23
|
+
const deserialize = serializer['deserialize'];
|
|
24
|
+
|
|
25
|
+
interface DbInfo {
|
|
26
|
+
keyPrefix: string;
|
|
27
|
+
serializer: {
|
|
28
|
+
serialize: typeof serialize;
|
|
29
|
+
deserialize: typeof deserialize;
|
|
30
|
+
};
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface LocalForageOptions {
|
|
35
|
+
name: string;
|
|
36
|
+
storeName: string;
|
|
37
|
+
[key: string]: any;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface IteratorCallback<T, U> {
|
|
41
|
+
(value: T, key: string, iterationNumber: number): U;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isSessionStorageValid(): boolean {
|
|
45
|
+
try {
|
|
46
|
+
if (sessionStorage && 'setItem' in sessionStorage) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.log(e);
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _getKeyPrefix(options: LocalForageOptions, defaultConfig: LocalForageOptions): string {
|
|
56
|
+
let keyPrefix = options.name + '/';
|
|
57
|
+
|
|
58
|
+
if (options.storeName !== defaultConfig.storeName) {
|
|
59
|
+
keyPrefix += options.storeName + '/';
|
|
60
|
+
}
|
|
61
|
+
return keyPrefix;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const dbInfo: DbInfo = {
|
|
65
|
+
keyPrefix: '',
|
|
66
|
+
serializer: {
|
|
67
|
+
serialize: serialize,
|
|
68
|
+
deserialize: deserialize,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function _initStorage(this: any, options: LocalForageOptions): void {
|
|
73
|
+
dbInfo.keyPrefix = _getKeyPrefix(options, this._defaultConfig);
|
|
74
|
+
if (options) {
|
|
75
|
+
for (const i in options) {
|
|
76
|
+
dbInfo[i] = options[i];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Remove all keys from the datastore, effectively destroying all data in
|
|
82
|
+
// the app's key/value store!
|
|
83
|
+
function clear(callback?: (err: any) => void): Promise<void> {
|
|
84
|
+
const promise = this.ready().then(function () {
|
|
85
|
+
const keyPrefix = dbInfo.keyPrefix;
|
|
86
|
+
|
|
87
|
+
for (let i = sessionStorage.length - 1; i >= 0; i--) {
|
|
88
|
+
const key = sessionStorage.key(i);
|
|
89
|
+
if (key && key.indexOf(keyPrefix) === 0) {
|
|
90
|
+
sessionStorage.removeItem(key);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
executeCallback(promise, callback);
|
|
96
|
+
return promise;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getItem<T>(key: string, callback?: (err: any, value: T | null) => void): Promise<T | null> {
|
|
100
|
+
key = normalizeKey(key);
|
|
101
|
+
|
|
102
|
+
const promise = this.ready().then(function (): T | null {
|
|
103
|
+
const result = sessionStorage.getItem(dbInfo.keyPrefix + key);
|
|
104
|
+
// If a result was found, parse it from the serialized
|
|
105
|
+
// string into a JS object. If result isn't truthy, the key
|
|
106
|
+
// is likely undefined and we'll pass it straight to the
|
|
107
|
+
// callback.
|
|
108
|
+
if (result) {
|
|
109
|
+
return dbInfo.serializer.deserialize(result) as T;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
executeCallback(promise, callback);
|
|
115
|
+
return promise;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function iterate<T, U>(
|
|
119
|
+
iterator: IteratorCallback<T, U>,
|
|
120
|
+
callback?: (err: any, result: U | undefined) => void
|
|
121
|
+
): Promise<U | undefined> {
|
|
122
|
+
const self = this;
|
|
123
|
+
|
|
124
|
+
const promise = self.ready().then(function (): U | undefined {
|
|
125
|
+
const keyPrefix = dbInfo.keyPrefix;
|
|
126
|
+
const keyPrefixLength = keyPrefix.length;
|
|
127
|
+
const length = sessionStorage.length;
|
|
128
|
+
let iterationNumber = 1;
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < length; i++) {
|
|
131
|
+
const key = sessionStorage.key(i);
|
|
132
|
+
if (!key || key.indexOf(keyPrefix) !== 0) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// If a result was found, parse it from the serialized
|
|
137
|
+
// string into a JS object. If result isn't truthy, the
|
|
138
|
+
// key is likely undefined and we'll pass it straight
|
|
139
|
+
// to the iterator.
|
|
140
|
+
const item = sessionStorage.getItem(key);
|
|
141
|
+
const value = item ? dbInfo.serializer.deserialize(item) : item;
|
|
142
|
+
|
|
143
|
+
const result = iterator(value as T, key.substring(keyPrefixLength), iterationNumber++);
|
|
144
|
+
if (result !== void 0) {
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
executeCallback(promise, callback);
|
|
152
|
+
return promise;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Same as sessionStorage's key() method, except takes a callback.
|
|
156
|
+
function key(n: number, callback?: (err: any, key: string | null) => void): Promise<string | null> {
|
|
157
|
+
const self = this;
|
|
158
|
+
const promise = self.ready().then(function (): string | null {
|
|
159
|
+
let result: string | null;
|
|
160
|
+
try {
|
|
161
|
+
result = sessionStorage.key(n);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
result = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Remove the prefix from the key, if a key is found.
|
|
167
|
+
if (result) {
|
|
168
|
+
result = result.substring(dbInfo.keyPrefix.length);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
executeCallback(promise, callback);
|
|
175
|
+
return promise;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function keys(callback?: (err: any, keys: string[]) => void): Promise<string[]> {
|
|
179
|
+
const self = this;
|
|
180
|
+
const promise = self.ready().then(function (): string[] {
|
|
181
|
+
const length = sessionStorage.length;
|
|
182
|
+
const keys: string[] = [];
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < length; i++) {
|
|
185
|
+
const itemKey = sessionStorage.key(i);
|
|
186
|
+
if (itemKey && itemKey.indexOf(dbInfo.keyPrefix) === 0) {
|
|
187
|
+
keys.push(itemKey.substring(dbInfo.keyPrefix.length));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return keys;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
executeCallback(promise, callback);
|
|
194
|
+
return promise;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Supply the number of keys in the datastore to the callback function.
|
|
198
|
+
function length(callback?: (err: any, numberOfKeys: number) => void): Promise<number> {
|
|
199
|
+
const self = this;
|
|
200
|
+
const promise = self.keys().then(function (keys: string[]) {
|
|
201
|
+
return keys.length;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
executeCallback(promise, callback);
|
|
205
|
+
return promise;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function removeItem(key: string, callback?: (err: any) => void): Promise<void> {
|
|
209
|
+
key = normalizeKey(key);
|
|
210
|
+
const promise = this.ready().then(function () {
|
|
211
|
+
sessionStorage.removeItem(dbInfo.keyPrefix + key);
|
|
212
|
+
});
|
|
213
|
+
executeCallback(promise, callback);
|
|
214
|
+
return promise;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function setItem<T>(key: string, value: T, callback?: (err: any, value: T) => void): Promise<T> {
|
|
218
|
+
key = normalizeKey(key);
|
|
219
|
+
await this.ready();
|
|
220
|
+
|
|
221
|
+
value = value ?? (null as unknown as T);
|
|
222
|
+
const originalValue = value;
|
|
223
|
+
|
|
224
|
+
return new Promise<T>((resolve, reject) => {
|
|
225
|
+
dbInfo.serializer.serialize(value, (serializedValue: string, error: any) => {
|
|
226
|
+
if (error) {
|
|
227
|
+
reject(error);
|
|
228
|
+
executeCallback(Promise.reject(error), callback);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
sessionStorage.setItem(dbInfo.keyPrefix + key, serializedValue);
|
|
234
|
+
resolve(originalValue);
|
|
235
|
+
executeCallback(Promise.resolve(originalValue), callback);
|
|
236
|
+
} catch (e: any) {
|
|
237
|
+
if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
|
|
238
|
+
console.error('Your sessionStorage capacity is used up.');
|
|
239
|
+
}
|
|
240
|
+
reject(e);
|
|
241
|
+
executeCallback(Promise.reject(e), callback);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function dropInstance(options: any, callback?: (err: any) => void): Promise<void> {
|
|
248
|
+
callback = getCallback.apply(this, arguments as any);
|
|
249
|
+
|
|
250
|
+
options = (typeof options !== 'function' && options) || {};
|
|
251
|
+
if (!options.name) {
|
|
252
|
+
const currentConfig = this.config();
|
|
253
|
+
options.name = options.name || currentConfig.name;
|
|
254
|
+
options.storeName = options.storeName || currentConfig.storeName;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const self = this;
|
|
258
|
+
let promise: Promise<void>;
|
|
259
|
+
if (!options.name) {
|
|
260
|
+
promise = Promise.reject(new Error('Invalid arguments'));
|
|
261
|
+
} else {
|
|
262
|
+
promise = new Promise<string>(function (resolve) {
|
|
263
|
+
if (!options.storeName) {
|
|
264
|
+
resolve(`${options.name}/`);
|
|
265
|
+
} else {
|
|
266
|
+
resolve(_getKeyPrefix(options, self._defaultConfig));
|
|
267
|
+
}
|
|
268
|
+
}).then(function (keyPrefix: string) {
|
|
269
|
+
for (let i = sessionStorage.length - 1; i >= 0; i--) {
|
|
270
|
+
const key = sessionStorage.key(i);
|
|
271
|
+
if (key && key.indexOf(keyPrefix) === 0) {
|
|
272
|
+
sessionStorage.removeItem(key);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
executeCallback(promise, callback);
|
|
279
|
+
return promise;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const sessionStorageWrapper = {
|
|
283
|
+
_driver: 'sessionStorageWrapper',
|
|
284
|
+
_initStorage: _initStorage,
|
|
285
|
+
_support: isSessionStorageValid(),
|
|
286
|
+
iterate,
|
|
287
|
+
getItem,
|
|
288
|
+
setItem,
|
|
289
|
+
removeItem,
|
|
290
|
+
clear,
|
|
291
|
+
length,
|
|
292
|
+
key,
|
|
293
|
+
keys,
|
|
294
|
+
dropInstance,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export default sessionStorageWrapper;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2010-2019 Jeremy Ashkenas and DocumentCloud
|
|
3
|
+
* @copyright 2023 JC Brand
|
|
4
|
+
*/
|
|
5
|
+
import isEmpty from 'lodash-es/isEmpty';
|
|
6
|
+
import keys from 'lodash-es/keys';
|
|
7
|
+
import uniqueId from 'lodash-es/uniqueId';
|
|
8
|
+
import Listening from './listening';
|
|
9
|
+
import { eventsApi, onApi, offApi, onceMap, tryCatchOn, triggerApi } from './utils/events';
|
|
10
|
+
import {
|
|
11
|
+
ClassConstructor,
|
|
12
|
+
EventCallback,
|
|
13
|
+
EventCallbackMap,
|
|
14
|
+
EventContext,
|
|
15
|
+
EventHandlersMap,
|
|
16
|
+
EventListenerMap,
|
|
17
|
+
ObjectListenedTo,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
// A private global variable to share between listeners and listenees.
|
|
21
|
+
let _listening: Listening | undefined;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
export function EventEmitter<T extends ClassConstructor>(Base: T) {
|
|
27
|
+
return class EventEmitter extends Base implements EventEmitter {
|
|
28
|
+
_events?: EventHandlersMap;
|
|
29
|
+
_listeners?: EventListenerMap;
|
|
30
|
+
_listeningTo?: EventListenerMap;
|
|
31
|
+
_listenId?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Bind an event to a `callback` function. Passing `"all"` will bind
|
|
35
|
+
* the callback to all events fired.
|
|
36
|
+
*/
|
|
37
|
+
on(name: string | EventCallbackMap, callback?: EventCallback | EventContext, context?: EventContext): this {
|
|
38
|
+
this._events = eventsApi(onApi, this._events || {}, name, callback, {
|
|
39
|
+
context: context,
|
|
40
|
+
ctx: this,
|
|
41
|
+
listening: _listening,
|
|
42
|
+
}) as EventHandlersMap;
|
|
43
|
+
|
|
44
|
+
if (_listening) {
|
|
45
|
+
const listeners = this._listeners || (this._listeners = {});
|
|
46
|
+
listeners[_listening.id] = _listening;
|
|
47
|
+
// Allow the listening to use a counter, instead of tracking
|
|
48
|
+
// callbacks for library interop
|
|
49
|
+
_listening.interop = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Inversion-of-control versions of `on`. Tell *this* object to listen to
|
|
57
|
+
* an event in another object... keeping track of what it's listening to
|
|
58
|
+
* for easier unbinding later.
|
|
59
|
+
*/
|
|
60
|
+
listenTo(obj: ObjectListenedTo, name: string | EventCallbackMap, callback?: EventCallback): this {
|
|
61
|
+
if (!obj) return this;
|
|
62
|
+
|
|
63
|
+
const id = obj._listenId || (obj._listenId = uniqueId('l'));
|
|
64
|
+
const listeningTo = this._listeningTo || (this._listeningTo = {});
|
|
65
|
+
let listening = (_listening = listeningTo[id]);
|
|
66
|
+
|
|
67
|
+
// This object is not listening to any other events on `obj` yet.
|
|
68
|
+
// Setup the necessary references to track the listening callbacks.
|
|
69
|
+
if (!listening) {
|
|
70
|
+
this._listenId || (this._listenId = uniqueId('l'));
|
|
71
|
+
listening = _listening = listeningTo[id] = new Listening(this, obj);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Bind callbacks on obj.
|
|
75
|
+
const error = tryCatchOn(obj, name, callback, this);
|
|
76
|
+
_listening = undefined;
|
|
77
|
+
|
|
78
|
+
if (error) throw error;
|
|
79
|
+
// If the target obj is not Backbone.Events, track events manually.
|
|
80
|
+
if (listening.interop) listening.start(name, callback, this, _listening);
|
|
81
|
+
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Remove one or many callbacks. If `context` is null, removes all
|
|
87
|
+
* callbacks with that function. If `callback` is null, removes all
|
|
88
|
+
* callbacks for the event. If `name` is null, removes all bound
|
|
89
|
+
* callbacks for all events.
|
|
90
|
+
*/
|
|
91
|
+
off(
|
|
92
|
+
name?: string | EventCallbackMap | null,
|
|
93
|
+
callback?: EventCallback | EventContext | null,
|
|
94
|
+
context?: EventContext
|
|
95
|
+
): this {
|
|
96
|
+
if (!this._events) return this;
|
|
97
|
+
this._events = eventsApi(offApi, this._events, name, callback, {
|
|
98
|
+
context: context,
|
|
99
|
+
listeners: this._listeners,
|
|
100
|
+
}) as EventHandlersMap;
|
|
101
|
+
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Tell this object to stop listening to either specific events ... or
|
|
107
|
+
* to every object it's currently listening to.
|
|
108
|
+
*/
|
|
109
|
+
stopListening(obj?: any, name?: string | EventCallbackMap, callback?: EventCallback): this {
|
|
110
|
+
const listeningTo = this._listeningTo;
|
|
111
|
+
if (!listeningTo) return this;
|
|
112
|
+
|
|
113
|
+
const ids = obj ? [obj._listenId] : keys(listeningTo);
|
|
114
|
+
for (let i = 0; i < ids.length; i++) {
|
|
115
|
+
const listening = listeningTo[ids[i]];
|
|
116
|
+
|
|
117
|
+
// If listening doesn't exist, this object is not currently
|
|
118
|
+
// listening to obj. Break out early.
|
|
119
|
+
if (!listening) break;
|
|
120
|
+
|
|
121
|
+
listening.obj.off(name, callback, this);
|
|
122
|
+
if (listening.interop) listening.stop(name, callback);
|
|
123
|
+
}
|
|
124
|
+
if (isEmpty(listeningTo)) this._listeningTo = undefined;
|
|
125
|
+
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Bind an event to only be triggered a single time. After the first time
|
|
131
|
+
* the callback is invoked, its listener will be removed. If multiple events
|
|
132
|
+
* are passed in using the space-separated syntax, the handler will fire
|
|
133
|
+
* once for each event, not once for a combination of all events.
|
|
134
|
+
*/
|
|
135
|
+
once(name: string | EventCallbackMap, callback?: EventCallback | EventContext, context?: EventContext): this {
|
|
136
|
+
// Map the event into a `{event: once}` object.
|
|
137
|
+
const events = eventsApi(onceMap, {}, name, callback, this.off.bind(this));
|
|
138
|
+
if (typeof name === 'string' && (context === null || context === undefined)) callback = undefined;
|
|
139
|
+
return this.on(events as EventCallbackMap, callback, context);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Inversion-of-control versions of `once`.
|
|
144
|
+
*/
|
|
145
|
+
listenToOnce(obj: any, name: string | EventCallbackMap, callback?: EventCallback): this {
|
|
146
|
+
// Map the event into a `{event: once}` object.
|
|
147
|
+
const events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj));
|
|
148
|
+
return this.listenTo(obj, events as string | EventCallbackMap);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Trigger one or many events, firing all bound callbacks. Callbacks are
|
|
153
|
+
* passed the same arguments as `trigger` is, apart from the event name
|
|
154
|
+
* (unless you're listening on `"all"`, which will cause your callback to
|
|
155
|
+
* receive the true name of the event as the first argument).
|
|
156
|
+
*/
|
|
157
|
+
trigger(name: string, ...args: any[]): this {
|
|
158
|
+
if (!this._events) return this;
|
|
159
|
+
|
|
160
|
+
eventsApi(triggerApi, this._events, name, undefined, args);
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @public
|
|
168
|
+
*/
|
|
169
|
+
const EventEmitterObject = EventEmitter(Object);
|
|
170
|
+
export { EventEmitterObject };
|
|
171
|
+
|
|
172
|
+
export default EventEmitter;
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// (c) 2010-2019 Jeremy Ashkenas and DocumentCloud
|
|
2
|
+
// (c) 2018-2025 JC Brand
|
|
3
|
+
import create from 'lodash-es/create';
|
|
4
|
+
import extend from 'lodash-es/extend';
|
|
5
|
+
import has from 'lodash-es/has';
|
|
6
|
+
import result from 'lodash-es/result';
|
|
7
|
+
import { Model } from './model';
|
|
8
|
+
import { Collection } from './collection';
|
|
9
|
+
import { type SyncOptions, SyncOperation } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Custom error for indicating timeouts
|
|
13
|
+
* @namespace _converse
|
|
14
|
+
*/
|
|
15
|
+
export class NotImplementedError extends Error {}
|
|
16
|
+
|
|
17
|
+
function S4(): string {
|
|
18
|
+
// Generate four random hex digits.
|
|
19
|
+
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function guid(): string {
|
|
23
|
+
// Generate a pseudo-GUID by concatenating random hexadecimal.
|
|
24
|
+
return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Helpers
|
|
28
|
+
// -------
|
|
29
|
+
|
|
30
|
+
// Helper function to correctly set up the prototype chain for subclasses.
|
|
31
|
+
// Similar to `goog.inherits`, but uses a hash of prototype properties and
|
|
32
|
+
// class properties to be extended.
|
|
33
|
+
//
|
|
34
|
+
export function inherits<T extends new (...args: any[]) => any>(
|
|
35
|
+
protoProps: Record<string, any> | null,
|
|
36
|
+
staticProps?: Record<string, any>
|
|
37
|
+
): T {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
39
|
+
const parent = this;
|
|
40
|
+
let child: T;
|
|
41
|
+
|
|
42
|
+
// The constructor function for the new subclass is either defined by you
|
|
43
|
+
// (the "constructor" property in your `extend` definition), or defaulted
|
|
44
|
+
// by us to simply call the parent constructor.
|
|
45
|
+
if (protoProps && has(protoProps, 'constructor')) {
|
|
46
|
+
child = protoProps.constructor;
|
|
47
|
+
} else {
|
|
48
|
+
child = function (this: any, ...args: any[]) {
|
|
49
|
+
return parent.apply(this, args);
|
|
50
|
+
} as unknown as T;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Add static properties to the constructor function, if supplied.
|
|
54
|
+
extend(child, parent, staticProps);
|
|
55
|
+
|
|
56
|
+
// Set the prototype chain to inherit from `parent`, without calling
|
|
57
|
+
// `parent`'s constructor function and add the prototype properties.
|
|
58
|
+
child.prototype = create(parent.prototype, protoProps);
|
|
59
|
+
child.prototype.constructor = child;
|
|
60
|
+
|
|
61
|
+
// Set a convenience property in case the parent's prototype is needed
|
|
62
|
+
// later.
|
|
63
|
+
(child as any).__super__ = parent.prototype;
|
|
64
|
+
|
|
65
|
+
return child;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface PromiseWrapper {
|
|
69
|
+
isResolved: boolean;
|
|
70
|
+
isPending: boolean;
|
|
71
|
+
isRejected: boolean;
|
|
72
|
+
resolve: ((value?: any) => void) | null;
|
|
73
|
+
reject: ((reason?: any) => void) | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type ResolveablePromise = Promise<any> & PromiseWrapper;
|
|
77
|
+
|
|
78
|
+
export function getResolveablePromise(): ResolveablePromise {
|
|
79
|
+
const wrapper: PromiseWrapper = {
|
|
80
|
+
isResolved: false,
|
|
81
|
+
isPending: true,
|
|
82
|
+
isRejected: false,
|
|
83
|
+
resolve: null,
|
|
84
|
+
reject: null,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const promise = new Promise((resolve, reject) => {
|
|
88
|
+
wrapper.resolve = resolve;
|
|
89
|
+
wrapper.reject = reject;
|
|
90
|
+
}) as ResolveablePromise;
|
|
91
|
+
|
|
92
|
+
Object.assign(promise, wrapper);
|
|
93
|
+
promise.then(
|
|
94
|
+
function (v) {
|
|
95
|
+
promise.isResolved = true;
|
|
96
|
+
promise.isPending = false;
|
|
97
|
+
promise.isRejected = false;
|
|
98
|
+
return v;
|
|
99
|
+
},
|
|
100
|
+
function (e) {
|
|
101
|
+
promise.isResolved = false;
|
|
102
|
+
promise.isPending = false;
|
|
103
|
+
promise.isRejected = true;
|
|
104
|
+
throw e;
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
return promise;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Throw an error when a URL is needed, and none is supplied.
|
|
111
|
+
export function urlError(): never {
|
|
112
|
+
throw new Error('A "url" property or function must be specified');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Wrap an optional error callback with a fallback error event.
|
|
116
|
+
export function wrapError(model: Model<any> | Collection<any>, options: any): void {
|
|
117
|
+
const error = options.error;
|
|
118
|
+
options.error = function (resp: any) {
|
|
119
|
+
if (error) error.call(options.context, model, resp, options);
|
|
120
|
+
model.trigger('error', model, resp, options);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Map from CRUD to HTTP for our default `sync` implementation.
|
|
125
|
+
const methodMap: Record<string, string> = {
|
|
126
|
+
create: 'POST',
|
|
127
|
+
update: 'PUT',
|
|
128
|
+
patch: 'PATCH',
|
|
129
|
+
delete: 'DELETE',
|
|
130
|
+
read: 'GET',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export function getSyncMethod(model: Model | Collection<any>): typeof sync & { __name__?: string } {
|
|
134
|
+
const store = result(model, 'browserStorage') || result((model as Model).collection, 'browserStorage');
|
|
135
|
+
return store ? (store as any).sync() : sync;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Override this function to change the manner in which Backbone persists
|
|
140
|
+
* models to the server. You will be passed the type of request, and the
|
|
141
|
+
* model in question. By default makes a `fetch()` API call
|
|
142
|
+
* to the model's `url()`.
|
|
143
|
+
*
|
|
144
|
+
* Some possible customizations could be:
|
|
145
|
+
*
|
|
146
|
+
* - Use `setTimeout` to batch rapid-fire updates into a single request.
|
|
147
|
+
* - Persist models via WebSockets instead of Ajax.
|
|
148
|
+
* - Persist models to browser storage
|
|
149
|
+
*/
|
|
150
|
+
/**
|
|
151
|
+
* @public
|
|
152
|
+
*/
|
|
153
|
+
export function sync(method: SyncOperation, model: Model | Collection<any>, options: SyncOptions = {}): Promise<any> {
|
|
154
|
+
let data = options.data;
|
|
155
|
+
|
|
156
|
+
// Ensure that we have the appropriate request data.
|
|
157
|
+
if (data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
|
|
158
|
+
data = options.attrs || model.toJSON();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const type = methodMap[method];
|
|
162
|
+
const params: RequestInit & { success?: any; error?: any } = {
|
|
163
|
+
method: type,
|
|
164
|
+
body: data ? JSON.stringify(data) : '',
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
},
|
|
168
|
+
success: options.success,
|
|
169
|
+
error: options.error,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const url = options.url || result(model, 'url') || urlError();
|
|
173
|
+
const xhr = fetch(url, params);
|
|
174
|
+
if (options) {
|
|
175
|
+
options.xhr = xhr;
|
|
176
|
+
}
|
|
177
|
+
model.trigger('request', model, xhr, { ...params, xhr });
|
|
178
|
+
return xhr;
|
|
179
|
+
}
|