@logimaxx/kviews.js 1.2.3
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/CHANGELOG.md +60 -0
- package/CODE_OF_CONDUCT.md +133 -0
- package/CONTRIBUTING.md +50 -0
- package/LICENSE +21 -0
- package/README.md +399 -0
- package/SECURITY.md +13 -0
- package/SUPPORT.md +16 -0
- package/dist/index.js +4039 -0
- package/dist/index.js.map +7 -0
- package/dist/kviews.js +4068 -0
- package/dist/kviews.js.map +7 -0
- package/dist/kviews.min.js +11 -0
- package/package.json +80 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4039 @@
|
|
|
1
|
+
// src/utils.js
|
|
2
|
+
function dbg() {
|
|
3
|
+
if (typeof kviewsLogLevel !== "undefined" && kviewsLogLevel >= 3) {
|
|
4
|
+
console.trace(...arguments);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
function log() {
|
|
8
|
+
if (typeof kviewsLogLevel !== "undefined" && kviewsLogLevel >= 2) {
|
|
9
|
+
console.log(...arguments);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function error() {
|
|
13
|
+
if (typeof kviewsLogLevel !== "undefined" && kviewsLogLevel >= 1) {
|
|
14
|
+
console.error(...arguments);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function trace() {
|
|
18
|
+
if (typeof kviewsLogLevel !== "undefined" && kviewsLogLevel >= 4) {
|
|
19
|
+
console.trace(...arguments);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function uid() {
|
|
23
|
+
return "uid_" + Math.random().toString(36).substr(2, 9);
|
|
24
|
+
}
|
|
25
|
+
function parseOptions(options) {
|
|
26
|
+
if (typeof options === "undefined") {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
if (options.constructor === Object) {
|
|
30
|
+
return options;
|
|
31
|
+
}
|
|
32
|
+
throw new Error("Invalid options", options);
|
|
33
|
+
}
|
|
34
|
+
function deepmerge(target, source, optionsArgument) {
|
|
35
|
+
function defaultArrayMerge(target2, source2, optionsArgument2) {
|
|
36
|
+
let destination = target2.slice();
|
|
37
|
+
source2.forEach(function(e, i) {
|
|
38
|
+
if (typeof destination[i] === "undefined") {
|
|
39
|
+
destination[i] = cloneIfNecessary(e, optionsArgument2);
|
|
40
|
+
} else if (isMergeableObject(e)) {
|
|
41
|
+
destination[i] = deepmerge(target2[i], e, optionsArgument2);
|
|
42
|
+
} else if (target2.indexOf(e) === -1) {
|
|
43
|
+
destination.push(cloneIfNecessary(e, optionsArgument2));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return destination;
|
|
47
|
+
}
|
|
48
|
+
function isMergeableObject(val) {
|
|
49
|
+
var nonNullObject = val && typeof val === "object";
|
|
50
|
+
return nonNullObject && Object.prototype.toString.call(val) !== "[object RegExp]" && Object.prototype.toString.call(val) !== "[object Date]";
|
|
51
|
+
}
|
|
52
|
+
function emptyTarget(val) {
|
|
53
|
+
return Array.isArray(val) ? [] : {};
|
|
54
|
+
}
|
|
55
|
+
function cloneIfNecessary(value, optionsArgument2) {
|
|
56
|
+
let clone = optionsArgument2 && optionsArgument2.clone === true;
|
|
57
|
+
return clone && isMergeableObject(value) ? deepmerge(emptyTarget(value), value, optionsArgument2) : value;
|
|
58
|
+
}
|
|
59
|
+
function mergeObject(target2, source2, optionsArgument2) {
|
|
60
|
+
let destination = {};
|
|
61
|
+
if (isMergeableObject(target2)) {
|
|
62
|
+
Object.keys(target2).forEach(function(key) {
|
|
63
|
+
destination[key] = cloneIfNecessary(target2[key], optionsArgument2);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
Object.keys(source2).forEach(function(key) {
|
|
67
|
+
if (!isMergeableObject(source2[key]) || !target2[key]) {
|
|
68
|
+
destination[key] = cloneIfNecessary(source2[key], optionsArgument2);
|
|
69
|
+
} else {
|
|
70
|
+
destination[key] = deepmerge(target2[key], source2[key], optionsArgument2);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return destination;
|
|
74
|
+
}
|
|
75
|
+
let array = Array.isArray(source);
|
|
76
|
+
let options = optionsArgument || { arrayMerge: defaultArrayMerge };
|
|
77
|
+
let arrayMerge = options.arrayMerge || defaultArrayMerge;
|
|
78
|
+
if (array) {
|
|
79
|
+
return Array.isArray(target) ? arrayMerge(target, source, optionsArgument) : cloneIfNecessary(source, optionsArgument);
|
|
80
|
+
} else {
|
|
81
|
+
return mergeObject(target, source, optionsArgument);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
deepmerge.all = function deepmergeAll(array, optionsArgument) {
|
|
85
|
+
if (!Array.isArray(array) || array.length < 2) {
|
|
86
|
+
throw new Error("first argument should be an array with at least two elements");
|
|
87
|
+
}
|
|
88
|
+
return array.reduce(function(prev, next) {
|
|
89
|
+
return deepmerge(prev, next, optionsArgument);
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
function getBoundObjects(el) {
|
|
93
|
+
let db = {};
|
|
94
|
+
if (!el || $(el).length === 0) {
|
|
95
|
+
return db;
|
|
96
|
+
}
|
|
97
|
+
let boundData = $(el).data();
|
|
98
|
+
for (let key in boundData) {
|
|
99
|
+
if (typeof boundData[key] === "object" && key !== "instance") {
|
|
100
|
+
db[key] = boundData[key];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return db;
|
|
104
|
+
}
|
|
105
|
+
function template(text) {
|
|
106
|
+
if (typeof Handlebars !== "undefined") {
|
|
107
|
+
return Handlebars.compile(text);
|
|
108
|
+
}
|
|
109
|
+
throw new Error("Handlebars is required for template compilation");
|
|
110
|
+
}
|
|
111
|
+
function createOverlay(instance) {
|
|
112
|
+
return $(document.createElement("div")).text("Se incarca 123").addClass("komponent-overlay").data("asd", instance).attr(
|
|
113
|
+
"style",
|
|
114
|
+
"background: linear-gradient(135deg,rgb(191, 225, 205),rgb(236, 234, 232) 70%, #fca); text-align: center; position:absolute; z-index:100000;"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/apiBase.js
|
|
119
|
+
var apiBaseConfig = {
|
|
120
|
+
baseUrl: null,
|
|
121
|
+
basePath: null,
|
|
122
|
+
defaultHeaders: {}
|
|
123
|
+
};
|
|
124
|
+
function resolveRequestUrl(url) {
|
|
125
|
+
if (url == null || url === "") {
|
|
126
|
+
return url;
|
|
127
|
+
}
|
|
128
|
+
const s = typeof url === "string" ? url : String(url);
|
|
129
|
+
if (/^https?:\/\//i.test(s) || s.startsWith("//")) {
|
|
130
|
+
return s;
|
|
131
|
+
}
|
|
132
|
+
const base = apiBaseConfig.baseUrl || apiBaseConfig.basePath || "";
|
|
133
|
+
if (!base) {
|
|
134
|
+
return s;
|
|
135
|
+
}
|
|
136
|
+
const baseNorm = base.replace(/\/+$/, "");
|
|
137
|
+
const pathNorm = s.replace(/^\/+/, "");
|
|
138
|
+
if (!pathNorm) {
|
|
139
|
+
return baseNorm + "/";
|
|
140
|
+
}
|
|
141
|
+
return baseNorm + "/" + pathNorm;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/errors.js
|
|
145
|
+
var KViewsError = class extends Error {
|
|
146
|
+
constructor(message, options = {}) {
|
|
147
|
+
super(message);
|
|
148
|
+
this.name = "KViewsError";
|
|
149
|
+
this.options = options.options || {};
|
|
150
|
+
this.context = options.context || null;
|
|
151
|
+
if (Error.captureStackTrace) {
|
|
152
|
+
Error.captureStackTrace(this, this.constructor);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
var KViewsHttpError = class extends KViewsError {
|
|
157
|
+
constructor(message, options = {}) {
|
|
158
|
+
super(message, options);
|
|
159
|
+
this.name = "KViewsHttpError";
|
|
160
|
+
this.status = options.status || 0;
|
|
161
|
+
this.statusText = options.statusText || "error";
|
|
162
|
+
this.responseText = options.responseText || null;
|
|
163
|
+
this.responseJSON = options.responseJSON || null;
|
|
164
|
+
this.jqXHR = options.jqXHR || null;
|
|
165
|
+
this.textStatus = options.textStatus || "error";
|
|
166
|
+
this.errorThrown = options.errorThrown || null;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var KViewsParseError = class extends KViewsError {
|
|
170
|
+
constructor(message, options = {}) {
|
|
171
|
+
super(message, options);
|
|
172
|
+
this.name = "KViewsParseError";
|
|
173
|
+
this.rawData = options.rawData || null;
|
|
174
|
+
this.parseStep = options.parseStep || null;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
var KViewsUrlError = class extends KViewsError {
|
|
178
|
+
constructor(message, options = {}) {
|
|
179
|
+
super(message, options);
|
|
180
|
+
this.name = "KViewsUrlError";
|
|
181
|
+
this.url = options.url || null;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var KViewsNetworkError = class extends KViewsError {
|
|
185
|
+
constructor(message, options = {}) {
|
|
186
|
+
super(message, options);
|
|
187
|
+
this.name = "KViewsNetworkError";
|
|
188
|
+
this.originalError = options.originalError || null;
|
|
189
|
+
this.url = options.url || null;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/URL.js
|
|
194
|
+
var URL = class {
|
|
195
|
+
constructor(url) {
|
|
196
|
+
if (!url) {
|
|
197
|
+
throw new KViewsUrlError("URL is not provided", { url });
|
|
198
|
+
}
|
|
199
|
+
if (typeof url === "object" && url.hasOwnProperty("protocol")) {
|
|
200
|
+
Object.assign(this, url);
|
|
201
|
+
if (this.parameters && !this.parameters.toString) {
|
|
202
|
+
this._addParametersToString();
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (url.constructor !== String) {
|
|
207
|
+
dbg("URL is not a string", url);
|
|
208
|
+
throw new KViewsUrlError("URL is not a string: " + url.toString(), { url });
|
|
209
|
+
}
|
|
210
|
+
const isAbsolute = /^[a-z]+:\/\//i.test(url);
|
|
211
|
+
if (isAbsolute && typeof window !== "undefined" && window.URL) {
|
|
212
|
+
try {
|
|
213
|
+
const standardUrl = new window.URL(url);
|
|
214
|
+
this.protocol = standardUrl.protocol ? standardUrl.protocol.replace(":", "") : null;
|
|
215
|
+
this.fqdn = standardUrl.hostname || null;
|
|
216
|
+
this.port = standardUrl.port || null;
|
|
217
|
+
this.path = standardUrl.pathname || null;
|
|
218
|
+
this.fragment = standardUrl.hash ? standardUrl.hash.replace("#", "") : null;
|
|
219
|
+
this.parameters = {};
|
|
220
|
+
if (standardUrl.search) {
|
|
221
|
+
const params = new URLSearchParams(standardUrl.search);
|
|
222
|
+
params.forEach((value, key) => {
|
|
223
|
+
this.parameters[key] = value;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
} catch (e) {
|
|
227
|
+
this._parseRelativeUrl(url);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
this._parseRelativeUrl(url);
|
|
231
|
+
}
|
|
232
|
+
this._addParametersToString();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Parse relative URL using regex fallback
|
|
236
|
+
* @private
|
|
237
|
+
*/
|
|
238
|
+
_parseRelativeUrl(url) {
|
|
239
|
+
let regExp = /^((?:([a-z]+):)([\/]{2,3})([\w\.\-\_]+)(?::(\d+))?)?(?:(\/?[^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/i;
|
|
240
|
+
let parts = regExp.exec(url);
|
|
241
|
+
this.protocol = null;
|
|
242
|
+
this.fqdn = null;
|
|
243
|
+
this.port = null;
|
|
244
|
+
this.path = null;
|
|
245
|
+
this.parameters = {};
|
|
246
|
+
this.fragment = null;
|
|
247
|
+
if (typeof parts[2] !== "undefined") {
|
|
248
|
+
this.protocol = parts[2];
|
|
249
|
+
}
|
|
250
|
+
if (typeof parts[4] !== "undefined") {
|
|
251
|
+
this.fqdn = parts[4];
|
|
252
|
+
}
|
|
253
|
+
if (typeof parts[5] !== "undefined") {
|
|
254
|
+
this.port = parts[5];
|
|
255
|
+
}
|
|
256
|
+
if (typeof parts[6] !== "undefined") {
|
|
257
|
+
this.path = parts[6];
|
|
258
|
+
}
|
|
259
|
+
if (typeof parts[7] !== "undefined") {
|
|
260
|
+
try {
|
|
261
|
+
const params = new URLSearchParams(parts[7]);
|
|
262
|
+
params.forEach((value, key) => {
|
|
263
|
+
this.parameters[key] = value;
|
|
264
|
+
});
|
|
265
|
+
} catch (e) {
|
|
266
|
+
let tmp = parts[7].split("&");
|
|
267
|
+
tmp.forEach((item) => {
|
|
268
|
+
if (!item || item === "") {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
let eqPos = item.indexOf("=");
|
|
272
|
+
if (eqPos === -1) {
|
|
273
|
+
this.parameters[item] = "";
|
|
274
|
+
} else {
|
|
275
|
+
this.parameters[item.substr(0, eqPos)] = decodeURIComponent(item.substr(eqPos + 1));
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (typeof parts[8] !== "undefined") {
|
|
281
|
+
this.fragment = parts[8];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Add toString method to parameters object
|
|
286
|
+
* @private
|
|
287
|
+
*/
|
|
288
|
+
_addParametersToString() {
|
|
289
|
+
if (!this.parameters.toString || this.parameters.toString === Object.prototype.toString) {
|
|
290
|
+
const self = this;
|
|
291
|
+
this.parameters.toString = function() {
|
|
292
|
+
if (typeof URLSearchParams !== "undefined") {
|
|
293
|
+
try {
|
|
294
|
+
const params = new URLSearchParams();
|
|
295
|
+
for (let para in this) {
|
|
296
|
+
if (this.hasOwnProperty(para) && para !== "toString") {
|
|
297
|
+
params.append(para, String(this[para]));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return params.toString();
|
|
301
|
+
} catch (e) {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
let paras = [];
|
|
305
|
+
for (let para in this) {
|
|
306
|
+
if (this.hasOwnProperty(para) && para !== "toString") {
|
|
307
|
+
const value = String(this[para]);
|
|
308
|
+
paras.push(encodeURIComponent(para) + "=" + encodeURIComponent(value));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return paras.join("&");
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
toString() {
|
|
316
|
+
let str = "";
|
|
317
|
+
if (this.protocol && this.fqdn) {
|
|
318
|
+
if (typeof window !== "undefined" && window.URL) {
|
|
319
|
+
try {
|
|
320
|
+
const baseUrl = this.protocol + "://" + this.fqdn + (this.port ? ":" + this.port : "");
|
|
321
|
+
const url = new window.URL(this.path || "/", baseUrl);
|
|
322
|
+
if (this.parameters && Object.keys(this.parameters).length > 0) {
|
|
323
|
+
Object.getOwnPropertyNames(this.parameters).forEach((key) => {
|
|
324
|
+
if (key !== "toString") {
|
|
325
|
+
url.searchParams.set(key, this.parameters[key]);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (this.fragment) {
|
|
330
|
+
url.hash = this.fragment;
|
|
331
|
+
}
|
|
332
|
+
return url.toString();
|
|
333
|
+
} catch (e) {
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
str += this.protocol + "://" + this.fqdn;
|
|
337
|
+
if (this.port) {
|
|
338
|
+
str += ":" + this.port;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (this.path) {
|
|
342
|
+
str += this.path;
|
|
343
|
+
}
|
|
344
|
+
if (this.parameters && Object.keys(this.parameters).length > 0) {
|
|
345
|
+
str += "?" + this.parameters.toString();
|
|
346
|
+
}
|
|
347
|
+
if (this.fragment) {
|
|
348
|
+
str += "#" + this.fragment;
|
|
349
|
+
}
|
|
350
|
+
return str;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
function createURL(url) {
|
|
354
|
+
return new URL(url);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/Storage.js
|
|
358
|
+
var Storage = class {
|
|
359
|
+
constructor(options = {}) {
|
|
360
|
+
let defaultOptions = {
|
|
361
|
+
url: null,
|
|
362
|
+
method: "GET"
|
|
363
|
+
};
|
|
364
|
+
options = parseOptions(options);
|
|
365
|
+
Object.assign(defaultOptions, options);
|
|
366
|
+
this.defaultOptions = defaultOptions;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Base sync method - uses Fetch API
|
|
370
|
+
*/
|
|
371
|
+
sync(options) {
|
|
372
|
+
if (options.url && typeof options.url === "object" && options.url.toString) {
|
|
373
|
+
options.url = options.url.toString();
|
|
374
|
+
}
|
|
375
|
+
options.url = resolveRequestUrl(options.url);
|
|
376
|
+
options = Object.assign(
|
|
377
|
+
Object.assign({}, this.defaultOptions),
|
|
378
|
+
parseOptions(options)
|
|
379
|
+
);
|
|
380
|
+
const globalHeaders = apiBaseConfig.defaultHeaders && typeof apiBaseConfig.defaultHeaders === "object" ? apiBaseConfig.defaultHeaders : {};
|
|
381
|
+
const defaultHeaders = this.defaultOptions.headers && typeof this.defaultOptions.headers === "object" ? this.defaultOptions.headers : {};
|
|
382
|
+
const requestHeaders = options.headers && typeof options.headers === "object" ? options.headers : {};
|
|
383
|
+
options.headers = Object.assign({}, globalHeaders, defaultHeaders, requestHeaders);
|
|
384
|
+
if (!options.hasOwnProperty("url")) {
|
|
385
|
+
throw new Error("No URL provided");
|
|
386
|
+
}
|
|
387
|
+
if (options.url && typeof options.url === "object" && options.url.toString) {
|
|
388
|
+
options.url = options.url.toString();
|
|
389
|
+
}
|
|
390
|
+
const fetchOptions = {
|
|
391
|
+
method: options.method || "GET",
|
|
392
|
+
headers: {}
|
|
393
|
+
};
|
|
394
|
+
if (options.headers) {
|
|
395
|
+
Object.assign(fetchOptions.headers, options.headers);
|
|
396
|
+
}
|
|
397
|
+
if (options.contentType) {
|
|
398
|
+
fetchOptions.headers["Content-Type"] = options.contentType;
|
|
399
|
+
}
|
|
400
|
+
if (options.data && ["POST", "PUT", "PATCH"].includes(fetchOptions.method)) {
|
|
401
|
+
fetchOptions.body = options.data;
|
|
402
|
+
}
|
|
403
|
+
return fetch(options.url, fetchOptions).catch((fetchError) => {
|
|
404
|
+
throw new KViewsNetworkError(
|
|
405
|
+
fetchError instanceof Error ? fetchError.message : String(fetchError),
|
|
406
|
+
{
|
|
407
|
+
originalError: fetchError instanceof Error ? fetchError : new Error(String(fetchError)),
|
|
408
|
+
url: options.url,
|
|
409
|
+
options
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
}).then(async (response) => {
|
|
413
|
+
const jqXHR = {
|
|
414
|
+
status: response.status,
|
|
415
|
+
statusText: response.statusText,
|
|
416
|
+
responseText: null,
|
|
417
|
+
responseJSON: null,
|
|
418
|
+
getAllResponseHeaders: () => {
|
|
419
|
+
const headers = {};
|
|
420
|
+
response.headers.forEach((value, key) => {
|
|
421
|
+
headers[key] = value;
|
|
422
|
+
});
|
|
423
|
+
return Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join("\r\n");
|
|
424
|
+
},
|
|
425
|
+
getResponseHeader: (name) => {
|
|
426
|
+
return response.headers.get(name);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const text = await response.text();
|
|
430
|
+
let data = text;
|
|
431
|
+
try {
|
|
432
|
+
data = JSON.parse(text);
|
|
433
|
+
} catch (e) {
|
|
434
|
+
}
|
|
435
|
+
jqXHR.responseText = text;
|
|
436
|
+
jqXHR.responseJSON = typeof data === "object" ? data : null;
|
|
437
|
+
if (!response.ok) {
|
|
438
|
+
const error2 = new KViewsHttpError(
|
|
439
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
440
|
+
{
|
|
441
|
+
status: response.status,
|
|
442
|
+
statusText: response.statusText,
|
|
443
|
+
responseText: text,
|
|
444
|
+
responseJSON: typeof data === "object" ? data : null,
|
|
445
|
+
jqXHR,
|
|
446
|
+
options,
|
|
447
|
+
errorThrown: new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
error2.textStatus = "error";
|
|
451
|
+
throw error2;
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
data,
|
|
455
|
+
textStatus: "success",
|
|
456
|
+
jqXHR
|
|
457
|
+
};
|
|
458
|
+
}).catch((error2) => {
|
|
459
|
+
if (error2 instanceof KViewsHttpError || error2 instanceof KViewsNetworkError) {
|
|
460
|
+
throw error2;
|
|
461
|
+
}
|
|
462
|
+
const jqXHR = {
|
|
463
|
+
status: 0,
|
|
464
|
+
statusText: "error",
|
|
465
|
+
responseText: null,
|
|
466
|
+
responseJSON: null,
|
|
467
|
+
getAllResponseHeaders: () => "",
|
|
468
|
+
getResponseHeader: () => null
|
|
469
|
+
};
|
|
470
|
+
const httpError = new KViewsHttpError(
|
|
471
|
+
error2 instanceof Error ? error2.message : String(error2),
|
|
472
|
+
{
|
|
473
|
+
status: 0,
|
|
474
|
+
statusText: "error",
|
|
475
|
+
responseText: null,
|
|
476
|
+
responseJSON: null,
|
|
477
|
+
jqXHR,
|
|
478
|
+
options,
|
|
479
|
+
errorThrown: error2 instanceof Error ? error2 : new Error(String(error2))
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
httpError.textStatus = "error";
|
|
483
|
+
throw httpError;
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Create (POST) operation
|
|
488
|
+
*/
|
|
489
|
+
create(ctx, url, opts, data) {
|
|
490
|
+
let options = {
|
|
491
|
+
context: ctx,
|
|
492
|
+
url,
|
|
493
|
+
method: "POST",
|
|
494
|
+
data
|
|
495
|
+
};
|
|
496
|
+
Object.assign(options, opts);
|
|
497
|
+
return this.sync(options);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Read (GET) operation
|
|
501
|
+
*/
|
|
502
|
+
read(ctx, url, opts) {
|
|
503
|
+
let options = {
|
|
504
|
+
context: ctx,
|
|
505
|
+
url,
|
|
506
|
+
method: "GET"
|
|
507
|
+
};
|
|
508
|
+
Object.assign(options, opts);
|
|
509
|
+
return this.sync(options);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Delete (DELETE) operation
|
|
513
|
+
*/
|
|
514
|
+
delete(ctx, url, opts) {
|
|
515
|
+
let options = {
|
|
516
|
+
context: ctx,
|
|
517
|
+
url,
|
|
518
|
+
method: "DELETE"
|
|
519
|
+
};
|
|
520
|
+
Object.assign(options, opts);
|
|
521
|
+
return this.sync(options);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Update (PATCH) operation
|
|
525
|
+
*/
|
|
526
|
+
update(ctx, url, opts, data) {
|
|
527
|
+
let options = {
|
|
528
|
+
context: ctx,
|
|
529
|
+
url,
|
|
530
|
+
method: "PATCH",
|
|
531
|
+
contentType: "application/vnd.api+json",
|
|
532
|
+
data
|
|
533
|
+
};
|
|
534
|
+
Object.assign(options, opts);
|
|
535
|
+
return this.sync(options);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/dataParser.js
|
|
540
|
+
function getIncludedResources(doc) {
|
|
541
|
+
if (!doc || typeof doc !== "object") {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
const includedData = doc.hasOwnProperty("included") ? doc.included : doc.hasOwnProperty("includes") ? doc.includes : null;
|
|
545
|
+
if (!includedData) {
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
if (!Array.isArray(includedData)) {
|
|
549
|
+
return [];
|
|
550
|
+
}
|
|
551
|
+
return includedData;
|
|
552
|
+
}
|
|
553
|
+
function buildResourceIndex(doc) {
|
|
554
|
+
const index = /* @__PURE__ */ new Map();
|
|
555
|
+
if (!doc || typeof doc !== "object") {
|
|
556
|
+
return index;
|
|
557
|
+
}
|
|
558
|
+
function indexResource(resource) {
|
|
559
|
+
if (!resource || typeof resource !== "object") {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (!resource.type || !resource.id) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const key = `${resource.type}/${resource.id}`;
|
|
566
|
+
if (!index.has(key)) {
|
|
567
|
+
index.set(key, resource);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (doc.data) {
|
|
571
|
+
if (Array.isArray(doc.data)) {
|
|
572
|
+
doc.data.forEach(indexResource);
|
|
573
|
+
} else if (typeof doc.data === "object") {
|
|
574
|
+
indexResource(doc.data);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const included = getIncludedResources(doc);
|
|
578
|
+
included.forEach(indexResource);
|
|
579
|
+
return index;
|
|
580
|
+
}
|
|
581
|
+
function getResourceFromIndex(typeOrRef, id, resourceIndex) {
|
|
582
|
+
let type, resourceId;
|
|
583
|
+
if (typeof typeOrRef === "object" && typeOrRef !== null) {
|
|
584
|
+
type = typeOrRef.type;
|
|
585
|
+
resourceId = typeOrRef.id;
|
|
586
|
+
} else {
|
|
587
|
+
type = typeOrRef;
|
|
588
|
+
resourceId = id;
|
|
589
|
+
}
|
|
590
|
+
if (!type || !resourceId) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
const key = `${type}/${resourceId}`;
|
|
594
|
+
return resourceIndex.get(key) || null;
|
|
595
|
+
}
|
|
596
|
+
function hydrateResource(resource, resourceIndex, visited = /* @__PURE__ */ new Set()) {
|
|
597
|
+
if (!resource || typeof resource !== "object") {
|
|
598
|
+
return resource;
|
|
599
|
+
}
|
|
600
|
+
if (!resource.relationships) {
|
|
601
|
+
return resource;
|
|
602
|
+
}
|
|
603
|
+
const resourceKey = resource.type && resource.id ? `${resource.type}/${resource.id}` : null;
|
|
604
|
+
if (resourceKey && visited.has(resourceKey)) {
|
|
605
|
+
return resource;
|
|
606
|
+
}
|
|
607
|
+
if (resourceKey) {
|
|
608
|
+
visited.add(resourceKey);
|
|
609
|
+
}
|
|
610
|
+
Object.keys(resource.relationships).forEach((relName) => {
|
|
611
|
+
const rel = resource.relationships[relName];
|
|
612
|
+
if (rel === null) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (rel.data !== void 0) {
|
|
616
|
+
if (rel.data === null) {
|
|
617
|
+
resource.relationships[relName] = null;
|
|
618
|
+
} else if (Array.isArray(rel.data)) {
|
|
619
|
+
resource.relationships[relName] = rel.data.map((ref) => {
|
|
620
|
+
const hydrated = getResourceFromIndex(ref, null, resourceIndex);
|
|
621
|
+
if (hydrated) {
|
|
622
|
+
return hydrateResource(
|
|
623
|
+
hydrated,
|
|
624
|
+
// Use same object instance from index
|
|
625
|
+
resourceIndex,
|
|
626
|
+
new Set(visited)
|
|
627
|
+
// New visited set for each branch
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
return ref;
|
|
631
|
+
}).filter((r) => r !== null);
|
|
632
|
+
} else if (typeof rel.data === "object" && rel.data.type && rel.data.id) {
|
|
633
|
+
const hydrated = getResourceFromIndex(rel.data, null, resourceIndex);
|
|
634
|
+
if (hydrated) {
|
|
635
|
+
resource.relationships[relName] = hydrateResource(
|
|
636
|
+
hydrated,
|
|
637
|
+
// Use same object instance from index
|
|
638
|
+
resourceIndex,
|
|
639
|
+
new Set(visited)
|
|
640
|
+
// New visited set for each branch
|
|
641
|
+
);
|
|
642
|
+
} else {
|
|
643
|
+
resource.relationships[relName] = rel.data;
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
resource.relationships[relName] = rel.data;
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
resource.relationships[relName] = rel;
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
return resource;
|
|
653
|
+
}
|
|
654
|
+
function hydrateDocumentData(doc) {
|
|
655
|
+
if (!doc || typeof doc !== "object") {
|
|
656
|
+
throw new KViewsParseError("Invalid document: must be an object");
|
|
657
|
+
}
|
|
658
|
+
const resourceIndex = buildResourceIndex(doc);
|
|
659
|
+
if (!doc.data) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
const data = doc.data;
|
|
663
|
+
if (Array.isArray(data)) {
|
|
664
|
+
data.forEach((resource) => hydrateResource(resource, resourceIndex));
|
|
665
|
+
return data;
|
|
666
|
+
} else if (typeof data === "object") {
|
|
667
|
+
return hydrateResource(data, resourceIndex);
|
|
668
|
+
}
|
|
669
|
+
return data;
|
|
670
|
+
}
|
|
671
|
+
function parseItemData(data, options = {}) {
|
|
672
|
+
let hydratedResource;
|
|
673
|
+
let doc = data;
|
|
674
|
+
if (data && typeof data === "object" && data.type && data.id && !data.data) {
|
|
675
|
+
hydratedResource = data;
|
|
676
|
+
} else if (data && data.data) {
|
|
677
|
+
doc = data;
|
|
678
|
+
hydratedResource = hydrateDocumentData(doc);
|
|
679
|
+
} else {
|
|
680
|
+
hydratedResource = data;
|
|
681
|
+
}
|
|
682
|
+
if (!hydratedResource || typeof hydratedResource !== "object") {
|
|
683
|
+
throw new KViewsParseError("Invalid item data: must be an object");
|
|
684
|
+
}
|
|
685
|
+
if (doc && doc.links && doc.links.self && !hydratedResource.url) {
|
|
686
|
+
hydratedResource.url = createURL(doc.links.self);
|
|
687
|
+
}
|
|
688
|
+
return hydratedResource;
|
|
689
|
+
}
|
|
690
|
+
function parseCollectionData(doc) {
|
|
691
|
+
if (!doc || typeof doc !== "object") {
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
const hydratedData = hydrateDocumentData(doc);
|
|
695
|
+
if (!hydratedData) {
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
if (!Array.isArray(hydratedData)) {
|
|
699
|
+
return [hydratedData];
|
|
700
|
+
}
|
|
701
|
+
return hydratedData;
|
|
702
|
+
}
|
|
703
|
+
function parseDataForInsertOrUpdate(itemData) {
|
|
704
|
+
if (itemData === null) {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
if (typeof itemData !== "object") {
|
|
708
|
+
throw new Error("Invalid item data: " + itemData);
|
|
709
|
+
}
|
|
710
|
+
if (itemData.constructor === Array || itemData.hasOwnProperty("items") && itemData.hasOwnProperty("length")) {
|
|
711
|
+
let resource2 = [];
|
|
712
|
+
itemData.forEach(function(item) {
|
|
713
|
+
resource2.push(parseDataForInsertOrUpdate(item));
|
|
714
|
+
});
|
|
715
|
+
return resource2;
|
|
716
|
+
}
|
|
717
|
+
if (itemData.constructor !== Object) {
|
|
718
|
+
throw new Error("Invalid case");
|
|
719
|
+
}
|
|
720
|
+
let resource = {};
|
|
721
|
+
if (!itemData.hasOwnProperty("attributes")) {
|
|
722
|
+
let tmp = { attributes: {} };
|
|
723
|
+
if (itemData.hasOwnProperty("type")) {
|
|
724
|
+
tmp.type = itemData.type;
|
|
725
|
+
}
|
|
726
|
+
Object.assign(tmp.attributes, itemData);
|
|
727
|
+
itemData = tmp;
|
|
728
|
+
}
|
|
729
|
+
Object.getOwnPropertyNames(itemData.attributes).forEach(function(attr) {
|
|
730
|
+
if (itemData.attributes[attr] && typeof itemData.attributes[attr] === "object") {
|
|
731
|
+
if (!resource.relationships) {
|
|
732
|
+
resource.relationships = {};
|
|
733
|
+
}
|
|
734
|
+
resource.relationships[attr] = {
|
|
735
|
+
data: parseDataForInsertOrUpdate(itemData.attributes[attr])
|
|
736
|
+
};
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (!resource.attributes) {
|
|
740
|
+
resource.attributes = {};
|
|
741
|
+
}
|
|
742
|
+
resource.attributes[attr] = itemData.attributes[attr];
|
|
743
|
+
});
|
|
744
|
+
return resource;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/adapters/JsonApiAdapter.js
|
|
748
|
+
var JsonApiAdapter = class {
|
|
749
|
+
/** @type {string} */
|
|
750
|
+
name = "jsonapi";
|
|
751
|
+
/**
|
|
752
|
+
* Whether a remote response represents a single resource (not a collection).
|
|
753
|
+
*
|
|
754
|
+
* @param {object} data - Raw HTTP response body
|
|
755
|
+
* @returns {boolean}
|
|
756
|
+
*/
|
|
757
|
+
isSingleItemResponse(data) {
|
|
758
|
+
return !!(data && data.data && typeof data.data === "object" && !Array.isArray(data.data));
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Extract pagination metadata from a remote document.
|
|
762
|
+
*
|
|
763
|
+
* @param {object} data - Raw HTTP response body
|
|
764
|
+
* @returns {{ totalRecords?: number, offset?: number }}
|
|
765
|
+
*/
|
|
766
|
+
extractMetadata(data) {
|
|
767
|
+
const meta = {};
|
|
768
|
+
if (!data || !data.hasOwnProperty("meta") || typeof data.meta !== "object") {
|
|
769
|
+
return meta;
|
|
770
|
+
}
|
|
771
|
+
if (data.meta.hasOwnProperty("totalRecords")) {
|
|
772
|
+
meta.totalRecords = data.meta.totalRecords * 1;
|
|
773
|
+
}
|
|
774
|
+
if (data.meta.hasOwnProperty("offset")) {
|
|
775
|
+
meta.offset = data.meta.offset;
|
|
776
|
+
}
|
|
777
|
+
return meta;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Apply extracted metadata to a Collection instance.
|
|
781
|
+
*
|
|
782
|
+
* @param {object} collection - Collection instance
|
|
783
|
+
* @param {{ totalRecords?: number, offset?: number }} meta
|
|
784
|
+
*/
|
|
785
|
+
applyMetadata(collection, meta) {
|
|
786
|
+
if (meta.totalRecords !== void 0) {
|
|
787
|
+
collection.total = meta.totalRecords;
|
|
788
|
+
}
|
|
789
|
+
if (meta.offset !== void 0) {
|
|
790
|
+
collection.offset = meta.offset;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Parse a single-item remote document into a canonical resource object.
|
|
795
|
+
*
|
|
796
|
+
* @param {object} data - Raw HTTP response body
|
|
797
|
+
* @param {object} [options]
|
|
798
|
+
* @returns {object} Hydrated resource ready for Item.loadFromData()
|
|
799
|
+
*/
|
|
800
|
+
parseItemResponse(data, options = {}) {
|
|
801
|
+
this.validateItemRemoteDoc(data, options);
|
|
802
|
+
return parseItemData(data, options);
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Validate that a remote document is suitable for a single Item load.
|
|
806
|
+
*
|
|
807
|
+
* @param {object} data - Raw HTTP response body
|
|
808
|
+
* @param {object} [options]
|
|
809
|
+
* @param {object} [options.collection] - Parent collection (for type inference)
|
|
810
|
+
*/
|
|
811
|
+
validateItemRemoteDoc(data) {
|
|
812
|
+
if (data?.data?.constructor === Array) {
|
|
813
|
+
throw new Error("Invalid configuration: resource type is item but server response is collection");
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Infer resource type from a single-item remote document.
|
|
818
|
+
*
|
|
819
|
+
* @param {object} data - Raw HTTP response body
|
|
820
|
+
* @returns {string|undefined}
|
|
821
|
+
*/
|
|
822
|
+
inferItemType(data) {
|
|
823
|
+
return data?.data?.type;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Parse a collection remote document into canonical resource objects.
|
|
827
|
+
*
|
|
828
|
+
* @param {object} doc - Raw HTTP response body
|
|
829
|
+
* @returns {{ items: Array<object>, meta: object }}
|
|
830
|
+
*/
|
|
831
|
+
parseCollectionResponse(doc) {
|
|
832
|
+
const items = parseCollectionData(doc);
|
|
833
|
+
const meta = this.extractMetadata(doc);
|
|
834
|
+
return { items, meta };
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Apply list query parameters to a URL object before a collection fetch.
|
|
838
|
+
*
|
|
839
|
+
* @param {import('../URL.js').URL} url - Collection URL
|
|
840
|
+
* @param {{ type?: string, offset?: number, pageSize?: number }} params
|
|
841
|
+
*/
|
|
842
|
+
applyListQuery(url, params) {
|
|
843
|
+
const { type, offset, pageSize } = params;
|
|
844
|
+
if (typeof offset !== "undefined" && offset !== null && type) {
|
|
845
|
+
url.parameters[`page[${type}][offset]`] = offset;
|
|
846
|
+
}
|
|
847
|
+
if (typeof pageSize !== "undefined" && pageSize !== null && type) {
|
|
848
|
+
url.parameters[`page[${type}][limit]`] = pageSize;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Serialize plain item data for a create (POST) request.
|
|
853
|
+
*
|
|
854
|
+
* @param {object|Array} itemData - Single item or array of items
|
|
855
|
+
* @param {{ type?: string }} [context]
|
|
856
|
+
* @returns {{ body: string, contentType: string, headers?: object }}
|
|
857
|
+
*/
|
|
858
|
+
serializeForCreate(itemData, context = {}) {
|
|
859
|
+
const doc = { data: parseDataForInsertOrUpdate(itemData) };
|
|
860
|
+
if (context.type) {
|
|
861
|
+
doc.type = context.type;
|
|
862
|
+
}
|
|
863
|
+
return {
|
|
864
|
+
body: JSON.stringify(doc),
|
|
865
|
+
contentType: "application/vnd.api+json"
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Serialize changed fields for an update (PATCH) request.
|
|
870
|
+
*
|
|
871
|
+
* @param {object} toUpdate - Resource patch with id, type, attributes, relationships
|
|
872
|
+
* @returns {{ body: string, contentType: string }}
|
|
873
|
+
*/
|
|
874
|
+
serializeForUpdate(toUpdate) {
|
|
875
|
+
return {
|
|
876
|
+
body: JSON.stringify({ data: toUpdate }),
|
|
877
|
+
contentType: "application/vnd.api+json"
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Serialize a runtime relationship value to JSON:API wire format.
|
|
882
|
+
*
|
|
883
|
+
* @param {object|Array|null} rel - Runtime relationship value
|
|
884
|
+
* @returns {object} JSON:API relationship: { data: ... }
|
|
885
|
+
*/
|
|
886
|
+
serializeRelationship(rel) {
|
|
887
|
+
if (rel === null) {
|
|
888
|
+
return { data: null };
|
|
889
|
+
}
|
|
890
|
+
if (rel && typeof rel === "object" && !Array.isArray(rel)) {
|
|
891
|
+
if (rel.id) {
|
|
892
|
+
const result = { data: { id: rel.id } };
|
|
893
|
+
if (rel.type) {
|
|
894
|
+
result.data.type = rel.type;
|
|
895
|
+
}
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
if (rel.hasOwnProperty("toJSON")) {
|
|
899
|
+
const json = rel.toJSON();
|
|
900
|
+
const result = { data: { id: json.id } };
|
|
901
|
+
if (json.type) {
|
|
902
|
+
result.data.type = json.type;
|
|
903
|
+
}
|
|
904
|
+
return result;
|
|
905
|
+
}
|
|
906
|
+
return { data: null };
|
|
907
|
+
}
|
|
908
|
+
if (Array.isArray(rel)) {
|
|
909
|
+
return {
|
|
910
|
+
data: rel.map((item) => {
|
|
911
|
+
if (item && typeof item === "object") {
|
|
912
|
+
if (item.type && item.id) {
|
|
913
|
+
const result = { id: item.id };
|
|
914
|
+
if (item.type) {
|
|
915
|
+
result.type = item.type;
|
|
916
|
+
}
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
if (item.hasOwnProperty("toJSON")) {
|
|
920
|
+
const json = item.toJSON();
|
|
921
|
+
const result = { id: json.id };
|
|
922
|
+
if (json.type) {
|
|
923
|
+
result.type = json.type;
|
|
924
|
+
}
|
|
925
|
+
return result;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return item;
|
|
929
|
+
}).filter((item) => item && item.id)
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
return { data: null };
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
// src/adapters/plainUtils.js
|
|
937
|
+
function getPath(obj, path) {
|
|
938
|
+
if (!obj || !path || typeof path !== "string") {
|
|
939
|
+
return void 0;
|
|
940
|
+
}
|
|
941
|
+
return path.split(".").reduce((current, key) => {
|
|
942
|
+
if (current == null || typeof current !== "object") {
|
|
943
|
+
return void 0;
|
|
944
|
+
}
|
|
945
|
+
return current[key];
|
|
946
|
+
}, obj);
|
|
947
|
+
}
|
|
948
|
+
function extractCollectionRows(doc, itemsPath) {
|
|
949
|
+
if (Array.isArray(doc)) {
|
|
950
|
+
return doc;
|
|
951
|
+
}
|
|
952
|
+
if (!doc || typeof doc !== "object") {
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
if (itemsPath) {
|
|
956
|
+
const rows = getPath(doc, itemsPath);
|
|
957
|
+
return Array.isArray(rows) ? rows : [];
|
|
958
|
+
}
|
|
959
|
+
if (Array.isArray(doc.data)) {
|
|
960
|
+
return doc.data;
|
|
961
|
+
}
|
|
962
|
+
if (Array.isArray(doc.items)) {
|
|
963
|
+
return doc.items;
|
|
964
|
+
}
|
|
965
|
+
if (Array.isArray(doc.results)) {
|
|
966
|
+
return doc.results;
|
|
967
|
+
}
|
|
968
|
+
return [];
|
|
969
|
+
}
|
|
970
|
+
function extractTotalRecords(doc, totalPath) {
|
|
971
|
+
if (!doc || typeof doc !== "object") {
|
|
972
|
+
return void 0;
|
|
973
|
+
}
|
|
974
|
+
if (totalPath) {
|
|
975
|
+
const value = getPath(doc, totalPath);
|
|
976
|
+
return value != null ? value * 1 : void 0;
|
|
977
|
+
}
|
|
978
|
+
for (const path of ["total", "count", "totalCount", "meta.totalRecords"]) {
|
|
979
|
+
const value = getPath(doc, path);
|
|
980
|
+
if (value != null) {
|
|
981
|
+
return value * 1;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return void 0;
|
|
985
|
+
}
|
|
986
|
+
function extractOffset(doc, offsetPath) {
|
|
987
|
+
if (!doc || typeof doc !== "object") {
|
|
988
|
+
return void 0;
|
|
989
|
+
}
|
|
990
|
+
if (offsetPath) {
|
|
991
|
+
const value = getPath(doc, offsetPath);
|
|
992
|
+
return value != null ? value : void 0;
|
|
993
|
+
}
|
|
994
|
+
for (const path of ["offset", "meta.offset"]) {
|
|
995
|
+
const value = getPath(doc, path);
|
|
996
|
+
if (value != null) {
|
|
997
|
+
return value;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return void 0;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/adapters/PlainRestAdapter.js
|
|
1004
|
+
var PlainRestAdapter = class {
|
|
1005
|
+
/** @type {string} */
|
|
1006
|
+
name = "plain";
|
|
1007
|
+
/**
|
|
1008
|
+
* @param {object} [opts]
|
|
1009
|
+
* @param {string|null} [opts.itemsPath] - Dot path to item array (null = auto-detect)
|
|
1010
|
+
* @param {string|null} [opts.itemPath] - Dot path to single item in a wrapper document
|
|
1011
|
+
* @param {string|null} [opts.totalPath] - Dot path to total count (null = auto-detect)
|
|
1012
|
+
* @param {string|null} [opts.offsetPath] - Dot path to offset (null = auto-detect)
|
|
1013
|
+
* @param {string} [opts.idField] - Primary key field name
|
|
1014
|
+
* @param {string|null} [opts.typeField] - Resource type field on wire objects
|
|
1015
|
+
* @param {'offset'|'page'} [opts.paginationStyle] - Query param style for list fetches
|
|
1016
|
+
* @param {string} [opts.offsetParam] - Offset query parameter name
|
|
1017
|
+
* @param {string} [opts.limitParam] - Page size query parameter name
|
|
1018
|
+
* @param {string} [opts.pageParam] - Page number query parameter name (1-based)
|
|
1019
|
+
* @param {boolean} [opts.embedRelationships] - Embed nested objects on write (default: id stub)
|
|
1020
|
+
*/
|
|
1021
|
+
constructor(opts = {}) {
|
|
1022
|
+
this.itemsPath = opts.itemsPath ?? null;
|
|
1023
|
+
this.itemPath = opts.itemPath ?? null;
|
|
1024
|
+
this.totalPath = opts.totalPath ?? null;
|
|
1025
|
+
this.offsetPath = opts.offsetPath ?? null;
|
|
1026
|
+
this.idField = opts.idField ?? "id";
|
|
1027
|
+
this.typeField = opts.typeField ?? "type";
|
|
1028
|
+
this.paginationStyle = opts.paginationStyle ?? "offset";
|
|
1029
|
+
this.offsetParam = opts.offsetParam ?? "offset";
|
|
1030
|
+
this.limitParam = opts.limitParam ?? "limit";
|
|
1031
|
+
this.pageParam = opts.pageParam ?? "page";
|
|
1032
|
+
this.embedRelationships = opts.embedRelationships ?? false;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* @param {object|Array} data
|
|
1036
|
+
* @returns {boolean}
|
|
1037
|
+
*/
|
|
1038
|
+
isSingleItemResponse(data) {
|
|
1039
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
if (this.itemPath) {
|
|
1043
|
+
const item = getPath(data, this.itemPath);
|
|
1044
|
+
if (Array.isArray(item)) {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
if (item && typeof item === "object") {
|
|
1048
|
+
return item[this.idField] != null;
|
|
1049
|
+
}
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
const rows = extractCollectionRows(data, this.itemsPath);
|
|
1053
|
+
if (rows.length > 0) {
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
if (data.data && Array.isArray(data.data)) {
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
if (data.data && typeof data.data === "object" && data.data[this.idField] != null) {
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1062
|
+
return data[this.idField] != null;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* @param {object} data
|
|
1066
|
+
* @returns {{ totalRecords?: number, offset?: number }}
|
|
1067
|
+
*/
|
|
1068
|
+
extractMetadata(data) {
|
|
1069
|
+
const meta = {};
|
|
1070
|
+
const totalRecords = extractTotalRecords(data, this.totalPath);
|
|
1071
|
+
if (totalRecords !== void 0) {
|
|
1072
|
+
meta.totalRecords = totalRecords;
|
|
1073
|
+
}
|
|
1074
|
+
const offset = extractOffset(data, this.offsetPath);
|
|
1075
|
+
if (offset !== void 0) {
|
|
1076
|
+
meta.offset = offset;
|
|
1077
|
+
}
|
|
1078
|
+
return meta;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* @param {object} collection
|
|
1082
|
+
* @param {{ totalRecords?: number, offset?: number }} meta
|
|
1083
|
+
*/
|
|
1084
|
+
applyMetadata(collection, meta) {
|
|
1085
|
+
if (meta.totalRecords !== void 0) {
|
|
1086
|
+
collection.total = meta.totalRecords;
|
|
1087
|
+
}
|
|
1088
|
+
if (meta.offset !== void 0) {
|
|
1089
|
+
collection.offset = meta.offset;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* @param {object} data
|
|
1094
|
+
* @param {object} [options]
|
|
1095
|
+
* @returns {object}
|
|
1096
|
+
*/
|
|
1097
|
+
parseItemResponse(data, options = {}) {
|
|
1098
|
+
this.validateItemRemoteDoc(data, options);
|
|
1099
|
+
const raw = this.extractRawItem(data);
|
|
1100
|
+
const defaultType = options.collection?.type ?? options.type;
|
|
1101
|
+
return this.normalize(raw, defaultType);
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* @param {object} data
|
|
1105
|
+
*/
|
|
1106
|
+
validateItemRemoteDoc(data) {
|
|
1107
|
+
if (Array.isArray(data)) {
|
|
1108
|
+
throw new Error("Invalid configuration: resource type is item but server response is collection");
|
|
1109
|
+
}
|
|
1110
|
+
if (this.itemPath) {
|
|
1111
|
+
const item = getPath(data, this.itemPath);
|
|
1112
|
+
if (Array.isArray(item)) {
|
|
1113
|
+
throw new Error("Invalid configuration: resource type is item but server response is collection");
|
|
1114
|
+
}
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (data?.data && Array.isArray(data.data)) {
|
|
1118
|
+
throw new Error("Invalid configuration: resource type is item but server response is collection");
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* @param {object} data
|
|
1123
|
+
* @returns {string|undefined}
|
|
1124
|
+
*/
|
|
1125
|
+
inferItemType(data) {
|
|
1126
|
+
const raw = this.extractRawItem(data);
|
|
1127
|
+
if (!raw || typeof raw !== "object") {
|
|
1128
|
+
return void 0;
|
|
1129
|
+
}
|
|
1130
|
+
return raw[this.typeField];
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* @param {object|Array} doc
|
|
1134
|
+
* @param {object} [options]
|
|
1135
|
+
* @returns {{ items: Array<object>, meta: object }}
|
|
1136
|
+
*/
|
|
1137
|
+
parseCollectionResponse(doc, options = {}) {
|
|
1138
|
+
const rows = extractCollectionRows(doc, this.itemsPath);
|
|
1139
|
+
const defaultType = options.type;
|
|
1140
|
+
const items = rows.map((row) => this.normalize(row, defaultType));
|
|
1141
|
+
const meta = this.extractMetadata(doc);
|
|
1142
|
+
return { items, meta };
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* @param {import('../URL.js').URL} url
|
|
1146
|
+
* @param {{ offset?: number, pageSize?: number }} params
|
|
1147
|
+
*/
|
|
1148
|
+
applyListQuery(url, params) {
|
|
1149
|
+
const { offset, pageSize } = params;
|
|
1150
|
+
if (typeof pageSize === "undefined" || pageSize === null) {
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if (this.paginationStyle === "page") {
|
|
1154
|
+
const safeOffset = typeof offset === "undefined" || offset === null ? 0 : offset;
|
|
1155
|
+
const page = Math.floor(safeOffset / pageSize) + 1;
|
|
1156
|
+
url.parameters[this.pageParam] = page;
|
|
1157
|
+
url.parameters[this.limitParam] = pageSize;
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (typeof offset !== "undefined" && offset !== null) {
|
|
1161
|
+
url.parameters[this.offsetParam] = offset;
|
|
1162
|
+
}
|
|
1163
|
+
url.parameters[this.limitParam] = pageSize;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* @param {object|Array} itemData
|
|
1167
|
+
* @param {object} [context]
|
|
1168
|
+
* @returns {{ body: string, contentType: string }}
|
|
1169
|
+
*/
|
|
1170
|
+
serializeForCreate(itemData, context = {}) {
|
|
1171
|
+
const defaultType = context.type;
|
|
1172
|
+
let payload;
|
|
1173
|
+
if (Array.isArray(itemData)) {
|
|
1174
|
+
payload = itemData.map((item) => this.flattenForWire(this.coerceToCanonical(item, defaultType)));
|
|
1175
|
+
} else {
|
|
1176
|
+
payload = this.flattenForWire(this.coerceToCanonical(itemData, defaultType));
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
body: JSON.stringify(payload),
|
|
1180
|
+
contentType: "application/json"
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* @param {object} toUpdate
|
|
1185
|
+
* @returns {{ body: string, contentType: string }}
|
|
1186
|
+
*/
|
|
1187
|
+
serializeForUpdate(toUpdate) {
|
|
1188
|
+
const relationships = {};
|
|
1189
|
+
Object.entries(toUpdate.relationships || {}).forEach(([name, rel]) => {
|
|
1190
|
+
relationships[name] = this.unwrapRelationship(rel);
|
|
1191
|
+
});
|
|
1192
|
+
const payload = this.flattenForWire({
|
|
1193
|
+
id: toUpdate.id,
|
|
1194
|
+
type: toUpdate.type,
|
|
1195
|
+
attributes: toUpdate.attributes,
|
|
1196
|
+
relationships
|
|
1197
|
+
});
|
|
1198
|
+
return {
|
|
1199
|
+
body: JSON.stringify(payload),
|
|
1200
|
+
contentType: "application/json"
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* @param {object|Array|null} rel
|
|
1205
|
+
* @returns {object|Array|null|undefined}
|
|
1206
|
+
*/
|
|
1207
|
+
serializeRelationship(rel) {
|
|
1208
|
+
return this.unwrapRelationship(rel);
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* @param {object} data
|
|
1212
|
+
* @returns {object}
|
|
1213
|
+
* @private
|
|
1214
|
+
*/
|
|
1215
|
+
extractRawItem(data) {
|
|
1216
|
+
if (this.itemPath) {
|
|
1217
|
+
return getPath(data, this.itemPath);
|
|
1218
|
+
}
|
|
1219
|
+
if (data?.data && typeof data.data === "object" && !Array.isArray(data.data)) {
|
|
1220
|
+
return data.data;
|
|
1221
|
+
}
|
|
1222
|
+
return data;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* @param {object} row
|
|
1226
|
+
* @param {string|undefined} defaultType
|
|
1227
|
+
* @returns {object}
|
|
1228
|
+
*/
|
|
1229
|
+
normalize(row, defaultType) {
|
|
1230
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
1231
|
+
throw new Error("Invalid item data: must be an object");
|
|
1232
|
+
}
|
|
1233
|
+
if (row.attributes && typeof row.attributes === "object") {
|
|
1234
|
+
const relationships2 = {};
|
|
1235
|
+
Object.entries(row.relationships || {}).forEach(([name, value]) => {
|
|
1236
|
+
if (value === null) {
|
|
1237
|
+
relationships2[name] = null;
|
|
1238
|
+
} else if (Array.isArray(value)) {
|
|
1239
|
+
relationships2[name] = value.map((entry) => this.normalize(entry, defaultType));
|
|
1240
|
+
} else {
|
|
1241
|
+
relationships2[name] = this.normalize(value, defaultType);
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
const normalized2 = {
|
|
1245
|
+
attributes: { ...row.attributes },
|
|
1246
|
+
relationships: relationships2
|
|
1247
|
+
};
|
|
1248
|
+
if (row.id != null) {
|
|
1249
|
+
normalized2.id = String(row.id);
|
|
1250
|
+
}
|
|
1251
|
+
if (row.type ?? defaultType) {
|
|
1252
|
+
normalized2.type = row.type ?? defaultType;
|
|
1253
|
+
}
|
|
1254
|
+
return normalized2;
|
|
1255
|
+
}
|
|
1256
|
+
const attributes = {};
|
|
1257
|
+
const relationships = {};
|
|
1258
|
+
let id;
|
|
1259
|
+
let type;
|
|
1260
|
+
Object.entries(row).forEach(([key, value]) => {
|
|
1261
|
+
if (key === this.idField) {
|
|
1262
|
+
id = value;
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (key === this.typeField) {
|
|
1266
|
+
type = value;
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (value === null || value === void 0) {
|
|
1270
|
+
attributes[key] = value;
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (Array.isArray(value)) {
|
|
1274
|
+
if (value.length > 0 && value.every((entry) => this.isNestedResource(entry))) {
|
|
1275
|
+
relationships[key] = value.map((entry) => this.normalize(entry, defaultType));
|
|
1276
|
+
} else {
|
|
1277
|
+
attributes[key] = value;
|
|
1278
|
+
}
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (this.isNestedResource(value)) {
|
|
1282
|
+
relationships[key] = this.normalize(value, defaultType);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
attributes[key] = value;
|
|
1286
|
+
});
|
|
1287
|
+
const normalized = { attributes, relationships };
|
|
1288
|
+
if (id != null) {
|
|
1289
|
+
normalized.id = String(id);
|
|
1290
|
+
}
|
|
1291
|
+
if (type ?? defaultType) {
|
|
1292
|
+
normalized.type = type ?? defaultType;
|
|
1293
|
+
}
|
|
1294
|
+
return normalized;
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* @param {*} value
|
|
1298
|
+
* @returns {boolean}
|
|
1299
|
+
* @private
|
|
1300
|
+
*/
|
|
1301
|
+
isNestedResource(value) {
|
|
1302
|
+
return value && typeof value === "object" && !Array.isArray(value) && value[this.idField] != null;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* @param {object} data
|
|
1306
|
+
* @param {string|undefined} defaultType
|
|
1307
|
+
* @returns {object}
|
|
1308
|
+
* @private
|
|
1309
|
+
*/
|
|
1310
|
+
coerceToCanonical(data, defaultType) {
|
|
1311
|
+
if (!data || typeof data !== "object") {
|
|
1312
|
+
return data;
|
|
1313
|
+
}
|
|
1314
|
+
if (data.attributes || data.relationships) {
|
|
1315
|
+
return data;
|
|
1316
|
+
}
|
|
1317
|
+
return this.normalize(data, defaultType);
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* @param {object} canonical
|
|
1321
|
+
* @returns {object}
|
|
1322
|
+
* @private
|
|
1323
|
+
*/
|
|
1324
|
+
flattenForWire(canonical) {
|
|
1325
|
+
if (!canonical || typeof canonical !== "object") {
|
|
1326
|
+
return canonical;
|
|
1327
|
+
}
|
|
1328
|
+
const result = { ...canonical.attributes || {} };
|
|
1329
|
+
if (canonical.id != null) {
|
|
1330
|
+
result[this.idField] = canonical.id;
|
|
1331
|
+
}
|
|
1332
|
+
if (canonical.type != null && this.typeField) {
|
|
1333
|
+
result[this.typeField] = canonical.type;
|
|
1334
|
+
}
|
|
1335
|
+
Object.entries(canonical.relationships || {}).forEach(([name, rel]) => {
|
|
1336
|
+
if (rel === null) {
|
|
1337
|
+
result[name] = null;
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (Array.isArray(rel)) {
|
|
1341
|
+
result[name] = rel.map((entry) => this.relationshipToWire(entry));
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
result[name] = this.relationshipToWire(rel);
|
|
1345
|
+
});
|
|
1346
|
+
return result;
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* @param {object|null|undefined} rel
|
|
1350
|
+
* @returns {object|null|undefined}
|
|
1351
|
+
* @private
|
|
1352
|
+
*/
|
|
1353
|
+
relationshipToWire(rel) {
|
|
1354
|
+
if (!rel) {
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
if (this.embedRelationships && rel.attributes) {
|
|
1358
|
+
return this.flattenForWire(rel);
|
|
1359
|
+
}
|
|
1360
|
+
const stub = { [this.idField]: rel.id ?? rel[this.idField] };
|
|
1361
|
+
if ((rel.type ?? rel[this.typeField]) != null) {
|
|
1362
|
+
stub[this.typeField] = rel.type ?? rel[this.typeField];
|
|
1363
|
+
}
|
|
1364
|
+
return stub;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* @param {*} rel
|
|
1368
|
+
* @returns {object|Array|null|undefined}
|
|
1369
|
+
* @private
|
|
1370
|
+
*/
|
|
1371
|
+
unwrapRelationship(rel) {
|
|
1372
|
+
if (rel == null) {
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
if (rel.data !== void 0) {
|
|
1376
|
+
if (rel.data === null) {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
if (Array.isArray(rel.data)) {
|
|
1380
|
+
return rel.data.map((entry) => ({ ...entry }));
|
|
1381
|
+
}
|
|
1382
|
+
return { ...rel.data };
|
|
1383
|
+
}
|
|
1384
|
+
if (Array.isArray(rel)) {
|
|
1385
|
+
return rel.map((entry) => this.unwrapRelationship(entry));
|
|
1386
|
+
}
|
|
1387
|
+
if (typeof rel === "object") {
|
|
1388
|
+
if (rel.attributes) {
|
|
1389
|
+
return rel;
|
|
1390
|
+
}
|
|
1391
|
+
return { ...rel };
|
|
1392
|
+
}
|
|
1393
|
+
return rel;
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
// src/adapters/resolveAdapter.js
|
|
1398
|
+
var registry = /* @__PURE__ */ new Map([
|
|
1399
|
+
["jsonapi", new JsonApiAdapter()],
|
|
1400
|
+
["plain", new PlainRestAdapter()]
|
|
1401
|
+
]);
|
|
1402
|
+
var defaultAdapter = "jsonapi";
|
|
1403
|
+
function registerAdapter(name, adapter) {
|
|
1404
|
+
if (!name || typeof name !== "string") {
|
|
1405
|
+
throw new Error("Adapter name must be a non-empty string");
|
|
1406
|
+
}
|
|
1407
|
+
registry.set(name, adapter);
|
|
1408
|
+
}
|
|
1409
|
+
function setDefaultAdapter(adapter) {
|
|
1410
|
+
defaultAdapter = adapter;
|
|
1411
|
+
}
|
|
1412
|
+
function getDefaultAdapter() {
|
|
1413
|
+
return defaultAdapter;
|
|
1414
|
+
}
|
|
1415
|
+
function resolveAdapter(adapter) {
|
|
1416
|
+
if (adapter && typeof adapter === "object") {
|
|
1417
|
+
return adapter;
|
|
1418
|
+
}
|
|
1419
|
+
const name = typeof adapter === "string" ? adapter : defaultAdapter;
|
|
1420
|
+
if (typeof name === "object") {
|
|
1421
|
+
return name;
|
|
1422
|
+
}
|
|
1423
|
+
const resolved = registry.get(name);
|
|
1424
|
+
if (!resolved) {
|
|
1425
|
+
throw new Error(`Unknown data adapter: ${name}`);
|
|
1426
|
+
}
|
|
1427
|
+
return resolved;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// src/ItemView.js
|
|
1431
|
+
var ItemView = class {
|
|
1432
|
+
constructor(params) {
|
|
1433
|
+
if (params && params.isView) {
|
|
1434
|
+
return params;
|
|
1435
|
+
}
|
|
1436
|
+
this.type = "ItemView";
|
|
1437
|
+
this.dataBindings = null;
|
|
1438
|
+
this.template = null;
|
|
1439
|
+
this.container = null;
|
|
1440
|
+
this.collectionView = null;
|
|
1441
|
+
this.item = null;
|
|
1442
|
+
this.el = null;
|
|
1443
|
+
this.id = uid();
|
|
1444
|
+
this.isView = true;
|
|
1445
|
+
this.callbacks = {};
|
|
1446
|
+
if (params && (params.length || params.nodeName && !params.jquery)) {
|
|
1447
|
+
dbg("params is actually a jquery object or an html node", params);
|
|
1448
|
+
let $el = $(params);
|
|
1449
|
+
if ($el.data("view")) {
|
|
1450
|
+
return $el.data("view");
|
|
1451
|
+
}
|
|
1452
|
+
let tmp = $("<div>").append($el.clone(true));
|
|
1453
|
+
let html = tmp.html().replace(/<%/gi, "<%").replace(/</gi, "<").replace(/%>/gi, "%>").replace(/>/gi, ">").replace(/&/gi, "&");
|
|
1454
|
+
params = {
|
|
1455
|
+
template: template(html),
|
|
1456
|
+
el: $el
|
|
1457
|
+
};
|
|
1458
|
+
if ($el.attr("id")) {
|
|
1459
|
+
params.id = $el.attr("id");
|
|
1460
|
+
}
|
|
1461
|
+
tmp.remove();
|
|
1462
|
+
}
|
|
1463
|
+
try {
|
|
1464
|
+
params = parseOptions(params);
|
|
1465
|
+
} catch (e) {
|
|
1466
|
+
throw new Error("Error on ItemView", this, e);
|
|
1467
|
+
}
|
|
1468
|
+
Object.assign(this, params);
|
|
1469
|
+
if (this.el !== null) {
|
|
1470
|
+
this.dataBindings = getBoundObjects(this.el);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Event listener registration
|
|
1475
|
+
*/
|
|
1476
|
+
on(event, cb) {
|
|
1477
|
+
if (!this.callbacks[event]) {
|
|
1478
|
+
this.callbacks[event] = [];
|
|
1479
|
+
}
|
|
1480
|
+
this.callbacks[event].push(cb);
|
|
1481
|
+
return this;
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Unbind from item
|
|
1485
|
+
*/
|
|
1486
|
+
unbind() {
|
|
1487
|
+
if (this.item) {
|
|
1488
|
+
this.item.unbindView(this);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Create element from template
|
|
1493
|
+
*/
|
|
1494
|
+
createElementFromTemplate() {
|
|
1495
|
+
if (this.template == null) {
|
|
1496
|
+
dbg("Warning: no template defined. Nothing to render");
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
if (!this.item) {
|
|
1500
|
+
dbg("Warning: no item bound to view. Cannot render template.");
|
|
1501
|
+
return null;
|
|
1502
|
+
}
|
|
1503
|
+
let el;
|
|
1504
|
+
try {
|
|
1505
|
+
const renderContext = this.item.getRenderContext();
|
|
1506
|
+
console.log("renderContext", renderContext);
|
|
1507
|
+
let html = this.template(renderContext);
|
|
1508
|
+
el = $(html).attr("data-type", "item").attr("id", this.id).data("view", this).data("instance", this.item);
|
|
1509
|
+
} catch (e) {
|
|
1510
|
+
console.log("Error create view from template", e, this.item);
|
|
1511
|
+
el = $("<div>Could not render view: <strong>" + e.toString() + "</strong></div>");
|
|
1512
|
+
}
|
|
1513
|
+
return el;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* After render callback
|
|
1517
|
+
*/
|
|
1518
|
+
afterrender() {
|
|
1519
|
+
if (!this.callbacks.afterrender) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
console.log("afterend of view", this);
|
|
1523
|
+
this.callbacks.afterrender.forEach((cb) => cb(this));
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Render the view
|
|
1527
|
+
*/
|
|
1528
|
+
render(doNotAttachToContainer = false, addontop = false) {
|
|
1529
|
+
log("ItemView.render called", this.item, this.el);
|
|
1530
|
+
let renderedEl = this.createElementFromTemplate();
|
|
1531
|
+
if (!renderedEl) {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
log("View item", this.item);
|
|
1535
|
+
if (this.item && this.item.uievents) {
|
|
1536
|
+
log("UI events", this.item.uievents);
|
|
1537
|
+
this.item.uievents.forEach((action) => {
|
|
1538
|
+
log("UI event", action, renderedEl);
|
|
1539
|
+
if (action.selector && action.event && action.callback) {
|
|
1540
|
+
const actionEls = $(renderedEl).find(action.selector);
|
|
1541
|
+
log("UI event els", action, renderedEl, actionEls);
|
|
1542
|
+
actionEls.on(action.event, (event) => {
|
|
1543
|
+
event.preventDefault();
|
|
1544
|
+
log("UIevent triggered", event, this.item, this);
|
|
1545
|
+
action.callback(event, this.item, this);
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
if (doNotAttachToContainer) {
|
|
1551
|
+
this.el = renderedEl;
|
|
1552
|
+
return this.el;
|
|
1553
|
+
}
|
|
1554
|
+
if (this.el) {
|
|
1555
|
+
let oldEl = this.el;
|
|
1556
|
+
if (!oldEl.jquery) {
|
|
1557
|
+
oldEl = $(oldEl);
|
|
1558
|
+
}
|
|
1559
|
+
oldEl.off();
|
|
1560
|
+
this.el = $(renderedEl).insertBefore(oldEl);
|
|
1561
|
+
oldEl.remove();
|
|
1562
|
+
this.afterrender();
|
|
1563
|
+
return this;
|
|
1564
|
+
}
|
|
1565
|
+
this.el = renderedEl;
|
|
1566
|
+
this.afterrender();
|
|
1567
|
+
if (!this.container) {
|
|
1568
|
+
return this;
|
|
1569
|
+
}
|
|
1570
|
+
$(this.el).appendTo(this.container.el);
|
|
1571
|
+
return this;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Render empty state
|
|
1575
|
+
*/
|
|
1576
|
+
renderEmpty(returnView) {
|
|
1577
|
+
if (this.item && this.item.emptyview && this.el) {
|
|
1578
|
+
let emptyView = $(this.item.emptyview).clone(true).css("display", "block");
|
|
1579
|
+
$(this.el).replaceWith(emptyView);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Remove view with animation
|
|
1584
|
+
*/
|
|
1585
|
+
remove(idx) {
|
|
1586
|
+
return new Promise((resolve) => {
|
|
1587
|
+
if (this.item && this.item.collection) {
|
|
1588
|
+
this.item.collection._trigger("afterrender", this.item.collection);
|
|
1589
|
+
}
|
|
1590
|
+
if (this.el) {
|
|
1591
|
+
let $el = this.el.jquery ? this.el : $(this.el);
|
|
1592
|
+
$el.fadeOut({
|
|
1593
|
+
complete: () => {
|
|
1594
|
+
$el.remove();
|
|
1595
|
+
resolve();
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
} else {
|
|
1599
|
+
resolve();
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Destroy view and clean up resources
|
|
1605
|
+
*
|
|
1606
|
+
* Removes event handlers, jQuery data, and DOM references
|
|
1607
|
+
*/
|
|
1608
|
+
destroy() {
|
|
1609
|
+
if (this.el) {
|
|
1610
|
+
const $el = this.el.jquery ? this.el : $(this.el);
|
|
1611
|
+
$el.off();
|
|
1612
|
+
$el.removeData();
|
|
1613
|
+
}
|
|
1614
|
+
this.callbacks = {};
|
|
1615
|
+
if (this.item) {
|
|
1616
|
+
this.item.unbindView(this);
|
|
1617
|
+
}
|
|
1618
|
+
this.item = null;
|
|
1619
|
+
this.container = null;
|
|
1620
|
+
this.collectionView = null;
|
|
1621
|
+
this.el = null;
|
|
1622
|
+
this.template = null;
|
|
1623
|
+
this.dataBindings = null;
|
|
1624
|
+
return this;
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
// src/Item.js
|
|
1629
|
+
var Item = class _Item {
|
|
1630
|
+
constructor(options = {}, data = null) {
|
|
1631
|
+
this.id = null;
|
|
1632
|
+
this.type = null;
|
|
1633
|
+
this.attributes = {};
|
|
1634
|
+
this.relationships = {};
|
|
1635
|
+
this.views = [];
|
|
1636
|
+
this.collection = null;
|
|
1637
|
+
this.url = null;
|
|
1638
|
+
this.updateUrl = null;
|
|
1639
|
+
this.deleteUrl = null;
|
|
1640
|
+
this.strict = false;
|
|
1641
|
+
this.shadow = null;
|
|
1642
|
+
this.syncOp = null;
|
|
1643
|
+
this.emptyview = null;
|
|
1644
|
+
this.uievents = [];
|
|
1645
|
+
this.callbacks = {};
|
|
1646
|
+
this.adapter = null;
|
|
1647
|
+
try {
|
|
1648
|
+
Object.assign(this, parseOptions(options));
|
|
1649
|
+
} catch (e) {
|
|
1650
|
+
throw new Error("Error on Item init", e);
|
|
1651
|
+
}
|
|
1652
|
+
this.adapter = resolveAdapter(
|
|
1653
|
+
options.adapter ?? (options.collection && options.collection.adapter)
|
|
1654
|
+
);
|
|
1655
|
+
this.storage = options.storage || new Storage(
|
|
1656
|
+
(() => {
|
|
1657
|
+
const storageOpts = Object.assign({}, options.ajaxOpts || {});
|
|
1658
|
+
if (options.headers && typeof options.headers === "object") {
|
|
1659
|
+
storageOpts.headers = Object.assign(
|
|
1660
|
+
{},
|
|
1661
|
+
storageOpts.headers || {},
|
|
1662
|
+
options.headers
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
return storageOpts;
|
|
1666
|
+
})()
|
|
1667
|
+
);
|
|
1668
|
+
let render = false;
|
|
1669
|
+
if (data) {
|
|
1670
|
+
log("Loading data", data);
|
|
1671
|
+
try {
|
|
1672
|
+
this.loadFromData(data);
|
|
1673
|
+
render = true;
|
|
1674
|
+
} catch (e) {
|
|
1675
|
+
console.error("Error loading data", e);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (this.url) {
|
|
1679
|
+
this.setUrl(this.url);
|
|
1680
|
+
}
|
|
1681
|
+
if (this.deleteUrl) {
|
|
1682
|
+
log("deleteUrl", this.deleteUrl);
|
|
1683
|
+
this.setUrl(this.deleteUrl, "delete");
|
|
1684
|
+
}
|
|
1685
|
+
if (this.updateUrl) {
|
|
1686
|
+
this.setUrl(this.updateUrl, "update");
|
|
1687
|
+
}
|
|
1688
|
+
if (this.insertUrl) {
|
|
1689
|
+
this.setUrl(this.insertUrl, "insert");
|
|
1690
|
+
}
|
|
1691
|
+
this.views.forEach((view) => {
|
|
1692
|
+
view.item = this;
|
|
1693
|
+
});
|
|
1694
|
+
if (options.itemListeners && typeof options.itemListeners === "object") {
|
|
1695
|
+
Object.getOwnPropertyNames(options.itemListeners).forEach((eventName) => {
|
|
1696
|
+
log("apply item listener", eventName, options.itemListeners[eventName]);
|
|
1697
|
+
this.on(eventName, options.itemListeners[eventName]);
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
if (render) {
|
|
1701
|
+
log("Rendering data", data);
|
|
1702
|
+
this.render();
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Event listener registration
|
|
1707
|
+
*/
|
|
1708
|
+
on(eventName, cb) {
|
|
1709
|
+
if (typeof this.callbacks[eventName] === "undefined") {
|
|
1710
|
+
this.callbacks[eventName] = [];
|
|
1711
|
+
}
|
|
1712
|
+
this.callbacks[eventName].push(cb);
|
|
1713
|
+
return this;
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Remove event listener(s)
|
|
1717
|
+
* @param {string} eventName - Event name
|
|
1718
|
+
* @param {Function} [cb] - Optional callback to remove. If not provided, removes all listeners for the event
|
|
1719
|
+
* @returns {Item} This instance for chaining
|
|
1720
|
+
*/
|
|
1721
|
+
off(eventName, cb) {
|
|
1722
|
+
if (!eventName) {
|
|
1723
|
+
this.callbacks = {};
|
|
1724
|
+
return this;
|
|
1725
|
+
}
|
|
1726
|
+
if (!this.callbacks[eventName]) {
|
|
1727
|
+
return this;
|
|
1728
|
+
}
|
|
1729
|
+
if (cb) {
|
|
1730
|
+
const index = this.callbacks[eventName].indexOf(cb);
|
|
1731
|
+
if (index > -1) {
|
|
1732
|
+
this.callbacks[eventName].splice(index, 1);
|
|
1733
|
+
}
|
|
1734
|
+
if (this.callbacks[eventName].length === 0) {
|
|
1735
|
+
delete this.callbacks[eventName];
|
|
1736
|
+
}
|
|
1737
|
+
} else {
|
|
1738
|
+
delete this.callbacks[eventName];
|
|
1739
|
+
}
|
|
1740
|
+
return this;
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Register a one-time event listener
|
|
1744
|
+
* @param {string} eventName - Event name
|
|
1745
|
+
* @param {Function} cb - Callback function
|
|
1746
|
+
* @returns {Item} This instance for chaining
|
|
1747
|
+
*/
|
|
1748
|
+
once(eventName, cb) {
|
|
1749
|
+
const wrapper = (...args) => {
|
|
1750
|
+
cb(...args);
|
|
1751
|
+
this.off(eventName, wrapper);
|
|
1752
|
+
};
|
|
1753
|
+
return this.on(eventName, wrapper);
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Check if event has listeners
|
|
1757
|
+
* @param {string} eventName - Event name
|
|
1758
|
+
* @returns {boolean} True if event has listeners
|
|
1759
|
+
*/
|
|
1760
|
+
hasListeners(eventName) {
|
|
1761
|
+
return this.callbacks[eventName] && Array.isArray(this.callbacks[eventName]) && this.callbacks[eventName].length > 0;
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Trigger an event (internal helper)
|
|
1765
|
+
* @private
|
|
1766
|
+
* @param {string} eventName - Event name
|
|
1767
|
+
* @param {...any} args - Arguments to pass to callbacks
|
|
1768
|
+
*/
|
|
1769
|
+
_trigger(eventName, ...args) {
|
|
1770
|
+
if (this.callbacks[eventName] && Array.isArray(this.callbacks[eventName])) {
|
|
1771
|
+
this.callbacks[eventName].forEach((cb) => {
|
|
1772
|
+
if (typeof cb === "function") {
|
|
1773
|
+
cb(...args);
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Emit/trigger an event manually
|
|
1780
|
+
* @param {string} eventName - Event name
|
|
1781
|
+
* @param {...any} args - Arguments to pass to callbacks
|
|
1782
|
+
* @returns {Item} This instance for chaining
|
|
1783
|
+
*/
|
|
1784
|
+
emit(eventName, ...args) {
|
|
1785
|
+
this._trigger(eventName, ...args);
|
|
1786
|
+
return this;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Set URL for this item
|
|
1790
|
+
*/
|
|
1791
|
+
setUrl(url, type) {
|
|
1792
|
+
switch (type) {
|
|
1793
|
+
case "delete":
|
|
1794
|
+
this.deleteUrl = createURL(url);
|
|
1795
|
+
break;
|
|
1796
|
+
case "update":
|
|
1797
|
+
this.updateUrl = createURL(url);
|
|
1798
|
+
break;
|
|
1799
|
+
case "insert":
|
|
1800
|
+
this.insertUrl = createURL(url);
|
|
1801
|
+
break;
|
|
1802
|
+
default:
|
|
1803
|
+
this.url = createURL(url);
|
|
1804
|
+
this.deleteUrl = typeof this.deleteUrl == "string" ? createURL(this.deleteUrl) : this.deleteUrl ?? createURL(this.url);
|
|
1805
|
+
this.updateUrl = typeof this.updateUrl == "string" ? createURL(this.updateUrl) : this.updateUrl ?? createURL(this.url);
|
|
1806
|
+
break;
|
|
1807
|
+
}
|
|
1808
|
+
return this;
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Load from remote
|
|
1812
|
+
*
|
|
1813
|
+
* Canonical method for loading item data from API
|
|
1814
|
+
*/
|
|
1815
|
+
loadFromRemote() {
|
|
1816
|
+
return this.loadFromDataSource();
|
|
1817
|
+
}
|
|
1818
|
+
load(data) {
|
|
1819
|
+
return data ? this.loadFromData(data) : this.loadFromRemote();
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Load from data source (internal implementation)
|
|
1823
|
+
* @private
|
|
1824
|
+
*/
|
|
1825
|
+
loadFromDataSource() {
|
|
1826
|
+
let loaders = [];
|
|
1827
|
+
const overlay = createOverlay(this);
|
|
1828
|
+
this.views.forEach((itemView) => {
|
|
1829
|
+
if (itemView.el) {
|
|
1830
|
+
let $el = $(itemView.el);
|
|
1831
|
+
let loader = overlay.clone();
|
|
1832
|
+
loader.insertBefore(itemView.el).width($el.width()).height($el.height());
|
|
1833
|
+
loaders.push(loader);
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
return new Promise((resolve, reject) => {
|
|
1837
|
+
if (!this.url) {
|
|
1838
|
+
reject(new Error("No valid URL provided"));
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
this._trigger("beforeload", this);
|
|
1842
|
+
let urlString = this.url.toString ? this.url.toString() : this.url;
|
|
1843
|
+
this.storage.read(this, urlString, {}).then((resp) => {
|
|
1844
|
+
let data = resp.data;
|
|
1845
|
+
this.loadFromRemoteDoc(data).render();
|
|
1846
|
+
this._trigger("load", this);
|
|
1847
|
+
loaders.forEach((loader) => {
|
|
1848
|
+
loader.remove();
|
|
1849
|
+
});
|
|
1850
|
+
resolve(this);
|
|
1851
|
+
}).catch((error2) => {
|
|
1852
|
+
dbg("fail to load item resource", this.url, error2);
|
|
1853
|
+
if (error2 instanceof Error && error2.jqXHR) {
|
|
1854
|
+
this.fail(error2.jqXHR, error2.textStatus || "error", error2.errorThrown || error2);
|
|
1855
|
+
reject(error2);
|
|
1856
|
+
} else if (error2 && error2.jqXHR) {
|
|
1857
|
+
this.fail(error2.jqXHR, error2.textStatus, error2.errorThrown);
|
|
1858
|
+
reject(error2);
|
|
1859
|
+
} else {
|
|
1860
|
+
this.fail(null, "error", error2);
|
|
1861
|
+
reject(error2);
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* @deprecated Use loadFromRemote() instead
|
|
1868
|
+
* Alias for backward compatibility
|
|
1869
|
+
*/
|
|
1870
|
+
refresh() {
|
|
1871
|
+
return this.loadFromRemote();
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* @deprecated Use loadFromRemote() instead
|
|
1875
|
+
* Alias for backward compatibility
|
|
1876
|
+
*/
|
|
1877
|
+
reload() {
|
|
1878
|
+
return this.loadFromRemote();
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* @deprecated Use loadFromRemote() instead
|
|
1882
|
+
* Internal method, use loadFromRemote() for public API
|
|
1883
|
+
* @private
|
|
1884
|
+
*/
|
|
1885
|
+
load_from_data_source() {
|
|
1886
|
+
return this.loadFromDataSource();
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Unbind a view from this item
|
|
1890
|
+
*/
|
|
1891
|
+
unbindView(view) {
|
|
1892
|
+
let found = false;
|
|
1893
|
+
for (let i = 0; i < this.views.length; i++) {
|
|
1894
|
+
if (this.views[i] === view) {
|
|
1895
|
+
found = i;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
if (found !== false) {
|
|
1899
|
+
this.views.splice(found, 1);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Bind a view to this item
|
|
1904
|
+
*/
|
|
1905
|
+
bindView(view, returnView) {
|
|
1906
|
+
let $el = $(view);
|
|
1907
|
+
if ($el.length === 0) {
|
|
1908
|
+
throw new Error("Nothing to bind to: empty view element");
|
|
1909
|
+
}
|
|
1910
|
+
if (!(view instanceof ItemView)) {
|
|
1911
|
+
view = new ItemView(view);
|
|
1912
|
+
}
|
|
1913
|
+
let bound = false;
|
|
1914
|
+
this.views.forEach((v) => {
|
|
1915
|
+
dbg("bind to existing view", v.el);
|
|
1916
|
+
if (v === view) {
|
|
1917
|
+
bound = true;
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
if (bound) {
|
|
1921
|
+
return returnView ? view : this;
|
|
1922
|
+
}
|
|
1923
|
+
view.item = this;
|
|
1924
|
+
this.views.push(view);
|
|
1925
|
+
return returnView ? view : this;
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Load from a remote API document (format determined by adapter).
|
|
1929
|
+
*
|
|
1930
|
+
* @param {object} data - Raw HTTP response body
|
|
1931
|
+
* @returns {Item} This instance for chaining
|
|
1932
|
+
*/
|
|
1933
|
+
loadFromRemoteDoc(data) {
|
|
1934
|
+
dbg("Load from remote doc", data);
|
|
1935
|
+
if (this.collection && !this.collection.type) {
|
|
1936
|
+
const inferredType = this.adapter.inferItemType(data);
|
|
1937
|
+
if (inferredType) {
|
|
1938
|
+
this.type = inferredType;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
this.adapter.validateItemRemoteDoc(data, { collection: this.collection });
|
|
1942
|
+
const parsedData = this.adapter.parseItemResponse(data, { collection: this.collection });
|
|
1943
|
+
Object.assign(this, parsedData);
|
|
1944
|
+
if (this.url) {
|
|
1945
|
+
this.url = createURL(this.url);
|
|
1946
|
+
}
|
|
1947
|
+
return this;
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* @deprecated Use loadFromRemoteDoc() instead
|
|
1951
|
+
* @param {object} data - Raw HTTP response body
|
|
1952
|
+
* @returns {Item} This instance for chaining
|
|
1953
|
+
*/
|
|
1954
|
+
loadFromJSONAPIDoc(data) {
|
|
1955
|
+
return this.loadFromRemoteDoc(data);
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Load from data object
|
|
1959
|
+
*/
|
|
1960
|
+
loadFromData(data, render = false) {
|
|
1961
|
+
if (data === null || typeof data !== "object" || data.constructor !== Object) {
|
|
1962
|
+
dbg("cannot load ", data, " into ", this);
|
|
1963
|
+
throw new Error("Cannot load data into item");
|
|
1964
|
+
}
|
|
1965
|
+
if (!data.hasOwnProperty("attributes") && !data.hasOwnProperty("id") && !data.hasOwnProperty("type")) {
|
|
1966
|
+
dbg("need to normalize data", data);
|
|
1967
|
+
let attributes = {};
|
|
1968
|
+
let relationships = {};
|
|
1969
|
+
Object.getOwnPropertyNames(data).forEach((propName) => {
|
|
1970
|
+
if (data[propName] && data[propName].constructor === Object) {
|
|
1971
|
+
relationships[propName] = new _Item().loadFromData(data[propName]);
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
if (data[propName] && data[propName].constructor === Array) {
|
|
1975
|
+
relationships[propName] = data[propName];
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
attributes[propName] = data[propName];
|
|
1979
|
+
});
|
|
1980
|
+
data = {
|
|
1981
|
+
attributes
|
|
1982
|
+
};
|
|
1983
|
+
if (Object.getOwnPropertyNames(relationships).length) {
|
|
1984
|
+
data.relationships = relationships;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
Object.assign(this, data);
|
|
1988
|
+
this._trigger("load", this);
|
|
1989
|
+
if (render) {
|
|
1990
|
+
this.render();
|
|
1991
|
+
}
|
|
1992
|
+
return this;
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Handle failure
|
|
1996
|
+
*/
|
|
1997
|
+
fail(xhr, statusText, error2) {
|
|
1998
|
+
dbg("item.fail", xhr, statusText, error2);
|
|
1999
|
+
this.views.forEach((view) => {
|
|
2000
|
+
if (xhr && xhr.status === 404) {
|
|
2001
|
+
view.renderEmpty();
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Get render context - safe view model for templates
|
|
2007
|
+
*
|
|
2008
|
+
* RENDER CONTEXT CONTRACT:
|
|
2009
|
+
*
|
|
2010
|
+
* Returns a template-friendly object where:
|
|
2011
|
+
* - Attributes are exposed directly (e.g., {{title}} not {{attributes.title}})
|
|
2012
|
+
* - Relationships are flattened to plain objects with attributes, id, type
|
|
2013
|
+
* - All data is shallow-copied to prevent mutation of internal state
|
|
2014
|
+
*
|
|
2015
|
+
* Relationship representation strategy:
|
|
2016
|
+
* - To-one: { id, type, ...attributes } (flattened plain object)
|
|
2017
|
+
* - To-many: Array of { id, type, ...attributes } (array of flattened objects)
|
|
2018
|
+
* - Null relationships: null
|
|
2019
|
+
*
|
|
2020
|
+
* This ensures Handlebars templates can access data directly:
|
|
2021
|
+
* {{title}} - item attribute
|
|
2022
|
+
* {{author.name}} - relationship attribute
|
|
2023
|
+
* {{#each tags}}{{name}}{{/each}} - relationship array
|
|
2024
|
+
*
|
|
2025
|
+
* @returns {Object} Render context object safe for template rendering
|
|
2026
|
+
*/
|
|
2027
|
+
getRenderContext() {
|
|
2028
|
+
function deepCloneStatic(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
2029
|
+
if (value === null || typeof value !== "object") {
|
|
2030
|
+
return value;
|
|
2031
|
+
}
|
|
2032
|
+
if (seen.has(value)) {
|
|
2033
|
+
return seen.get(value);
|
|
2034
|
+
}
|
|
2035
|
+
if (Array.isArray(value)) {
|
|
2036
|
+
const clonedArray = [];
|
|
2037
|
+
seen.set(value, clonedArray);
|
|
2038
|
+
value.forEach((item) => clonedArray.push(deepCloneStatic(item, seen)));
|
|
2039
|
+
return clonedArray;
|
|
2040
|
+
}
|
|
2041
|
+
const clonedObject = {};
|
|
2042
|
+
seen.set(value, clonedObject);
|
|
2043
|
+
Object.keys(value).forEach((key) => {
|
|
2044
|
+
clonedObject[key] = deepCloneStatic(value[key], seen);
|
|
2045
|
+
});
|
|
2046
|
+
return clonedObject;
|
|
2047
|
+
}
|
|
2048
|
+
function isResourceNode(obj) {
|
|
2049
|
+
return obj instanceof _Item || obj.attributes != null && typeof obj.attributes === "object";
|
|
2050
|
+
}
|
|
2051
|
+
function isReferenceStub(obj) {
|
|
2052
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj) || isResourceNode(obj)) {
|
|
2053
|
+
return false;
|
|
2054
|
+
}
|
|
2055
|
+
if (obj.id == null) {
|
|
2056
|
+
return false;
|
|
2057
|
+
}
|
|
2058
|
+
return Object.keys(obj).every((key) => key === "id" || key === "type");
|
|
2059
|
+
}
|
|
2060
|
+
function copyntransform(obj, cache = /* @__PURE__ */ new WeakMap()) {
|
|
2061
|
+
if (!obj) {
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
if (typeof obj !== "object") {
|
|
2065
|
+
return obj;
|
|
2066
|
+
}
|
|
2067
|
+
if (cache.has(obj)) {
|
|
2068
|
+
return cache.get(obj);
|
|
2069
|
+
}
|
|
2070
|
+
if (isReferenceStub(obj)) {
|
|
2071
|
+
const stub = { id: obj.id };
|
|
2072
|
+
cache.set(obj, stub);
|
|
2073
|
+
return stub;
|
|
2074
|
+
}
|
|
2075
|
+
if (!isResourceNode(obj)) {
|
|
2076
|
+
if (obj.id != null) {
|
|
2077
|
+
const flat = { id: obj.id };
|
|
2078
|
+
Object.keys(obj).forEach((key) => {
|
|
2079
|
+
if (key === "id" || key === "type" || key === "relationships" || key === "attributes") {
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
flat[key] = deepCloneStatic(obj[key]);
|
|
2083
|
+
});
|
|
2084
|
+
cache.set(obj, flat);
|
|
2085
|
+
return flat;
|
|
2086
|
+
}
|
|
2087
|
+
const cloned = deepCloneStatic(obj);
|
|
2088
|
+
cache.set(obj, cloned);
|
|
2089
|
+
return cloned;
|
|
2090
|
+
}
|
|
2091
|
+
const result = { id: obj.id };
|
|
2092
|
+
cache.set(obj, result);
|
|
2093
|
+
Object.assign(result, deepCloneStatic(obj.attributes ?? {}));
|
|
2094
|
+
Object.keys(obj.relationships ?? {}).forEach((relName) => {
|
|
2095
|
+
if (Array.isArray(obj.relationships[relName])) {
|
|
2096
|
+
result[relName] = obj.relationships[relName].map((item) => copyntransform(item, cache));
|
|
2097
|
+
} else {
|
|
2098
|
+
result[relName] = copyntransform(obj.relationships[relName], cache);
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
return result;
|
|
2102
|
+
}
|
|
2103
|
+
return copyntransform(this);
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Convert to JSON:API format
|
|
2107
|
+
*
|
|
2108
|
+
* Serializes item to JSON:API format for API requests.
|
|
2109
|
+
* This method is side-effect free - it does not mutate this.relationships.
|
|
2110
|
+
*
|
|
2111
|
+
* Runtime relationships (hydrated objects) are converted to JSON:API
|
|
2112
|
+
* relationship format: { data: { type, id } } or { data: [{ type, id }, ...] }
|
|
2113
|
+
*
|
|
2114
|
+
* @returns {Object} JSON:API formatted object
|
|
2115
|
+
*/
|
|
2116
|
+
toJSON() {
|
|
2117
|
+
let json = {
|
|
2118
|
+
type: this.type,
|
|
2119
|
+
attributes: this.attributes || {}
|
|
2120
|
+
};
|
|
2121
|
+
if (this.id) {
|
|
2122
|
+
json.id = this.id;
|
|
2123
|
+
}
|
|
2124
|
+
if (this.relationships && Object.keys(this.relationships).length > 0) {
|
|
2125
|
+
json.relationships = {};
|
|
2126
|
+
for (let relName in this.relationships) {
|
|
2127
|
+
if (!this.relationships.hasOwnProperty(relName)) {
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
const rel = this.relationships[relName];
|
|
2131
|
+
if (rel === null) {
|
|
2132
|
+
json.relationships[relName] = { data: null };
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
if (rel && typeof rel === "object" && !Array.isArray(rel)) {
|
|
2136
|
+
if (rel.type && rel.id) {
|
|
2137
|
+
json.relationships[relName] = {
|
|
2138
|
+
data: {
|
|
2139
|
+
type: rel.type,
|
|
2140
|
+
id: rel.id
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
} else if (rel.hasOwnProperty("toJSON")) {
|
|
2144
|
+
json.relationships[relName] = {
|
|
2145
|
+
data: rel.toJSON()
|
|
2146
|
+
};
|
|
2147
|
+
} else {
|
|
2148
|
+
continue;
|
|
2149
|
+
}
|
|
2150
|
+
continue;
|
|
2151
|
+
}
|
|
2152
|
+
if (Array.isArray(rel)) {
|
|
2153
|
+
json.relationships[relName] = {
|
|
2154
|
+
data: rel.map((item) => {
|
|
2155
|
+
if (item && typeof item === "object") {
|
|
2156
|
+
if (item.type && item.id) {
|
|
2157
|
+
return {
|
|
2158
|
+
type: item.type,
|
|
2159
|
+
id: item.id
|
|
2160
|
+
};
|
|
2161
|
+
} else if (item.hasOwnProperty("toJSON")) {
|
|
2162
|
+
return item.toJSON();
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
return item;
|
|
2166
|
+
})
|
|
2167
|
+
};
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
dbg("item.json", json);
|
|
2174
|
+
return json;
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Serialize a single relationship to JSON:API wire format
|
|
2178
|
+
*
|
|
2179
|
+
* Converts runtime relationship state (hydrated objects, arrays, null) to
|
|
2180
|
+
* JSON:API relationship format for wire transmission.
|
|
2181
|
+
*
|
|
2182
|
+
* IMPORTANT: This is a serialization function - it does NOT mutate runtime state.
|
|
2183
|
+
* Runtime relationships remain as hydrated objects/arrays/null.
|
|
2184
|
+
*
|
|
2185
|
+
* @param {Object|Array|null} rel - Runtime relationship value
|
|
2186
|
+
* @returns {Object} JSON:API relationship format: { data: { type, id } } or { data: [{ type, id }, ...] } or { data: null }
|
|
2187
|
+
*/
|
|
2188
|
+
_serializeRelationshipToWireFormat(rel) {
|
|
2189
|
+
return this.adapter.serializeRelationship(rel);
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Sync pending operations
|
|
2193
|
+
*/
|
|
2194
|
+
sync() {
|
|
2195
|
+
if (this.syncOp) {
|
|
2196
|
+
let syncOp = this.syncOp;
|
|
2197
|
+
dbg("Syncing", this, syncOp);
|
|
2198
|
+
this.syncOp = null;
|
|
2199
|
+
return syncOp();
|
|
2200
|
+
} else {
|
|
2201
|
+
dbg("Nothing to sync on", this);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Perform update operation
|
|
2206
|
+
*
|
|
2207
|
+
* Builds PATCH payload with changed attributes and relationships.
|
|
2208
|
+
*
|
|
2209
|
+
* IMPORTANT: Runtime relationship state (hydrated objects/arrays/null) is serialized
|
|
2210
|
+
* to JSON:API wire format ({ data: { type, id } }) for transmission. Runtime state
|
|
2211
|
+
* remains unchanged - this is a serialization layer, not a state mutation.
|
|
2212
|
+
*/
|
|
2213
|
+
perform_update(opts) {
|
|
2214
|
+
let options = {
|
|
2215
|
+
rerender: true
|
|
2216
|
+
};
|
|
2217
|
+
Object.assign(options, opts);
|
|
2218
|
+
return new Promise((resolve, reject) => {
|
|
2219
|
+
let toUpdate = {
|
|
2220
|
+
id: this.id,
|
|
2221
|
+
attributes: {},
|
|
2222
|
+
relationships: {}
|
|
2223
|
+
};
|
|
2224
|
+
if (this.type) {
|
|
2225
|
+
toUpdate.type = this.type;
|
|
2226
|
+
}
|
|
2227
|
+
Object.getOwnPropertyNames(this.attributes).forEach((attrName) => {
|
|
2228
|
+
if (this.shadow && this.shadow.attributes[attrName] !== this.attributes[attrName]) {
|
|
2229
|
+
toUpdate.attributes[attrName] = this.attributes[attrName];
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
Object.getOwnPropertyNames(this.relationships).forEach((relaName) => {
|
|
2233
|
+
if (this.shadow && this.shadow.relationships[relaName] !== this.relationships[relaName]) {
|
|
2234
|
+
const runtimeRel = this.relationships[relaName];
|
|
2235
|
+
toUpdate.relationships[relaName] = this._serializeRelationshipToWireFormat(runtimeRel);
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
if (!Object.getOwnPropertyNames(toUpdate.attributes).length && !Object.getOwnPropertyNames(toUpdate.relationships).length) {
|
|
2239
|
+
this.syncOp = null;
|
|
2240
|
+
resolve(this);
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
const payload = this.adapter.serializeForUpdate(toUpdate);
|
|
2244
|
+
if (opts && opts.justSimulate) {
|
|
2245
|
+
dbg(payload.body);
|
|
2246
|
+
resolve(this);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
let updateUrlString = this.updateUrl.toString ? this.updateUrl.toString() : this.updateUrl;
|
|
2250
|
+
this.storage.update(this, updateUrlString, { contentType: payload.contentType }, payload.body).then((resp) => {
|
|
2251
|
+
let newData = this.adapter.parseItemResponse(resp.data);
|
|
2252
|
+
Object.assign(this, newData);
|
|
2253
|
+
this.shadow = null;
|
|
2254
|
+
if (options.rerender) {
|
|
2255
|
+
this.views.forEach((view) => {
|
|
2256
|
+
view.render();
|
|
2257
|
+
this._trigger("afterrender", this, view);
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
this._trigger("update", this);
|
|
2261
|
+
if (this.collection) {
|
|
2262
|
+
this.collection.onupdate();
|
|
2263
|
+
}
|
|
2264
|
+
resolve(this);
|
|
2265
|
+
}).catch((error2) => {
|
|
2266
|
+
dbg("Update NOK", this.updateUrl, patchData, error2);
|
|
2267
|
+
if (error2 instanceof Error && error2.jqXHR) {
|
|
2268
|
+
reject(error2);
|
|
2269
|
+
} else if (error2.jqXHR) {
|
|
2270
|
+
reject(error2);
|
|
2271
|
+
} else {
|
|
2272
|
+
reject(error2 instanceof Error ? error2 : new Error(String(error2)));
|
|
2273
|
+
}
|
|
2274
|
+
});
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Update item
|
|
2279
|
+
*/
|
|
2280
|
+
update(updateData, opts) {
|
|
2281
|
+
if (!updateData || updateData.constructor !== Object) {
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
let updateOptions = {
|
|
2285
|
+
sync: true,
|
|
2286
|
+
rerender: true
|
|
2287
|
+
};
|
|
2288
|
+
if (opts && opts.constructor === Object) {
|
|
2289
|
+
Object.assign(updateOptions, opts);
|
|
2290
|
+
}
|
|
2291
|
+
if (!this.shadow) {
|
|
2292
|
+
this.shadow = { attributes: {}, relationships: {} };
|
|
2293
|
+
Object.assign(this.shadow.attributes, this.attributes);
|
|
2294
|
+
Object.assign(this.shadow.relationships, this.relationships);
|
|
2295
|
+
}
|
|
2296
|
+
const updateRelation = (rel, data) => {
|
|
2297
|
+
dbg("update relation", rel, data);
|
|
2298
|
+
if (data === null) {
|
|
2299
|
+
return null;
|
|
2300
|
+
}
|
|
2301
|
+
if (rel && Array.isArray(rel)) {
|
|
2302
|
+
if (Array.isArray(data)) {
|
|
2303
|
+
return data.map((item) => {
|
|
2304
|
+
if (typeof item === "object" && item !== null) {
|
|
2305
|
+
return new _Item().loadFromData(item);
|
|
2306
|
+
}
|
|
2307
|
+
return item;
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
dbg("to fix: array relationship update");
|
|
2311
|
+
return rel;
|
|
2312
|
+
}
|
|
2313
|
+
if (typeof data === "object" || data === null) {
|
|
2314
|
+
dbg("Update 1:1 relation");
|
|
2315
|
+
let item = new _Item().loadFromData(data);
|
|
2316
|
+
dbg("relation", item);
|
|
2317
|
+
return item;
|
|
2318
|
+
}
|
|
2319
|
+
if (typeof data === "string" || typeof data === "number") {
|
|
2320
|
+
dbg("Update 1:1 relation with id", data);
|
|
2321
|
+
if (rel && rel.id && (rel.id === data || String(rel.id) === String(data))) {
|
|
2322
|
+
return rel;
|
|
2323
|
+
}
|
|
2324
|
+
const newRel = {
|
|
2325
|
+
id: String(data)
|
|
2326
|
+
};
|
|
2327
|
+
if (rel && rel.type) {
|
|
2328
|
+
newRel.type = rel.type;
|
|
2329
|
+
}
|
|
2330
|
+
return newRel;
|
|
2331
|
+
}
|
|
2332
|
+
return rel;
|
|
2333
|
+
};
|
|
2334
|
+
Object.getOwnPropertyNames(this.relationships).forEach((relName) => {
|
|
2335
|
+
if (!updateData.hasOwnProperty(relName)) {
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
if (updateData[relName] === null) {
|
|
2339
|
+
this.relationships[relName] = null;
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
this.relationships[relName] = updateRelation(this.relationships[relName], updateData[relName]);
|
|
2343
|
+
delete updateData[relName];
|
|
2344
|
+
});
|
|
2345
|
+
Object.getOwnPropertyNames(updateData).forEach((attrName) => {
|
|
2346
|
+
if (updateData[attrName] && typeof updateData[attrName] === "object") {
|
|
2347
|
+
if (!this.strict && typeof this.relationships[attrName] === "undefined") {
|
|
2348
|
+
this.relationships[attrName] = updateRelation(this.relationships[attrName], updateData[attrName]);
|
|
2349
|
+
}
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
if (!this.shadow.attributes.hasOwnProperty(attrName)) {
|
|
2353
|
+
if (!this.strict) {
|
|
2354
|
+
this.attributes[attrName] = updateData[attrName];
|
|
2355
|
+
}
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
if (updateData[attrName] !== this.shadow.attributes[attrName]) {
|
|
2359
|
+
this.attributes[attrName] = updateData[attrName];
|
|
2360
|
+
}
|
|
2361
|
+
});
|
|
2362
|
+
dbg("updateOptions", updateOptions);
|
|
2363
|
+
if (updateOptions.sync) {
|
|
2364
|
+
return this.perform_update(updateOptions);
|
|
2365
|
+
}
|
|
2366
|
+
return new Promise((resolve) => {
|
|
2367
|
+
this.syncOp = () => this.perform_update(updateOptions);
|
|
2368
|
+
this.views.forEach((view) => {
|
|
2369
|
+
if (updateOptions.rerender) {
|
|
2370
|
+
view.render();
|
|
2371
|
+
}
|
|
2372
|
+
});
|
|
2373
|
+
resolve();
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Remove item
|
|
2378
|
+
*/
|
|
2379
|
+
remove() {
|
|
2380
|
+
return new Promise((resolve, reject) => {
|
|
2381
|
+
let ps = [];
|
|
2382
|
+
for (let i = this.views.length - 1; i >= 0; i--) {
|
|
2383
|
+
ps.push(this.views[i].remove());
|
|
2384
|
+
}
|
|
2385
|
+
let collection = this.collection;
|
|
2386
|
+
if (collection) {
|
|
2387
|
+
ps.push(collection.removeItem(this));
|
|
2388
|
+
}
|
|
2389
|
+
Promise.all(ps).then(() => {
|
|
2390
|
+
this._trigger("remove", this);
|
|
2391
|
+
if (collection) {
|
|
2392
|
+
console.log("removed");
|
|
2393
|
+
collection.onupdate();
|
|
2394
|
+
}
|
|
2395
|
+
}).finally(() => resolve());
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
/**
|
|
2399
|
+
* Delete item
|
|
2400
|
+
*/
|
|
2401
|
+
async delete(ops) {
|
|
2402
|
+
if (!this.deleteUrl) {
|
|
2403
|
+
return this.remove();
|
|
2404
|
+
}
|
|
2405
|
+
let deleteOps = {
|
|
2406
|
+
sync: true
|
|
2407
|
+
};
|
|
2408
|
+
if (ops && ops.constructor === Object) {
|
|
2409
|
+
Object.assign(deleteOps, ops);
|
|
2410
|
+
}
|
|
2411
|
+
try {
|
|
2412
|
+
log("delete", this.deleteUrl.toString());
|
|
2413
|
+
await this.storage.delete(this, this.deleteUrl.toString(), {});
|
|
2414
|
+
await this.remove();
|
|
2415
|
+
} catch (error2) {
|
|
2416
|
+
dbg("Error deleting item", error2);
|
|
2417
|
+
throw error2;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Render item
|
|
2422
|
+
*/
|
|
2423
|
+
render(collectionView, addontop = false) {
|
|
2424
|
+
dbg("Render from item", this);
|
|
2425
|
+
this.views.forEach((view) => {
|
|
2426
|
+
if (typeof collectionView === "undefined") {
|
|
2427
|
+
dbg("collectionView is undefined so render view");
|
|
2428
|
+
view.render();
|
|
2429
|
+
} else if (view.container === collectionView) {
|
|
2430
|
+
dbg("collectionView matches view container so render view");
|
|
2431
|
+
view.render(false, addontop);
|
|
2432
|
+
}
|
|
2433
|
+
dbg("trigger afterrender", this, view, view.el);
|
|
2434
|
+
this._trigger("afterrender", this, view);
|
|
2435
|
+
});
|
|
2436
|
+
return this;
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Destroy item and clean up resources
|
|
2440
|
+
*
|
|
2441
|
+
* Removes event handlers, views, and clears references.
|
|
2442
|
+
* Safe to call multiple times.
|
|
2443
|
+
*
|
|
2444
|
+
* @returns {Item} This instance for chaining
|
|
2445
|
+
*/
|
|
2446
|
+
destroy() {
|
|
2447
|
+
const viewsToDestroy = this.views ? [...this.views] : [];
|
|
2448
|
+
viewsToDestroy.forEach((view) => {
|
|
2449
|
+
if (view && typeof view.destroy === "function") {
|
|
2450
|
+
view.destroy();
|
|
2451
|
+
}
|
|
2452
|
+
});
|
|
2453
|
+
this.views = [];
|
|
2454
|
+
this.callbacks = {};
|
|
2455
|
+
if (this.collection) {
|
|
2456
|
+
this.collection = null;
|
|
2457
|
+
}
|
|
2458
|
+
this.storage = null;
|
|
2459
|
+
this.url = null;
|
|
2460
|
+
this.updateUrl = null;
|
|
2461
|
+
this.deleteUrl = null;
|
|
2462
|
+
this.attributes = {};
|
|
2463
|
+
this.relationships = {};
|
|
2464
|
+
this.shadow = null;
|
|
2465
|
+
this.views = [];
|
|
2466
|
+
return this;
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
|
|
2470
|
+
// src/CollectionView.js
|
|
2471
|
+
var CollectionView = class {
|
|
2472
|
+
constructor(options = {}) {
|
|
2473
|
+
this.el = null;
|
|
2474
|
+
this.type = "CollectionView";
|
|
2475
|
+
this.container = null;
|
|
2476
|
+
this.collection = null;
|
|
2477
|
+
this.itemsContainer = null;
|
|
2478
|
+
this.allowempty = true;
|
|
2479
|
+
try {
|
|
2480
|
+
Object.assign(this, parseOptions(options));
|
|
2481
|
+
} catch (e) {
|
|
2482
|
+
throw new Error("Error on CollectionView init", e);
|
|
2483
|
+
}
|
|
2484
|
+
this.dataBindings = getBoundObjects(this.el);
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Reset the view
|
|
2488
|
+
*/
|
|
2489
|
+
reset(force) {
|
|
2490
|
+
if (this.allowempty || force) {
|
|
2491
|
+
if (this.el) {
|
|
2492
|
+
$(this.el).empty();
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
return this;
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Render the collection view
|
|
2499
|
+
*/
|
|
2500
|
+
render() {
|
|
2501
|
+
dbg("Render _collectionView", this.collection);
|
|
2502
|
+
if (this.collection && this.collection.navtype === "page") {
|
|
2503
|
+
this.reset();
|
|
2504
|
+
}
|
|
2505
|
+
if (this.collection && this.collection.items.length === 0) {
|
|
2506
|
+
this.renderEmpty();
|
|
2507
|
+
return this;
|
|
2508
|
+
}
|
|
2509
|
+
if (this.collection) {
|
|
2510
|
+
this.collection.items.forEach((item) => {
|
|
2511
|
+
item.render(this);
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
return this;
|
|
2515
|
+
}
|
|
2516
|
+
/**
|
|
2517
|
+
* Render empty state
|
|
2518
|
+
*/
|
|
2519
|
+
renderEmpty() {
|
|
2520
|
+
if (!this.collection || !this.collection.emptyview) {
|
|
2521
|
+
return this;
|
|
2522
|
+
}
|
|
2523
|
+
this.reset();
|
|
2524
|
+
$(this.el).append(this.collection.emptyview);
|
|
2525
|
+
return this;
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* Destroy view and clean up resources
|
|
2529
|
+
*/
|
|
2530
|
+
destroy() {
|
|
2531
|
+
if (this.el) {
|
|
2532
|
+
const $el = $(this.el);
|
|
2533
|
+
$el.empty();
|
|
2534
|
+
$el.removeData();
|
|
2535
|
+
}
|
|
2536
|
+
this.collection = null;
|
|
2537
|
+
this.el = null;
|
|
2538
|
+
this.container = null;
|
|
2539
|
+
this.itemsContainer = null;
|
|
2540
|
+
this.dataBindings = null;
|
|
2541
|
+
return this;
|
|
2542
|
+
}
|
|
2543
|
+
};
|
|
2544
|
+
|
|
2545
|
+
// src/Paging.js
|
|
2546
|
+
var Paging = class {
|
|
2547
|
+
constructor(pagingEl, collection) {
|
|
2548
|
+
this.collection = collection;
|
|
2549
|
+
this.el = $(pagingEl);
|
|
2550
|
+
this.collection.paging = this;
|
|
2551
|
+
this.iniOffset = (this.collection.offset ? this.collection.offset : 0) * 1;
|
|
2552
|
+
this.defaultPageSize = 20;
|
|
2553
|
+
this.pageSize = this.collection.pageSize;
|
|
2554
|
+
this.setupPageSizeInput();
|
|
2555
|
+
this.setupOffsetInput();
|
|
2556
|
+
this.buttons = this.extractButtons();
|
|
2557
|
+
log("buttons", this.buttons);
|
|
2558
|
+
this.setupTotalCount();
|
|
2559
|
+
this.render();
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Setup page size input handler
|
|
2563
|
+
*/
|
|
2564
|
+
setupPageSizeInput() {
|
|
2565
|
+
let pageSizeInp = $(this.collection.pagesizeinp);
|
|
2566
|
+
if (pageSizeInp.length) {
|
|
2567
|
+
this.collection.setPageSize(pageSizeInp.val());
|
|
2568
|
+
pageSizeInp.off("change").on("change", () => {
|
|
2569
|
+
if (this.collection.setPageSize(pageSizeInp.val())) {
|
|
2570
|
+
this.collection.loadFromRemote();
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* Setup offset input handler
|
|
2577
|
+
*/
|
|
2578
|
+
setupOffsetInput() {
|
|
2579
|
+
let offsetInp = $(this.collection.offsetinp);
|
|
2580
|
+
if (offsetInp.length) {
|
|
2581
|
+
this.collection.setOffset(offsetInp.val());
|
|
2582
|
+
offsetInp.off("change").on("change", () => {
|
|
2583
|
+
if (this.collection.setOffset(offsetInp.val())) {
|
|
2584
|
+
this.collection.loadFromRemote();
|
|
2585
|
+
}
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Extract button templates from container
|
|
2591
|
+
*/
|
|
2592
|
+
extractButtons() {
|
|
2593
|
+
let buttons = {};
|
|
2594
|
+
const pageBtn = $(this.el).find("[name=page]");
|
|
2595
|
+
if (pageBtn.length) {
|
|
2596
|
+
buttons.page = pageBtn.clone();
|
|
2597
|
+
pageBtn.remove();
|
|
2598
|
+
}
|
|
2599
|
+
const prevBtn = $(this.el).find("[name=prev]");
|
|
2600
|
+
if (prevBtn.length) {
|
|
2601
|
+
buttons.prev = prevBtn.clone();
|
|
2602
|
+
prevBtn.remove();
|
|
2603
|
+
}
|
|
2604
|
+
const nextBtn = $(this.el).find("[name=next]");
|
|
2605
|
+
if (nextBtn.length) {
|
|
2606
|
+
buttons.next = nextBtn.clone();
|
|
2607
|
+
nextBtn.remove();
|
|
2608
|
+
}
|
|
2609
|
+
const firstBtn = $(this.el).find("[name=first]");
|
|
2610
|
+
if (firstBtn.length) {
|
|
2611
|
+
buttons.first = firstBtn.clone();
|
|
2612
|
+
firstBtn.remove();
|
|
2613
|
+
}
|
|
2614
|
+
const lastBtn = $(this.el).find("[name=last]");
|
|
2615
|
+
if (lastBtn.length) {
|
|
2616
|
+
buttons.last = lastBtn.clone();
|
|
2617
|
+
lastBtn.remove();
|
|
2618
|
+
}
|
|
2619
|
+
return buttons;
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2622
|
+
* Setup total count element
|
|
2623
|
+
*/
|
|
2624
|
+
setupTotalCount() {
|
|
2625
|
+
this.$totalCount = $(this.collection.totalrecscount);
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* Clear container
|
|
2629
|
+
*/
|
|
2630
|
+
clearContainer() {
|
|
2631
|
+
$(this.el).empty();
|
|
2632
|
+
$(this.el).find("[data-type=pages]").empty();
|
|
2633
|
+
}
|
|
2634
|
+
/**
|
|
2635
|
+
* Update total count display
|
|
2636
|
+
*/
|
|
2637
|
+
updateTotalCount(total) {
|
|
2638
|
+
if (this.$totalCount && this.$totalCount.length) {
|
|
2639
|
+
if (this.$totalCount[0].tagName === "INPUT") {
|
|
2640
|
+
this.$totalCount.val(total);
|
|
2641
|
+
} else {
|
|
2642
|
+
this.$totalCount.text(total);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Create and append button element
|
|
2648
|
+
*/
|
|
2649
|
+
appendButton(button, clickHandler, title) {
|
|
2650
|
+
let btn = button.clone();
|
|
2651
|
+
if (title !== void 0) {
|
|
2652
|
+
btn.attr("title", title);
|
|
2653
|
+
}
|
|
2654
|
+
btn.on("click", clickHandler);
|
|
2655
|
+
$(this.el).append(btn);
|
|
2656
|
+
return btn;
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Render pagination controls
|
|
2660
|
+
*/
|
|
2661
|
+
render() {
|
|
2662
|
+
const pagesToShow = 5;
|
|
2663
|
+
const total = this.collection.total;
|
|
2664
|
+
log("Paging render", total, this.buttons);
|
|
2665
|
+
this.updateTotalCount(total);
|
|
2666
|
+
this.clearContainer();
|
|
2667
|
+
this.iniOffset = this.collection.offset * 1;
|
|
2668
|
+
if (this.collection.pageSize) {
|
|
2669
|
+
this.pageSize = this.collection.pageSize;
|
|
2670
|
+
} else if (total - this.iniOffset - this.collection.items.length > 0) {
|
|
2671
|
+
this.pageSize = this.collection.items.length;
|
|
2672
|
+
} else {
|
|
2673
|
+
this.pageSize = this.defaultPageSize;
|
|
2674
|
+
}
|
|
2675
|
+
this.pageSize = this.pageSize * 1;
|
|
2676
|
+
log("Paging obj", this, this.pageSize, total);
|
|
2677
|
+
if (this.pageSize > total) {
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
if (this.iniOffset > 0) {
|
|
2681
|
+
if (this.buttons.first) {
|
|
2682
|
+
this.appendButton(
|
|
2683
|
+
this.buttons.first,
|
|
2684
|
+
() => {
|
|
2685
|
+
this.collection.setOffset(0);
|
|
2686
|
+
this.collection.loadFromRemote();
|
|
2687
|
+
},
|
|
2688
|
+
0
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
if (this.buttons.prev) {
|
|
2692
|
+
this.appendButton(
|
|
2693
|
+
this.buttons.prev,
|
|
2694
|
+
() => {
|
|
2695
|
+
this.collection.setOffset(this.iniOffset - this.pageSize);
|
|
2696
|
+
this.collection.loadFromRemote();
|
|
2697
|
+
},
|
|
2698
|
+
this.iniOffset - this.pageSize
|
|
2699
|
+
);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
let lowerLimit = Math.floor(this.iniOffset / this.pageSize) - Math.floor(pagesToShow / 2);
|
|
2703
|
+
lowerLimit = lowerLimit < 0 ? 0 : lowerLimit;
|
|
2704
|
+
let upperLimit = Math.floor(this.iniOffset / this.pageSize) + Math.ceil(pagesToShow / 2);
|
|
2705
|
+
upperLimit = upperLimit * this.pageSize < total ? upperLimit : Math.ceil(total / this.pageSize);
|
|
2706
|
+
for (let i = lowerLimit; i < upperLimit; i++) {
|
|
2707
|
+
if (!this.buttons.page) {
|
|
2708
|
+
continue;
|
|
2709
|
+
}
|
|
2710
|
+
const pageOffset = i * this.pageSize;
|
|
2711
|
+
const isActive = Math.floor(this.iniOffset / this.pageSize) === i;
|
|
2712
|
+
let pageBtn = this.buttons.page.clone();
|
|
2713
|
+
pageBtn.text(i + 1).attr("title", pageOffset).on("click", () => {
|
|
2714
|
+
this.collection.setOffset(pageOffset);
|
|
2715
|
+
this.collection.loadFromRemote();
|
|
2716
|
+
});
|
|
2717
|
+
if (isActive) {
|
|
2718
|
+
pageBtn.addClass("active").off("click");
|
|
2719
|
+
}
|
|
2720
|
+
$(this.el).append(pageBtn);
|
|
2721
|
+
}
|
|
2722
|
+
const nxtOffset = this.iniOffset + this.pageSize;
|
|
2723
|
+
if (this.iniOffset + this.pageSize < total) {
|
|
2724
|
+
if (this.buttons.next) {
|
|
2725
|
+
this.appendButton(
|
|
2726
|
+
this.buttons.next,
|
|
2727
|
+
() => {
|
|
2728
|
+
this.collection.setOffset(nxtOffset);
|
|
2729
|
+
this.collection.loadFromRemote();
|
|
2730
|
+
},
|
|
2731
|
+
nxtOffset
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
if (this.buttons.last) {
|
|
2735
|
+
const lastPageOffset = (Math.ceil(total / this.pageSize) - 1) * this.pageSize;
|
|
2736
|
+
log("last button", total, lastPageOffset, this.pageSize, this.offset);
|
|
2737
|
+
if (lastPageOffset > this.iniOffset * 1) {
|
|
2738
|
+
this.appendButton(
|
|
2739
|
+
this.buttons.last,
|
|
2740
|
+
() => {
|
|
2741
|
+
this.collection.setOffset(lastPageOffset);
|
|
2742
|
+
this.collection.loadFromRemote();
|
|
2743
|
+
},
|
|
2744
|
+
lastPageOffset
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
let offsetInp = $(this.collection.offsetinp);
|
|
2750
|
+
if (offsetInp.length) {
|
|
2751
|
+
offsetInp.val(this.iniOffset);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* Destroy paging and clean up resources
|
|
2756
|
+
*/
|
|
2757
|
+
destroy() {
|
|
2758
|
+
if (this.collection && this.collection.pagesizeinp) {
|
|
2759
|
+
const pageSizeInp = $(this.collection.pagesizeinp);
|
|
2760
|
+
pageSizeInp.off("change");
|
|
2761
|
+
}
|
|
2762
|
+
if (this.collection && this.collection.offsetinp) {
|
|
2763
|
+
const offsetInp = $(this.collection.offsetinp);
|
|
2764
|
+
offsetInp.off("change");
|
|
2765
|
+
}
|
|
2766
|
+
if (this.el) {
|
|
2767
|
+
const $el = $(this.el);
|
|
2768
|
+
$el.empty();
|
|
2769
|
+
$el.off();
|
|
2770
|
+
$el.removeData();
|
|
2771
|
+
}
|
|
2772
|
+
this.collection = null;
|
|
2773
|
+
this.el = null;
|
|
2774
|
+
this.buttons = null;
|
|
2775
|
+
this.$totalCount = null;
|
|
2776
|
+
return this;
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
|
|
2780
|
+
// src/Collection.js
|
|
2781
|
+
var Collection = class {
|
|
2782
|
+
constructor(opts = {}) {
|
|
2783
|
+
this.url = null;
|
|
2784
|
+
this.deleteUrl = null;
|
|
2785
|
+
this.insertUrl = null;
|
|
2786
|
+
this.updateUrl = null;
|
|
2787
|
+
this.paging = null;
|
|
2788
|
+
this.view = null;
|
|
2789
|
+
this.offset = 0;
|
|
2790
|
+
this.total = null;
|
|
2791
|
+
this.pageSize = 10;
|
|
2792
|
+
this.template = null;
|
|
2793
|
+
this.navtype = "page";
|
|
2794
|
+
this.type = null;
|
|
2795
|
+
this.emptyview = null;
|
|
2796
|
+
this.items = [];
|
|
2797
|
+
this.addontop = false;
|
|
2798
|
+
this.uievents = [];
|
|
2799
|
+
this.setAttrAsId = null;
|
|
2800
|
+
this.itemListeners = null;
|
|
2801
|
+
this.adapter = null;
|
|
2802
|
+
this.callbacks = {};
|
|
2803
|
+
this.iterator = -1;
|
|
2804
|
+
trace("Collection init", opts);
|
|
2805
|
+
try {
|
|
2806
|
+
opts = parseOptions(opts);
|
|
2807
|
+
} catch (e) {
|
|
2808
|
+
throw new Error("Error on Collection init", e);
|
|
2809
|
+
}
|
|
2810
|
+
Object.defineProperty(this, "length", {
|
|
2811
|
+
get() {
|
|
2812
|
+
return this.items.length;
|
|
2813
|
+
},
|
|
2814
|
+
enumerable: true,
|
|
2815
|
+
configurable: true
|
|
2816
|
+
});
|
|
2817
|
+
let options = Object.assign({}, opts);
|
|
2818
|
+
Object.assign(this, options);
|
|
2819
|
+
if (options.hasOwnProperty("paging") && $(options.paging).length) {
|
|
2820
|
+
this.paging = new Paging($(options.paging)[0], this);
|
|
2821
|
+
}
|
|
2822
|
+
if (this.url) {
|
|
2823
|
+
this.setUrl(this.url);
|
|
2824
|
+
}
|
|
2825
|
+
if (this.deleteUrl) {
|
|
2826
|
+
this.setUrl(this.deleteUrl, "delete");
|
|
2827
|
+
}
|
|
2828
|
+
if (this.updateUrl) {
|
|
2829
|
+
this.setUrl(this.updateUrl, "update");
|
|
2830
|
+
}
|
|
2831
|
+
if (this.insertUrl) {
|
|
2832
|
+
this.setUrl(this.insertUrl, "insert");
|
|
2833
|
+
}
|
|
2834
|
+
if (this.view) {
|
|
2835
|
+
this.view.collection = this;
|
|
2836
|
+
}
|
|
2837
|
+
if (this.total) {
|
|
2838
|
+
this.total = this.total * 1;
|
|
2839
|
+
}
|
|
2840
|
+
if (["page", "scroll"].indexOf(this.navtype) === -1) {
|
|
2841
|
+
throw new Error("Invalid navigations type. Should be page or scroll");
|
|
2842
|
+
}
|
|
2843
|
+
this.adapter = resolveAdapter(opts.adapter);
|
|
2844
|
+
this.storage = opts.hasOwnProperty("storage") ? opts.storage : new Storage(
|
|
2845
|
+
(() => {
|
|
2846
|
+
const storageOpts = Object.assign({}, opts.ajaxOpts || {});
|
|
2847
|
+
if (opts.headers && typeof opts.headers === "object") {
|
|
2848
|
+
storageOpts.headers = Object.assign(
|
|
2849
|
+
{},
|
|
2850
|
+
storageOpts.headers || {},
|
|
2851
|
+
opts.headers
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
return storageOpts;
|
|
2855
|
+
})()
|
|
2856
|
+
);
|
|
2857
|
+
if (typeof opts.listeners === "object") {
|
|
2858
|
+
for (let event in opts.listeners) {
|
|
2859
|
+
this.on(event, opts.listeners[event]);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
if (opts.itemListeners && typeof opts.itemListeners === "object") {
|
|
2863
|
+
this.itemListeners = opts.itemListeners;
|
|
2864
|
+
} else if (opts.itemOn && typeof opts.itemOn === "object") {
|
|
2865
|
+
this.itemListeners = opts.itemOn;
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Event listener registration
|
|
2870
|
+
*/
|
|
2871
|
+
on(eventName, cb) {
|
|
2872
|
+
if (typeof this.callbacks[eventName] === "undefined") {
|
|
2873
|
+
this.callbacks[eventName] = [];
|
|
2874
|
+
}
|
|
2875
|
+
this.callbacks[eventName].push(cb);
|
|
2876
|
+
return this;
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Remove event listener(s)
|
|
2880
|
+
* @param {string} eventName - Event name
|
|
2881
|
+
* @param {Function} [cb] - Optional callback to remove. If not provided, removes all listeners for the event
|
|
2882
|
+
* @returns {Collection} This instance for chaining
|
|
2883
|
+
*/
|
|
2884
|
+
off(eventName, cb) {
|
|
2885
|
+
if (!eventName) {
|
|
2886
|
+
this.callbacks = {};
|
|
2887
|
+
return this;
|
|
2888
|
+
}
|
|
2889
|
+
if (!this.callbacks[eventName]) {
|
|
2890
|
+
return this;
|
|
2891
|
+
}
|
|
2892
|
+
if (cb) {
|
|
2893
|
+
const index = this.callbacks[eventName].indexOf(cb);
|
|
2894
|
+
if (index > -1) {
|
|
2895
|
+
this.callbacks[eventName].splice(index, 1);
|
|
2896
|
+
}
|
|
2897
|
+
if (this.callbacks[eventName].length === 0) {
|
|
2898
|
+
delete this.callbacks[eventName];
|
|
2899
|
+
}
|
|
2900
|
+
} else {
|
|
2901
|
+
delete this.callbacks[eventName];
|
|
2902
|
+
}
|
|
2903
|
+
return this;
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Register a one-time event listener
|
|
2907
|
+
* @param {string} eventName - Event name
|
|
2908
|
+
* @param {Function} cb - Callback function
|
|
2909
|
+
* @returns {Collection} This instance for chaining
|
|
2910
|
+
*/
|
|
2911
|
+
once(eventName, cb) {
|
|
2912
|
+
const wrapper = (...args) => {
|
|
2913
|
+
cb(...args);
|
|
2914
|
+
this.off(eventName, wrapper);
|
|
2915
|
+
};
|
|
2916
|
+
return this.on(eventName, wrapper);
|
|
2917
|
+
}
|
|
2918
|
+
/**
|
|
2919
|
+
* Check if event has listeners
|
|
2920
|
+
* @param {string} eventName - Event name
|
|
2921
|
+
* @returns {boolean} True if event has listeners
|
|
2922
|
+
*/
|
|
2923
|
+
hasListeners(eventName) {
|
|
2924
|
+
return this.callbacks[eventName] && Array.isArray(this.callbacks[eventName]) && this.callbacks[eventName].length > 0;
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Trigger an event (internal helper)
|
|
2928
|
+
* @private
|
|
2929
|
+
* @param {string} eventName - Event name
|
|
2930
|
+
* @param {...any} args - Arguments to pass to callbacks
|
|
2931
|
+
*/
|
|
2932
|
+
_trigger(eventName, ...args) {
|
|
2933
|
+
if (this.callbacks[eventName] && Array.isArray(this.callbacks[eventName])) {
|
|
2934
|
+
this.callbacks[eventName].forEach((cb) => {
|
|
2935
|
+
if (typeof cb === "function") {
|
|
2936
|
+
cb(...args);
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* Emit/trigger an event manually
|
|
2943
|
+
* @param {string} eventName - Event name
|
|
2944
|
+
* @param {...any} args - Arguments to pass to callbacks
|
|
2945
|
+
* @returns {Collection} This instance for chaining
|
|
2946
|
+
*/
|
|
2947
|
+
emit(eventName, ...args) {
|
|
2948
|
+
this._trigger(eventName, ...args);
|
|
2949
|
+
return this;
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Show listeners (debug)
|
|
2953
|
+
*/
|
|
2954
|
+
showlisteners() {
|
|
2955
|
+
dbg(this.callbacks);
|
|
2956
|
+
}
|
|
2957
|
+
/**
|
|
2958
|
+
* Remove item from collection
|
|
2959
|
+
*
|
|
2960
|
+
* Removes item from items array. Does NOT trigger update event
|
|
2961
|
+
* (that's handled by Item.remove() to avoid duplication).
|
|
2962
|
+
*
|
|
2963
|
+
* @param {Item} item - Item instance to remove
|
|
2964
|
+
* @returns {Promise} Resolves when item is removed
|
|
2965
|
+
*/
|
|
2966
|
+
removeItem(item) {
|
|
2967
|
+
const index = this.items.findIndex((i) => i === item || i.id && item.id && i.id === item.id);
|
|
2968
|
+
if (index !== -1) {
|
|
2969
|
+
this.items.splice(index, 1);
|
|
2970
|
+
}
|
|
2971
|
+
return Promise.resolve();
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Set page size
|
|
2975
|
+
*/
|
|
2976
|
+
setPageSize(val) {
|
|
2977
|
+
if (/^\d+$/.test(val)) {
|
|
2978
|
+
this.pageSize = val;
|
|
2979
|
+
return true;
|
|
2980
|
+
}
|
|
2981
|
+
return false;
|
|
2982
|
+
}
|
|
2983
|
+
/**
|
|
2984
|
+
* Empty collection
|
|
2985
|
+
*/
|
|
2986
|
+
empty() {
|
|
2987
|
+
return this.clear();
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Set offset
|
|
2991
|
+
*/
|
|
2992
|
+
setOffset(val) {
|
|
2993
|
+
if (/^\d+$/.test(val)) {
|
|
2994
|
+
this.offset = val;
|
|
2995
|
+
return true;
|
|
2996
|
+
}
|
|
2997
|
+
return false;
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Bulk update (not implemented)
|
|
3001
|
+
*/
|
|
3002
|
+
update(data) {
|
|
3003
|
+
throw new Error("Not implemented... yet");
|
|
3004
|
+
}
|
|
3005
|
+
setUrl(url, type) {
|
|
3006
|
+
if (!url)
|
|
3007
|
+
return this;
|
|
3008
|
+
switch (type) {
|
|
3009
|
+
case "delete":
|
|
3010
|
+
this.deleteUrl = createURL(url);
|
|
3011
|
+
break;
|
|
3012
|
+
case "update":
|
|
3013
|
+
this.updateUrl = createURL(url);
|
|
3014
|
+
break;
|
|
3015
|
+
case "insert":
|
|
3016
|
+
this.insertUrl = createURL(url);
|
|
3017
|
+
break;
|
|
3018
|
+
default:
|
|
3019
|
+
log("setUrl", url);
|
|
3020
|
+
this.url = createURL(url);
|
|
3021
|
+
this.deleteUrl = typeof this.deleteUrl == "string" ? createURL(this.deleteUrl) : this.deleteUrl ?? createURL(this.url);
|
|
3022
|
+
this.updateUrl = typeof this.updateUrl == "string" ? createURL(this.updateUrl) : this.updateUrl ?? createURL(this.url);
|
|
3023
|
+
this.insertUrl = typeof this.insertUrl == "string" ? createURL(this.insertUrl) : this.insertUrl ?? createURL(this.url);
|
|
3024
|
+
break;
|
|
3025
|
+
}
|
|
3026
|
+
return this;
|
|
3027
|
+
}
|
|
3028
|
+
/**
|
|
3029
|
+
* Receive remote data
|
|
3030
|
+
*
|
|
3031
|
+
* Processes JSON:API document by hydrating relationships and extracting data array.
|
|
3032
|
+
* Uses the new explicit hydration layer to replace relationship references with
|
|
3033
|
+
* actual resource objects from included resources.
|
|
3034
|
+
*
|
|
3035
|
+
* IMPORTANT: For single item responses (e.g., from append/create), uses parseItemData()
|
|
3036
|
+
* to avoid wrapping in array and triggering collection replacement behavior.
|
|
3037
|
+
*/
|
|
3038
|
+
receiveRemoteData(data) {
|
|
3039
|
+
dbg("Remote data received", data);
|
|
3040
|
+
if (this.adapter.isSingleItemResponse(data)) {
|
|
3041
|
+
const hydratedItem = this.adapter.parseItemResponse(data);
|
|
3042
|
+
this.adapter.applyMetadata(this, this.adapter.extractMetadata(data));
|
|
3043
|
+
if (this.items.length === 0) {
|
|
3044
|
+
this.view.reset(true);
|
|
3045
|
+
}
|
|
3046
|
+
dbg("Append single item to collection");
|
|
3047
|
+
let newItem = this.loadItem(hydratedItem);
|
|
3048
|
+
newItem.render(this.view, this.addontop);
|
|
3049
|
+
this._trigger("afterrender", this);
|
|
3050
|
+
return newItem;
|
|
3051
|
+
}
|
|
3052
|
+
const { items, meta } = this.adapter.parseCollectionResponse(data, { type: this.type });
|
|
3053
|
+
this.adapter.applyMetadata(this, meta);
|
|
3054
|
+
if (items == null) {
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
if (items.constructor === Array) {
|
|
3058
|
+
log("Append multiple items to collection");
|
|
3059
|
+
if (this.items.length === 0) {
|
|
3060
|
+
this.view.reset(true);
|
|
3061
|
+
}
|
|
3062
|
+
const loadedItems = [];
|
|
3063
|
+
items.forEach((item) => {
|
|
3064
|
+
const loadedItem = this.loadItem(item);
|
|
3065
|
+
if (loadedItem) {
|
|
3066
|
+
loadedItems.push(loadedItem);
|
|
3067
|
+
}
|
|
3068
|
+
});
|
|
3069
|
+
this.render();
|
|
3070
|
+
return loadedItems;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Extract metadata and data from JSON:API document
|
|
3075
|
+
*
|
|
3076
|
+
* Extracts metadata (totalRecords, offset) from JSON:API response meta object
|
|
3077
|
+
* and returns the hydrated data array. Data hydration is handled by
|
|
3078
|
+
* parseCollectionData() before this is called.
|
|
3079
|
+
*
|
|
3080
|
+
* @param {Object} data - JSON:API document (data property should already be hydrated)
|
|
3081
|
+
* @returns {Array} Array of hydrated item data objects
|
|
3082
|
+
*/
|
|
3083
|
+
extractMetadataAndData(data) {
|
|
3084
|
+
dbg("extract metadata and data", data);
|
|
3085
|
+
if (!data.hasOwnProperty("data")) {
|
|
3086
|
+
return data;
|
|
3087
|
+
}
|
|
3088
|
+
this.adapter.applyMetadata(this, this.adapter.extractMetadata(data));
|
|
3089
|
+
return data.data;
|
|
3090
|
+
}
|
|
3091
|
+
/**
|
|
3092
|
+
* Parse collection data from JSON:API document (legacy alias)
|
|
3093
|
+
*
|
|
3094
|
+
* @deprecated Use extractMetadataAndData() instead
|
|
3095
|
+
* @param {Object} data - JSON:API document
|
|
3096
|
+
* @returns {Array} Array of hydrated item data objects
|
|
3097
|
+
*/
|
|
3098
|
+
parse(data) {
|
|
3099
|
+
return this.extractMetadataAndData(data);
|
|
3100
|
+
}
|
|
3101
|
+
/**
|
|
3102
|
+
* Load from data
|
|
3103
|
+
*/
|
|
3104
|
+
loadFromData(data) {
|
|
3105
|
+
dbg("collection load from data", data);
|
|
3106
|
+
if (data === null || typeof data !== "object" || data.constructor !== Array) {
|
|
3107
|
+
dbg("cannot load ", data, " into collection ", this);
|
|
3108
|
+
return this;
|
|
3109
|
+
}
|
|
3110
|
+
if (this.navtype === "page") {
|
|
3111
|
+
this.items = [];
|
|
3112
|
+
}
|
|
3113
|
+
data.forEach((item) => {
|
|
3114
|
+
this.loadItem(item);
|
|
3115
|
+
});
|
|
3116
|
+
if (this.view) {
|
|
3117
|
+
this.view.render();
|
|
3118
|
+
} else {
|
|
3119
|
+
dbg("collection does not have a view ", this);
|
|
3120
|
+
}
|
|
3121
|
+
this._trigger("load", this);
|
|
3122
|
+
return this;
|
|
3123
|
+
}
|
|
3124
|
+
/**
|
|
3125
|
+
* Next page
|
|
3126
|
+
*/
|
|
3127
|
+
next() {
|
|
3128
|
+
this.offset = parseInt(this.offset) + parseInt(this.pageSize);
|
|
3129
|
+
this.loadFromRemote();
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Previous page
|
|
3133
|
+
*/
|
|
3134
|
+
prev() {
|
|
3135
|
+
this.offset = parseInt(this.offset) - parseInt(this.pageSize);
|
|
3136
|
+
this.loadFromRemote();
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Clear collection
|
|
3140
|
+
*
|
|
3141
|
+
* Synchronously clears items array and renders empty state.
|
|
3142
|
+
* For async item cleanup, use destroy() instead.
|
|
3143
|
+
*
|
|
3144
|
+
* @returns {Collection} This instance for chaining
|
|
3145
|
+
*/
|
|
3146
|
+
clear() {
|
|
3147
|
+
this.items = [];
|
|
3148
|
+
if (this.view) {
|
|
3149
|
+
this.view.render();
|
|
3150
|
+
}
|
|
3151
|
+
this._trigger("update", this);
|
|
3152
|
+
return this;
|
|
3153
|
+
}
|
|
3154
|
+
/**
|
|
3155
|
+
* Render collection
|
|
3156
|
+
*/
|
|
3157
|
+
render() {
|
|
3158
|
+
if (this.view) {
|
|
3159
|
+
this.view.render();
|
|
3160
|
+
}
|
|
3161
|
+
this._trigger("afterrender", this);
|
|
3162
|
+
return this;
|
|
3163
|
+
}
|
|
3164
|
+
/**
|
|
3165
|
+
* Load from remote
|
|
3166
|
+
*
|
|
3167
|
+
* Canonical method for loading collection data from API
|
|
3168
|
+
*/
|
|
3169
|
+
loadFromRemote() {
|
|
3170
|
+
return this.loadFromDataSource();
|
|
3171
|
+
}
|
|
3172
|
+
load(data) {
|
|
3173
|
+
return data ? this.loadFromData(data) : this.loadFromRemote();
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* Load from data source (internal implementation)
|
|
3177
|
+
* @private
|
|
3178
|
+
*/
|
|
3179
|
+
loadFromDataSource() {
|
|
3180
|
+
const overlay = createOverlay(this);
|
|
3181
|
+
let loader = null;
|
|
3182
|
+
if (this.view && this.view.el) {
|
|
3183
|
+
loader = $(overlay).clone().insertBefore(this.view.el).width($(this.view.el).width()).height($(this.view.el).height());
|
|
3184
|
+
}
|
|
3185
|
+
this._trigger("beforeload", this);
|
|
3186
|
+
return new Promise((resolve, reject) => {
|
|
3187
|
+
if (!this.url) {
|
|
3188
|
+
loader.remove();
|
|
3189
|
+
reject(new Error("No valid URL provided"));
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
this.adapter.applyListQuery(this.url, {
|
|
3193
|
+
type: this.type,
|
|
3194
|
+
offset: this.offset,
|
|
3195
|
+
pageSize: this.pageSize
|
|
3196
|
+
});
|
|
3197
|
+
let urlString = this.url.toString ? this.url.toString() : this.url;
|
|
3198
|
+
this.storage.read(this, urlString, {}).then((res) => {
|
|
3199
|
+
if (this.navtype === "page") {
|
|
3200
|
+
this.items = [];
|
|
3201
|
+
}
|
|
3202
|
+
this.receiveRemoteData(res.data);
|
|
3203
|
+
this._trigger("load", this);
|
|
3204
|
+
if (loader) {
|
|
3205
|
+
$(loader).remove();
|
|
3206
|
+
}
|
|
3207
|
+
if (this.paging) {
|
|
3208
|
+
this.paging.render();
|
|
3209
|
+
}
|
|
3210
|
+
resolve(this);
|
|
3211
|
+
}).catch((error2) => {
|
|
3212
|
+
if (error2 instanceof Error && error2.jqXHR) {
|
|
3213
|
+
this.fail(error2.jqXHR, error2.textStatus || "error", error2.errorThrown || error2);
|
|
3214
|
+
loader.remove();
|
|
3215
|
+
reject(error2);
|
|
3216
|
+
} else if (error2 && error2.jqXHR) {
|
|
3217
|
+
this.fail(error2.jqXHR, error2.textStatus, error2.errorThrown);
|
|
3218
|
+
loader.remove();
|
|
3219
|
+
reject(error2);
|
|
3220
|
+
} else {
|
|
3221
|
+
this.fail(null, "error", error2);
|
|
3222
|
+
loader.remove();
|
|
3223
|
+
reject(error2);
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Handle failure
|
|
3230
|
+
*/
|
|
3231
|
+
fail(xhr, txt, err) {
|
|
3232
|
+
dbg("Fail to load collection", xhr, txt, err, this);
|
|
3233
|
+
}
|
|
3234
|
+
/**
|
|
3235
|
+
* On update callback
|
|
3236
|
+
*/
|
|
3237
|
+
onupdate() {
|
|
3238
|
+
console.log("onupdate");
|
|
3239
|
+
this._trigger("update", this);
|
|
3240
|
+
return this;
|
|
3241
|
+
}
|
|
3242
|
+
/**
|
|
3243
|
+
* Insert a single new item into collection
|
|
3244
|
+
*
|
|
3245
|
+
* Creates a single item via POST request and adds it to the collection.
|
|
3246
|
+
* The server response should contain the created item in JSON:API format.
|
|
3247
|
+
*
|
|
3248
|
+
* @param {Object} itemData - Single item data object (not an array)
|
|
3249
|
+
* @returns {Promise<Item>} Promise resolving to the created Item instance
|
|
3250
|
+
* @throws {Error} If itemData is an array (use batchInsert() instead)
|
|
3251
|
+
*/
|
|
3252
|
+
insert(itemData) {
|
|
3253
|
+
if (Array.isArray(itemData)) {
|
|
3254
|
+
throw new Error("insert() expects a single item object. Use batchInsert() for multiple items.");
|
|
3255
|
+
}
|
|
3256
|
+
const payload = this.adapter.serializeForCreate(itemData, { type: this.type });
|
|
3257
|
+
return new Promise((resolve, reject) => {
|
|
3258
|
+
if (!this.insertUrl) {
|
|
3259
|
+
this.insertUrl = this.url;
|
|
3260
|
+
}
|
|
3261
|
+
let insertUrlString = this.insertUrl.toString ? this.insertUrl.toString() : this.insertUrl;
|
|
3262
|
+
this.storage.create(this, insertUrlString, { contentType: payload.contentType }, payload.body).then((resp) => {
|
|
3263
|
+
let data = resp.data;
|
|
3264
|
+
let newItem = this.receiveRemoteData(data);
|
|
3265
|
+
log("newItem", newItem);
|
|
3266
|
+
this.onupdate();
|
|
3267
|
+
resolve(newItem);
|
|
3268
|
+
}).catch((resp) => {
|
|
3269
|
+
dbg("fail to receive data", resp);
|
|
3270
|
+
reject(resp);
|
|
3271
|
+
});
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
/**
|
|
3275
|
+
* Batch insert multiple items into collection
|
|
3276
|
+
*
|
|
3277
|
+
* Creates multiple items via POST request and adds them to the collection.
|
|
3278
|
+
* The server response should contain an array of created items in JSON:API format.
|
|
3279
|
+
*
|
|
3280
|
+
* @param {Array} itemsData - Array of item data objects
|
|
3281
|
+
* @returns {Promise<Array<Item>>} Promise resolving to array of created Item instances
|
|
3282
|
+
* @throws {Error} If itemsData is not an array
|
|
3283
|
+
*/
|
|
3284
|
+
batchInsert(itemsData) {
|
|
3285
|
+
if (!Array.isArray(itemsData)) {
|
|
3286
|
+
throw new Error("batchInsert() expects an array of items. Use insert() for a single item.");
|
|
3287
|
+
}
|
|
3288
|
+
if (itemsData.length === 0) {
|
|
3289
|
+
return Promise.resolve([]);
|
|
3290
|
+
}
|
|
3291
|
+
const payload = this.adapter.serializeForCreate(itemsData, { type: this.type });
|
|
3292
|
+
return new Promise((resolve, reject) => {
|
|
3293
|
+
if (!this.insertUrl) {
|
|
3294
|
+
this.insertUrl = this.url;
|
|
3295
|
+
}
|
|
3296
|
+
let insertUrlString = this.insertUrl.toString ? this.insertUrl.toString() : this.insertUrl;
|
|
3297
|
+
this.storage.create(this, insertUrlString, { contentType: payload.contentType }, payload.body).then((resp) => {
|
|
3298
|
+
let data = resp.data;
|
|
3299
|
+
const result = this.receiveRemoteData(data);
|
|
3300
|
+
const newItems = Array.isArray(result) ? result : result ? [result] : [];
|
|
3301
|
+
log("batchInsert newItems", newItems);
|
|
3302
|
+
this.onupdate();
|
|
3303
|
+
resolve(newItems);
|
|
3304
|
+
}).catch((resp) => {
|
|
3305
|
+
dbg("fail to receive batch data", resp);
|
|
3306
|
+
reject(resp);
|
|
3307
|
+
});
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3310
|
+
/**
|
|
3311
|
+
* @deprecated Use insert() for single items or batchInsert() for multiple items
|
|
3312
|
+
* This method is bivalent and will be removed in a future version.
|
|
3313
|
+
* Alias for backward compatibility - delegates to insert() or batchInsert() based on input type.
|
|
3314
|
+
*/
|
|
3315
|
+
append(itemData) {
|
|
3316
|
+
if (Array.isArray(itemData)) {
|
|
3317
|
+
return this.batchInsert(itemData);
|
|
3318
|
+
} else {
|
|
3319
|
+
return this.insert(itemData);
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
/**
|
|
3323
|
+
* @deprecated Use insert() instead
|
|
3324
|
+
* Alias for backward compatibility
|
|
3325
|
+
*/
|
|
3326
|
+
createItem(itemData) {
|
|
3327
|
+
return this.insert(itemData);
|
|
3328
|
+
}
|
|
3329
|
+
/**
|
|
3330
|
+
* @deprecated Use insert() instead
|
|
3331
|
+
* Alias for backward compatibility
|
|
3332
|
+
*/
|
|
3333
|
+
newItem(itemData) {
|
|
3334
|
+
return this.insert(itemData);
|
|
3335
|
+
}
|
|
3336
|
+
/**
|
|
3337
|
+
* Load item
|
|
3338
|
+
*/
|
|
3339
|
+
loadItem(itemData) {
|
|
3340
|
+
log("loadItem from collection", itemData);
|
|
3341
|
+
if (!itemData) {
|
|
3342
|
+
log("no item data", itemData);
|
|
3343
|
+
return null;
|
|
3344
|
+
}
|
|
3345
|
+
let opts = {
|
|
3346
|
+
type: this.type,
|
|
3347
|
+
collection: this,
|
|
3348
|
+
uievents: this.uievents,
|
|
3349
|
+
storage: this.storage,
|
|
3350
|
+
adapter: this.adapter
|
|
3351
|
+
};
|
|
3352
|
+
if (this.setAttrAsId && itemData.id == null) {
|
|
3353
|
+
log("set item id from attribute", this.setAttrAsId, itemData);
|
|
3354
|
+
itemData.id = itemData.attributes[this.setAttrAsId];
|
|
3355
|
+
}
|
|
3356
|
+
if (itemData.id && this.url) {
|
|
3357
|
+
let tmp;
|
|
3358
|
+
const url = createURL(this.url.toString());
|
|
3359
|
+
url.path += "/" + itemData.id;
|
|
3360
|
+
opts.url = createURL(url.toString());
|
|
3361
|
+
const updateUrl = createURL(this.updateUrl.toString());
|
|
3362
|
+
updateUrl.path += "/" + itemData.id;
|
|
3363
|
+
opts.updateUrl = createURL(updateUrl.toString());
|
|
3364
|
+
const deleteUrl = createURL(this.deleteUrl.toString());
|
|
3365
|
+
deleteUrl.path += "/" + itemData.id;
|
|
3366
|
+
opts.deleteUrl = createURL(deleteUrl.toString());
|
|
3367
|
+
const insertUrl = createURL(this.insertUrl.toString());
|
|
3368
|
+
insertUrl.path += "/" + itemData.id;
|
|
3369
|
+
opts.insertUrl = createURL(insertUrl.toString());
|
|
3370
|
+
}
|
|
3371
|
+
if (this.itemListeners) {
|
|
3372
|
+
opts.itemListeners = this.itemListeners;
|
|
3373
|
+
}
|
|
3374
|
+
let newItem = new Item(opts).bindView(new ItemView({
|
|
3375
|
+
template: this.template,
|
|
3376
|
+
container: this.view
|
|
3377
|
+
})).loadFromData(itemData);
|
|
3378
|
+
if (this.addontop) {
|
|
3379
|
+
dbg("Add on top");
|
|
3380
|
+
this.items.unshift(newItem);
|
|
3381
|
+
} else {
|
|
3382
|
+
this.items.push(newItem);
|
|
3383
|
+
}
|
|
3384
|
+
return newItem;
|
|
3385
|
+
}
|
|
3386
|
+
/**
|
|
3387
|
+
* Destroy collection and clean up resources
|
|
3388
|
+
*
|
|
3389
|
+
* Removes event handlers, destroys all items and views, clears references.
|
|
3390
|
+
* Safe to call multiple times.
|
|
3391
|
+
*
|
|
3392
|
+
* @returns {Collection} This instance for chaining
|
|
3393
|
+
*/
|
|
3394
|
+
destroy() {
|
|
3395
|
+
const itemsToDestroy = [...this.items];
|
|
3396
|
+
itemsToDestroy.forEach((item) => {
|
|
3397
|
+
if (item && typeof item.destroy === "function") {
|
|
3398
|
+
item.destroy();
|
|
3399
|
+
}
|
|
3400
|
+
});
|
|
3401
|
+
this.items = [];
|
|
3402
|
+
if (this.view && typeof this.view.destroy === "function") {
|
|
3403
|
+
this.view.destroy();
|
|
3404
|
+
}
|
|
3405
|
+
this.view = null;
|
|
3406
|
+
if (this.filtering && typeof this.filtering.destroy === "function") {
|
|
3407
|
+
this.filtering.destroy();
|
|
3408
|
+
}
|
|
3409
|
+
this.filtering = null;
|
|
3410
|
+
if (this.paging && typeof this.paging.destroy === "function") {
|
|
3411
|
+
this.paging.destroy();
|
|
3412
|
+
}
|
|
3413
|
+
this.paging = null;
|
|
3414
|
+
this.callbacks = {};
|
|
3415
|
+
this.storage = null;
|
|
3416
|
+
this.url = null;
|
|
3417
|
+
this.deleteUrl = null;
|
|
3418
|
+
this.insertUrl = null;
|
|
3419
|
+
this.updateUrl = null;
|
|
3420
|
+
this.items = [];
|
|
3421
|
+
this.total = null;
|
|
3422
|
+
this.offset = 0;
|
|
3423
|
+
return this;
|
|
3424
|
+
}
|
|
3425
|
+
};
|
|
3426
|
+
|
|
3427
|
+
// src/Filtering.js
|
|
3428
|
+
var Filtering = class {
|
|
3429
|
+
constructor(filterForm, collection) {
|
|
3430
|
+
this.collection = collection;
|
|
3431
|
+
this.el = filterForm;
|
|
3432
|
+
let $form = $(filterForm);
|
|
3433
|
+
$form.data("instance", collection).on("submit", (e) => {
|
|
3434
|
+
dbg("Filter form was submitted");
|
|
3435
|
+
e.preventDefault();
|
|
3436
|
+
this.handleSubmit($form[0]);
|
|
3437
|
+
}).on("reset", () => {
|
|
3438
|
+
delete this.collection.url.parameters.filter;
|
|
3439
|
+
this.collection.loadFromRemote();
|
|
3440
|
+
dbg("filter form reset");
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
/**
|
|
3444
|
+
* Handle form submit
|
|
3445
|
+
*/
|
|
3446
|
+
handleSubmit(form) {
|
|
3447
|
+
let filter = [];
|
|
3448
|
+
for (let i = 0; i < form.elements.length; i++) {
|
|
3449
|
+
let el = form.elements[i];
|
|
3450
|
+
let $el = $(el);
|
|
3451
|
+
let value = $el.val();
|
|
3452
|
+
let operator = $el.data("operator");
|
|
3453
|
+
if (el.name && value) {
|
|
3454
|
+
filter.push(
|
|
3455
|
+
el.name + (operator ? operator : "=") + value
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
this.collection.offset = 0;
|
|
3460
|
+
if (filter.length) {
|
|
3461
|
+
this.collection.url.parameters.filter = filter.join(",");
|
|
3462
|
+
} else {
|
|
3463
|
+
delete this.collection.url.parameters.filter;
|
|
3464
|
+
}
|
|
3465
|
+
this.collection.loadFromRemote();
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3468
|
+
* Destroy filtering and clean up resources
|
|
3469
|
+
*/
|
|
3470
|
+
destroy() {
|
|
3471
|
+
if (this.el) {
|
|
3472
|
+
const $form = $(this.el);
|
|
3473
|
+
$form.off("submit");
|
|
3474
|
+
$form.off("reset");
|
|
3475
|
+
if (typeof $form.removeData === "function") {
|
|
3476
|
+
$form.removeData("instance");
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
this.collection = null;
|
|
3480
|
+
this.el = null;
|
|
3481
|
+
return this;
|
|
3482
|
+
}
|
|
3483
|
+
};
|
|
3484
|
+
|
|
3485
|
+
// src/Sorting.js
|
|
3486
|
+
var Sorting = class {
|
|
3487
|
+
constructor(sortHeader, collection) {
|
|
3488
|
+
this.el = sortHeader;
|
|
3489
|
+
this.collection = collection;
|
|
3490
|
+
const $sorts = sortHeader.find("[data-sortfld]").data("instance", this.collection).on("click", this.sortNow.bind(this));
|
|
3491
|
+
}
|
|
3492
|
+
sortNow(ev) {
|
|
3493
|
+
let $lnk = $(ev.currentTarget);
|
|
3494
|
+
let fld = $lnk.data("sortfld");
|
|
3495
|
+
let dir = $lnk.data("sortdir");
|
|
3496
|
+
let inst = this.collection;
|
|
3497
|
+
let sort = inst.url.parameters.hasOwnProperty("sort") ? inst.url.parameters.sort : "";
|
|
3498
|
+
let sortArr = [];
|
|
3499
|
+
sort.split(",").forEach(function(item) {
|
|
3500
|
+
let res = /^(-*)([a-z0-9\-\_]+)$/.exec(item.trim());
|
|
3501
|
+
if (!res)
|
|
3502
|
+
return;
|
|
3503
|
+
if (res[2] == fld)
|
|
3504
|
+
return;
|
|
3505
|
+
sortArr.push(item);
|
|
3506
|
+
});
|
|
3507
|
+
switch (dir) {
|
|
3508
|
+
case "up":
|
|
3509
|
+
sortArr.push("-" + fld);
|
|
3510
|
+
$lnk.data("sortdir", "down");
|
|
3511
|
+
$lnk.find(".sort-up").hide();
|
|
3512
|
+
$lnk.find(".sort-down").show();
|
|
3513
|
+
$lnk.find(".sort-default").hide();
|
|
3514
|
+
break;
|
|
3515
|
+
case "down":
|
|
3516
|
+
$lnk.data("sortdir", null);
|
|
3517
|
+
$lnk.find(".sort-up").hide();
|
|
3518
|
+
$lnk.find(".sort-down").hide();
|
|
3519
|
+
$lnk.find(".sort-default").show();
|
|
3520
|
+
break;
|
|
3521
|
+
default:
|
|
3522
|
+
$lnk.data("sortdir", "up");
|
|
3523
|
+
sortArr.push(fld);
|
|
3524
|
+
$lnk.find(".sort-up").show();
|
|
3525
|
+
$lnk.find(".sort-down").hide();
|
|
3526
|
+
$lnk.find(".sort-default").hide();
|
|
3527
|
+
}
|
|
3528
|
+
let nxtSort = sortArr.join(",");
|
|
3529
|
+
if (sort !== nxtSort) {
|
|
3530
|
+
inst.url.parameters.sort = nxtSort;
|
|
3531
|
+
inst.loadFromRemote();
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
destroy() {
|
|
3535
|
+
if (this.el) {
|
|
3536
|
+
$(this.el).find("[data-sortfld]").each(function(sort) {
|
|
3537
|
+
$(this).off("click");
|
|
3538
|
+
$(this).removeData("instance");
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
return this;
|
|
3542
|
+
}
|
|
3543
|
+
};
|
|
3544
|
+
|
|
3545
|
+
// src/utilities.js
|
|
3546
|
+
var utilities = {
|
|
3547
|
+
/**
|
|
3548
|
+
* Fill form fields with data from instance
|
|
3549
|
+
*/
|
|
3550
|
+
fillForm: function(form, instance) {
|
|
3551
|
+
let formEl = $(form)[0];
|
|
3552
|
+
if ($(form).prop("tagName") !== "FORM") {
|
|
3553
|
+
return null;
|
|
3554
|
+
}
|
|
3555
|
+
if (!instance || !instance.hasOwnProperty("attributes")) {
|
|
3556
|
+
return null;
|
|
3557
|
+
}
|
|
3558
|
+
Object.getOwnPropertyNames(instance.attributes).forEach((attrName) => {
|
|
3559
|
+
if (!formEl.elements.hasOwnProperty(attrName)) {
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
let val = instance.attributes[attrName];
|
|
3563
|
+
let inp = formEl.elements[attrName];
|
|
3564
|
+
let $inp = $(inp);
|
|
3565
|
+
if (instance.attributes[attrName] && typeof instance.attributes[attrName] === "object" && instance.attributes[attrName].hasOwnProperty("id")) {
|
|
3566
|
+
val = instance.attributes[attrName].id;
|
|
3567
|
+
}
|
|
3568
|
+
if ($inp.attr("type") === "date") {
|
|
3569
|
+
val = val ? val.substr(0, 10) : val;
|
|
3570
|
+
}
|
|
3571
|
+
$inp.val(val);
|
|
3572
|
+
});
|
|
3573
|
+
if (!instance.relationships) {
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
Object.getOwnPropertyNames(instance.relationships).forEach((relName) => {
|
|
3577
|
+
if (!formEl.elements.hasOwnProperty(relName)) {
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
if (!instance.relationships[relName]) {
|
|
3581
|
+
$(formEl.elements[relName]).val(null);
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
let rel = instance.relationships[relName];
|
|
3585
|
+
let formElRel = formEl.elements[relName];
|
|
3586
|
+
if (rel.constructor === Array) {
|
|
3587
|
+
let vals = [];
|
|
3588
|
+
rel.forEach((relItem) => {
|
|
3589
|
+
vals.push(relItem.id);
|
|
3590
|
+
});
|
|
3591
|
+
$(formElRel).val(vals);
|
|
3592
|
+
} else {
|
|
3593
|
+
dbg("set ", relName, rel);
|
|
3594
|
+
if (formElRel.tagName === "SELECT") {
|
|
3595
|
+
let lbl = $(formElRel).data("label");
|
|
3596
|
+
let lblVal = rel.hasOwnProperty("attributes") && rel.attributes[lbl] ? rel.attributes[lbl] : rel.id;
|
|
3597
|
+
$("<option>").val(rel.id).text(lblVal).appendTo($(formElRel));
|
|
3598
|
+
}
|
|
3599
|
+
$(formElRel).val(rel.id);
|
|
3600
|
+
}
|
|
3601
|
+
});
|
|
3602
|
+
},
|
|
3603
|
+
/**
|
|
3604
|
+
* Capture form submit event and redirect it to callback
|
|
3605
|
+
*/
|
|
3606
|
+
captureFormSubmit: function(form, cb) {
|
|
3607
|
+
let formEl = $(form);
|
|
3608
|
+
if (formEl.prop("tagName") !== "FORM" || typeof cb !== "function") {
|
|
3609
|
+
return;
|
|
3610
|
+
}
|
|
3611
|
+
formEl.off("submit").on("submit", (event) => {
|
|
3612
|
+
event.preventDefault();
|
|
3613
|
+
let frm = formEl[0];
|
|
3614
|
+
cb(this.fetchFormData(frm), event);
|
|
3615
|
+
});
|
|
3616
|
+
return formEl;
|
|
3617
|
+
},
|
|
3618
|
+
/**
|
|
3619
|
+
* Fetch form data - handles array notation (name[]) and normalizes form element
|
|
3620
|
+
*/
|
|
3621
|
+
fetchFormData: function(form) {
|
|
3622
|
+
let formEl = $(form)[0];
|
|
3623
|
+
let formElements = {};
|
|
3624
|
+
Object.getOwnPropertyNames(formEl.elements).forEach((item) => {
|
|
3625
|
+
let el = formEl.elements[item];
|
|
3626
|
+
let $item = $(el);
|
|
3627
|
+
if (!$item.attr("name") || $item.attr("name") === "") {
|
|
3628
|
+
return;
|
|
3629
|
+
}
|
|
3630
|
+
if ($item.attr("type") === "checkbox" && !$item[0].checked) {
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
let name = $item.attr("name");
|
|
3634
|
+
let value = $item.val();
|
|
3635
|
+
let arrayMatch = /(\w+)\[\]/.exec(name);
|
|
3636
|
+
if (arrayMatch) {
|
|
3637
|
+
let arrayName = arrayMatch[1];
|
|
3638
|
+
if (!formElements[arrayName]) {
|
|
3639
|
+
formElements[arrayName] = [];
|
|
3640
|
+
}
|
|
3641
|
+
formElements[arrayName].push(value);
|
|
3642
|
+
} else {
|
|
3643
|
+
formElements[name] = value;
|
|
3644
|
+
}
|
|
3645
|
+
});
|
|
3646
|
+
return formElements;
|
|
3647
|
+
},
|
|
3648
|
+
/**
|
|
3649
|
+
* Extract form data - alias for fetchFormData (for backward compatibility)
|
|
3650
|
+
*/
|
|
3651
|
+
extractFormData: function(form) {
|
|
3652
|
+
return this.fetchFormData(form);
|
|
3653
|
+
}
|
|
3654
|
+
};
|
|
3655
|
+
|
|
3656
|
+
// src/KViews.js
|
|
3657
|
+
var KViews = class _KViews {
|
|
3658
|
+
/**
|
|
3659
|
+
* Helper: Extract and merge options from element and parameters
|
|
3660
|
+
*/
|
|
3661
|
+
static prepareOptions(el, opts) {
|
|
3662
|
+
if (!el) {
|
|
3663
|
+
dbg("Warning: no DOM element provided for apiator");
|
|
3664
|
+
return null;
|
|
3665
|
+
}
|
|
3666
|
+
if (typeof opts === "string") {
|
|
3667
|
+
opts = {
|
|
3668
|
+
url: opts
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
let options = { dataBindings: {}, addontop: false };
|
|
3672
|
+
options = Object.assign(options, $(el).data());
|
|
3673
|
+
try {
|
|
3674
|
+
Object.assign(options, parseOptions(opts));
|
|
3675
|
+
} catch (e) {
|
|
3676
|
+
throw new Error("Error on KViews init", e);
|
|
3677
|
+
}
|
|
3678
|
+
return options;
|
|
3679
|
+
}
|
|
3680
|
+
/**
|
|
3681
|
+
* Helper: Check for existing instance and update if found
|
|
3682
|
+
*
|
|
3683
|
+
* SAFE UPDATE CONTRACT:
|
|
3684
|
+
* Only updates whitelisted safe configuration options.
|
|
3685
|
+
* Does not overwrite internal runtime state (callbacks, items, views, etc.)
|
|
3686
|
+
*
|
|
3687
|
+
* Safe to update:
|
|
3688
|
+
* - url, updateUrl, deleteUrl, insertUrl (via setUrl)
|
|
3689
|
+
* - template, type, pageSize, offset (configuration)
|
|
3690
|
+
* - emptyview, filter, paging (view configuration)
|
|
3691
|
+
*
|
|
3692
|
+
* NOT updated (internal state):
|
|
3693
|
+
* - callbacks, items, views, storage, filtering, paging instances
|
|
3694
|
+
* - length, total, iterator (runtime state)
|
|
3695
|
+
*/
|
|
3696
|
+
static getOrUpdateInstance(el, options) {
|
|
3697
|
+
let existingInstance = $(el).data("instance");
|
|
3698
|
+
if (existingInstance !== void 0) {
|
|
3699
|
+
const safeUpdateOptions = [
|
|
3700
|
+
"url",
|
|
3701
|
+
"updateUrl",
|
|
3702
|
+
"deleteUrl",
|
|
3703
|
+
"insertUrl",
|
|
3704
|
+
"template",
|
|
3705
|
+
"type",
|
|
3706
|
+
"pageSize",
|
|
3707
|
+
"offset",
|
|
3708
|
+
"emptyview",
|
|
3709
|
+
"filter",
|
|
3710
|
+
"paging",
|
|
3711
|
+
"addontop",
|
|
3712
|
+
"uievents",
|
|
3713
|
+
"setAttrAsId",
|
|
3714
|
+
"itemListeners",
|
|
3715
|
+
"itemOn",
|
|
3716
|
+
"headers",
|
|
3717
|
+
"adapter"
|
|
3718
|
+
];
|
|
3719
|
+
if (options.url) {
|
|
3720
|
+
existingInstance.setUrl(options.url);
|
|
3721
|
+
delete options.url;
|
|
3722
|
+
}
|
|
3723
|
+
const parsedOptions = parseOptions(options);
|
|
3724
|
+
const safeUpdates = {};
|
|
3725
|
+
safeUpdateOptions.forEach((key) => {
|
|
3726
|
+
if (parsedOptions.hasOwnProperty(key)) {
|
|
3727
|
+
safeUpdates[key] = parsedOptions[key];
|
|
3728
|
+
}
|
|
3729
|
+
});
|
|
3730
|
+
Object.assign(existingInstance, safeUpdates);
|
|
3731
|
+
if (safeUpdates.adapter) {
|
|
3732
|
+
existingInstance.adapter = resolveAdapter(safeUpdates.adapter);
|
|
3733
|
+
}
|
|
3734
|
+
if (safeUpdates.headers && existingInstance.storage && existingInstance.storage.defaultOptions) {
|
|
3735
|
+
existingInstance.storage.defaultOptions.headers = Object.assign(
|
|
3736
|
+
{},
|
|
3737
|
+
existingInstance.storage.defaultOptions.headers || {},
|
|
3738
|
+
safeUpdates.headers
|
|
3739
|
+
);
|
|
3740
|
+
}
|
|
3741
|
+
return existingInstance;
|
|
3742
|
+
}
|
|
3743
|
+
return null;
|
|
3744
|
+
}
|
|
3745
|
+
/**
|
|
3746
|
+
* Helper: Handle emptyview option
|
|
3747
|
+
*/
|
|
3748
|
+
static processEmptyView(options) {
|
|
3749
|
+
if (options.hasOwnProperty("emptyview")) {
|
|
3750
|
+
options.emptyview = $(options.emptyview).remove();
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
/**
|
|
3754
|
+
* Helper: Attach listeners and finalize instance
|
|
3755
|
+
*/
|
|
3756
|
+
static finalizeInstance(el, instance, options, listeners) {
|
|
3757
|
+
if (listeners) {
|
|
3758
|
+
Object.getOwnPropertyNames(listeners).forEach((eventName) => {
|
|
3759
|
+
instance.on(eventName, listeners[eventName]);
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
$(el).data("instance", instance);
|
|
3763
|
+
dbg("instance", instance.url);
|
|
3764
|
+
if (instance.url && (typeof options.dontload === "undefined" || !options.dontload)) {
|
|
3765
|
+
log("loadFromRemote now", options, instance);
|
|
3766
|
+
instance.loadFromRemote();
|
|
3767
|
+
}
|
|
3768
|
+
return instance;
|
|
3769
|
+
}
|
|
3770
|
+
/**
|
|
3771
|
+
* Create collection instance
|
|
3772
|
+
*/
|
|
3773
|
+
static createCollectionInstance(el, opts) {
|
|
3774
|
+
let options = _KViews.prepareOptions(el, opts);
|
|
3775
|
+
log("createCollectionInstance", options);
|
|
3776
|
+
if (!options) {
|
|
3777
|
+
return null;
|
|
3778
|
+
}
|
|
3779
|
+
let existingInstance = _KViews.getOrUpdateInstance(el, options);
|
|
3780
|
+
if (existingInstance) {
|
|
3781
|
+
return existingInstance;
|
|
3782
|
+
}
|
|
3783
|
+
dbg("init apiator collection on ", el, options);
|
|
3784
|
+
_KViews.processEmptyView(options);
|
|
3785
|
+
let listeners = options.on;
|
|
3786
|
+
delete options.on;
|
|
3787
|
+
dbg("Create collection instance", options);
|
|
3788
|
+
let templateTxt = $(el).length ? $(el).html() : null;
|
|
3789
|
+
if (options.template) {
|
|
3790
|
+
if (options.template instanceof jQuery) {
|
|
3791
|
+
dbg("template is jQuery object", options.template, el);
|
|
3792
|
+
let $tpl = $(options.template).clone().removeAttr("id");
|
|
3793
|
+
templateTxt = $("<div>").append($tpl).html();
|
|
3794
|
+
} else if (typeof options.template === "string") {
|
|
3795
|
+
dbg("template is raw text: can be either a jQuery selector or raw HTML", options.template, el);
|
|
3796
|
+
templateTxt = $("<div>").append($(options.template).clone().removeAttr("id")).html();
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
if (templateTxt !== null) {
|
|
3800
|
+
templateTxt = templateTxt.replace(/</gi, "<").replace(/>/gi, ">").replace(/'/gi, "'").replace(/"/gi, '"').replace(/ /gi, " ").replace(/&/gi, "&");
|
|
3801
|
+
options.template = template(templateTxt);
|
|
3802
|
+
}
|
|
3803
|
+
let collectionConfig = {
|
|
3804
|
+
el,
|
|
3805
|
+
itemsContainer: options.hasOwnProperty("container") ? $(options.container) : el,
|
|
3806
|
+
allowempty: options.disableempty !== true
|
|
3807
|
+
};
|
|
3808
|
+
options.view = new CollectionView(collectionConfig);
|
|
3809
|
+
log("Collection constructor", options);
|
|
3810
|
+
let instance = new Collection(options);
|
|
3811
|
+
if (options.hasOwnProperty("filter")) {
|
|
3812
|
+
let filterEl = $(options.filter);
|
|
3813
|
+
if (filterEl.length && filterEl.prop("tagName") === "FORM") {
|
|
3814
|
+
instance.filtering = new Filtering(filterEl, instance);
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
if (options.hasOwnProperty("sort")) {
|
|
3818
|
+
log("setup sorting", options.sort);
|
|
3819
|
+
let sortEl = $(options.sort);
|
|
3820
|
+
if (sortEl.length) {
|
|
3821
|
+
instance.sorting = new Sorting(sortEl, instance);
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
_KViews.finalizeInstance(el, instance, options, listeners);
|
|
3825
|
+
return instance;
|
|
3826
|
+
}
|
|
3827
|
+
/**
|
|
3828
|
+
* Create item instance
|
|
3829
|
+
*/
|
|
3830
|
+
static createItemInstance(el, opts, data = null) {
|
|
3831
|
+
let options = _KViews.prepareOptions(el, opts);
|
|
3832
|
+
if (!options) {
|
|
3833
|
+
return null;
|
|
3834
|
+
}
|
|
3835
|
+
let existingInstance = _KViews.getOrUpdateInstance(el, options);
|
|
3836
|
+
if (existingInstance) {
|
|
3837
|
+
return existingInstance;
|
|
3838
|
+
}
|
|
3839
|
+
dbg("init apiator item on ", el, options);
|
|
3840
|
+
_KViews.processEmptyView(options);
|
|
3841
|
+
let listeners = options.on;
|
|
3842
|
+
delete options.on;
|
|
3843
|
+
options.template = null;
|
|
3844
|
+
let templateTxt = null;
|
|
3845
|
+
if ($(el).length) {
|
|
3846
|
+
var node = $(el)[0];
|
|
3847
|
+
templateTxt = node ? node.outerHTML : null;
|
|
3848
|
+
}
|
|
3849
|
+
if (templateTxt) {
|
|
3850
|
+
templateTxt = templateTxt.replace(/</gi, "<").replace(/>/gi, ">").replace(/'/gi, "'").replace(/"/gi, '"').replace(/ /gi, " ").replace(/&/gi, "&");
|
|
3851
|
+
options.template = template(templateTxt);
|
|
3852
|
+
}
|
|
3853
|
+
let elId = $(el).attr("id");
|
|
3854
|
+
let instance = new Item(options, data).bindView(new ItemView({
|
|
3855
|
+
template: options.template,
|
|
3856
|
+
el,
|
|
3857
|
+
id: elId ? elId : null
|
|
3858
|
+
}));
|
|
3859
|
+
_KViews.finalizeInstance(el, instance, options, listeners);
|
|
3860
|
+
if (options.dontload && instance.attributes && Object.keys(instance.attributes).length > 0) {
|
|
3861
|
+
instance.render();
|
|
3862
|
+
}
|
|
3863
|
+
return instance;
|
|
3864
|
+
}
|
|
3865
|
+
static helpers = utilities;
|
|
3866
|
+
/**
|
|
3867
|
+
* Global default data adapter (name or instance). Defaults to 'jsonapi'.
|
|
3868
|
+
*/
|
|
3869
|
+
static get defaultAdapter() {
|
|
3870
|
+
return getDefaultAdapter();
|
|
3871
|
+
}
|
|
3872
|
+
static set defaultAdapter(adapter) {
|
|
3873
|
+
setDefaultAdapter(adapter);
|
|
3874
|
+
}
|
|
3875
|
+
/**
|
|
3876
|
+
* Register a custom data adapter by name.
|
|
3877
|
+
*
|
|
3878
|
+
* @param {string} name
|
|
3879
|
+
* @param {object} adapter
|
|
3880
|
+
*/
|
|
3881
|
+
static registerAdapter(name, adapter) {
|
|
3882
|
+
registerAdapter(name, adapter);
|
|
3883
|
+
}
|
|
3884
|
+
};
|
|
3885
|
+
Object.defineProperty(KViews, "baseUrl", {
|
|
3886
|
+
enumerable: true,
|
|
3887
|
+
configurable: true,
|
|
3888
|
+
get() {
|
|
3889
|
+
return apiBaseConfig.baseUrl;
|
|
3890
|
+
},
|
|
3891
|
+
set(v) {
|
|
3892
|
+
apiBaseConfig.baseUrl = v;
|
|
3893
|
+
}
|
|
3894
|
+
});
|
|
3895
|
+
Object.defineProperty(KViews, "basePath", {
|
|
3896
|
+
enumerable: true,
|
|
3897
|
+
configurable: true,
|
|
3898
|
+
get() {
|
|
3899
|
+
return apiBaseConfig.basePath;
|
|
3900
|
+
},
|
|
3901
|
+
set(v) {
|
|
3902
|
+
apiBaseConfig.basePath = v;
|
|
3903
|
+
}
|
|
3904
|
+
});
|
|
3905
|
+
Object.defineProperty(KViews, "defaultHeaders", {
|
|
3906
|
+
enumerable: true,
|
|
3907
|
+
configurable: true,
|
|
3908
|
+
get() {
|
|
3909
|
+
return apiBaseConfig.defaultHeaders;
|
|
3910
|
+
},
|
|
3911
|
+
set(v) {
|
|
3912
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
3913
|
+
apiBaseConfig.defaultHeaders = v;
|
|
3914
|
+
} else {
|
|
3915
|
+
apiBaseConfig.defaultHeaders = {};
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
});
|
|
3919
|
+
|
|
3920
|
+
// src/index.js
|
|
3921
|
+
var index_default = KViews;
|
|
3922
|
+
if (typeof window !== "undefined") {
|
|
3923
|
+
window.KViews = KViews;
|
|
3924
|
+
}
|
|
3925
|
+
if (typeof $ !== "undefined" && $.fn) {
|
|
3926
|
+
$.fn.kviews = function(opts) {
|
|
3927
|
+
let el = this.length ? this[0] : this;
|
|
3928
|
+
let resourcetype = "collection";
|
|
3929
|
+
if (opts && opts.resourcetype) {
|
|
3930
|
+
resourcetype = opts.resourcetype;
|
|
3931
|
+
} else {
|
|
3932
|
+
let dataResourcetype = $(el).data("resourcetype");
|
|
3933
|
+
if (dataResourcetype) {
|
|
3934
|
+
resourcetype = dataResourcetype;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
if (resourcetype === "item") {
|
|
3938
|
+
return KViews.createItemInstance(el, opts);
|
|
3939
|
+
} else {
|
|
3940
|
+
return KViews.createCollectionInstance(el, opts);
|
|
3941
|
+
}
|
|
3942
|
+
};
|
|
3943
|
+
Object.defineProperty($.fn.kviews, "baseUrl", {
|
|
3944
|
+
enumerable: true,
|
|
3945
|
+
configurable: true,
|
|
3946
|
+
get() {
|
|
3947
|
+
return apiBaseConfig.baseUrl;
|
|
3948
|
+
},
|
|
3949
|
+
set(v) {
|
|
3950
|
+
apiBaseConfig.baseUrl = v;
|
|
3951
|
+
}
|
|
3952
|
+
});
|
|
3953
|
+
Object.defineProperty($.fn.kviews, "basePath", {
|
|
3954
|
+
enumerable: true,
|
|
3955
|
+
configurable: true,
|
|
3956
|
+
get() {
|
|
3957
|
+
return apiBaseConfig.basePath;
|
|
3958
|
+
},
|
|
3959
|
+
set(v) {
|
|
3960
|
+
apiBaseConfig.basePath = v;
|
|
3961
|
+
}
|
|
3962
|
+
});
|
|
3963
|
+
Object.defineProperty($.fn.kviews, "defaultHeaders", {
|
|
3964
|
+
enumerable: true,
|
|
3965
|
+
configurable: true,
|
|
3966
|
+
get() {
|
|
3967
|
+
return apiBaseConfig.defaultHeaders;
|
|
3968
|
+
},
|
|
3969
|
+
set(v) {
|
|
3970
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
3971
|
+
apiBaseConfig.defaultHeaders = v;
|
|
3972
|
+
} else {
|
|
3973
|
+
apiBaseConfig.defaultHeaders = {};
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
});
|
|
3977
|
+
$.fn.kviewsCollection = function(opts) {
|
|
3978
|
+
let options = {
|
|
3979
|
+
resourcetype: "collection"
|
|
3980
|
+
};
|
|
3981
|
+
if (typeof opts === "undefined") {
|
|
3982
|
+
opts = {};
|
|
3983
|
+
}
|
|
3984
|
+
if (typeof opts === "string") {
|
|
3985
|
+
opts = {
|
|
3986
|
+
url: opts
|
|
3987
|
+
};
|
|
3988
|
+
}
|
|
3989
|
+
opts = Object.assign(opts, options);
|
|
3990
|
+
return this.kviews(opts);
|
|
3991
|
+
};
|
|
3992
|
+
$.fn.kviewsItem = function(opts) {
|
|
3993
|
+
let options = {
|
|
3994
|
+
resourcetype: "item"
|
|
3995
|
+
};
|
|
3996
|
+
if (typeof opts === "undefined") {
|
|
3997
|
+
opts = {};
|
|
3998
|
+
}
|
|
3999
|
+
if (typeof opts === "string") {
|
|
4000
|
+
opts = {
|
|
4001
|
+
url: opts
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
opts = Object.assign(opts, options);
|
|
4005
|
+
return this.kviews(opts);
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
export {
|
|
4009
|
+
Collection,
|
|
4010
|
+
CollectionView,
|
|
4011
|
+
Filtering,
|
|
4012
|
+
Item,
|
|
4013
|
+
ItemView,
|
|
4014
|
+
JsonApiAdapter,
|
|
4015
|
+
KViews,
|
|
4016
|
+
Paging,
|
|
4017
|
+
PlainRestAdapter,
|
|
4018
|
+
Sorting,
|
|
4019
|
+
Storage,
|
|
4020
|
+
URL,
|
|
4021
|
+
createOverlay,
|
|
4022
|
+
createURL,
|
|
4023
|
+
dbg,
|
|
4024
|
+
deepmerge,
|
|
4025
|
+
index_default as default,
|
|
4026
|
+
error,
|
|
4027
|
+
getBoundObjects,
|
|
4028
|
+
getDefaultAdapter,
|
|
4029
|
+
log,
|
|
4030
|
+
parseOptions,
|
|
4031
|
+
registerAdapter,
|
|
4032
|
+
resolveAdapter,
|
|
4033
|
+
setDefaultAdapter,
|
|
4034
|
+
template,
|
|
4035
|
+
trace,
|
|
4036
|
+
uid,
|
|
4037
|
+
utilities
|
|
4038
|
+
};
|
|
4039
|
+
//# sourceMappingURL=index.js.map
|