@logimaxx/kviews.js 1.2.4 → 1.3.1

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/dist/kviews.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * KViews - Class-based API data binding library
3
- * Version: 1.2.4
4
- * Built: 2026-06-10T14:41:16.731Z
3
+ * Version: 1.3.1
4
+ * Built: 2026-06-29T17:09:29.460Z
5
5
  */
6
6
  var KViews = (() => {
7
7
  var __defProp = Object.defineProperty;
@@ -40,6 +40,7 @@ var KViews = (() => {
40
40
  createOverlay: () => createOverlay,
41
41
  createURL: () => createURL,
42
42
  dbg: () => dbg,
43
+ deepEqual: () => deepEqual,
43
44
  deepmerge: () => deepmerge,
44
45
  default: () => index_default,
45
46
  error: () => error,
@@ -89,6 +90,36 @@ var KViews = (() => {
89
90
  }
90
91
  throw new Error("Invalid options", options);
91
92
  }
93
+ function deepEqual(a, b) {
94
+ if (a === b) {
95
+ return true;
96
+ }
97
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
98
+ return false;
99
+ }
100
+ if (Array.isArray(a) || Array.isArray(b)) {
101
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
102
+ return false;
103
+ }
104
+ for (let i = 0; i < a.length; i++) {
105
+ if (!deepEqual(a[i], b[i])) {
106
+ return false;
107
+ }
108
+ }
109
+ return true;
110
+ }
111
+ const keysA = Object.keys(a);
112
+ const keysB = Object.keys(b);
113
+ if (keysA.length !== keysB.length) {
114
+ return false;
115
+ }
116
+ for (const key of keysA) {
117
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !deepEqual(a[key], b[key])) {
118
+ return false;
119
+ }
120
+ }
121
+ return true;
122
+ }
92
123
  function deepmerge(target, source, optionsArgument) {
93
124
  function defaultArrayMerge(target2, source2, optionsArgument2) {
94
125
  let destination = target2.slice();
@@ -906,6 +937,29 @@ var KViews = (() => {
906
937
  url.parameters[`page[${type}][limit]`] = pageSize;
907
938
  }
908
939
  }
940
+ /**
941
+ * Read pagination params already present in a collection URL.
942
+ *
943
+ * @param {import('../URL.js').URL} url - Collection URL
944
+ * @param {{ type?: string }} [context]
945
+ * @returns {{ offset?: number, pageSize?: number }}
946
+ */
947
+ extractListQueryFromUrl(url, context = {}) {
948
+ const { type } = context;
949
+ const result = {};
950
+ if (!url || !url.parameters || !type) {
951
+ return result;
952
+ }
953
+ const limitKey = `page[${type}][limit]`;
954
+ const offsetKey = `page[${type}][offset]`;
955
+ if (url.parameters.hasOwnProperty(limitKey)) {
956
+ result.pageSize = url.parameters[limitKey];
957
+ }
958
+ if (url.parameters.hasOwnProperty(offsetKey)) {
959
+ result.offset = url.parameters[offsetKey];
960
+ }
961
+ return result;
962
+ }
909
963
  /**
910
964
  * Serialize plain item data for a create (POST) request.
911
965
  *
@@ -1220,6 +1274,32 @@ var KViews = (() => {
1220
1274
  }
1221
1275
  url.parameters[this.limitParam] = pageSize;
1222
1276
  }
1277
+ /**
1278
+ * @param {import('../URL.js').URL} url
1279
+ * @param {object} [context]
1280
+ * @returns {{ offset?: number, pageSize?: number }}
1281
+ */
1282
+ extractListQueryFromUrl(url) {
1283
+ const result = {};
1284
+ if (!url || !url.parameters) {
1285
+ return result;
1286
+ }
1287
+ if (url.parameters.hasOwnProperty(this.limitParam)) {
1288
+ result.pageSize = url.parameters[this.limitParam];
1289
+ } else if (url.parameters.hasOwnProperty("pageSize")) {
1290
+ result.pageSize = url.parameters.pageSize;
1291
+ }
1292
+ if (this.paginationStyle === "page" && url.parameters.hasOwnProperty(this.pageParam)) {
1293
+ const page = url.parameters[this.pageParam] * 1;
1294
+ const pageSize = result.pageSize != null ? result.pageSize * 1 : null;
1295
+ if (pageSize) {
1296
+ result.offset = (page - 1) * pageSize;
1297
+ }
1298
+ } else if (url.parameters.hasOwnProperty(this.offsetParam)) {
1299
+ result.offset = url.parameters[this.offsetParam];
1300
+ }
1301
+ return result;
1302
+ }
1223
1303
  /**
1224
1304
  * @param {object|Array} itemData
1225
1305
  * @param {object} [context]
@@ -1882,17 +1962,26 @@ var KViews = (() => {
1882
1962
  */
1883
1963
  loadFromDataSource() {
1884
1964
  let loaders = [];
1885
- const overlay = createOverlay(this);
1886
- this.views.forEach((itemView) => {
1887
- if (itemView.el) {
1888
- let $el = $(itemView.el);
1889
- let loader = overlay.clone();
1890
- loader.insertBefore(itemView.el).width($el.width()).height($el.height());
1891
- loaders.push(loader);
1892
- }
1893
- });
1965
+ const showLoader = this.showLoader !== false;
1966
+ if (showLoader) {
1967
+ const overlay = createOverlay(this);
1968
+ this.views.forEach((itemView) => {
1969
+ if (itemView.el) {
1970
+ let $el = $(itemView.el);
1971
+ let loader = overlay.clone();
1972
+ loader.insertBefore(itemView.el).width($el.width()).height($el.height());
1973
+ loaders.push(loader);
1974
+ }
1975
+ });
1976
+ }
1977
+ const removeLoaders = () => {
1978
+ loaders.forEach((loader) => {
1979
+ loader.remove();
1980
+ });
1981
+ };
1894
1982
  return new Promise((resolve, reject) => {
1895
1983
  if (!this.url) {
1984
+ removeLoaders();
1896
1985
  reject(new Error("No valid URL provided"));
1897
1986
  return;
1898
1987
  }
@@ -1900,13 +1989,16 @@ var KViews = (() => {
1900
1989
  let urlString = this.url.toString ? this.url.toString() : this.url;
1901
1990
  this.storage.read(this, urlString, {}).then((resp) => {
1902
1991
  let data = resp.data;
1903
- this.loadFromRemoteDoc(data).render();
1992
+ const parsedData = this._parseRemoteDoc(data);
1993
+ if (!this._remoteDataEquals(parsedData)) {
1994
+ this._applyParsedRemoteData(parsedData);
1995
+ this.render();
1996
+ }
1904
1997
  this._trigger("load", this);
1905
- loaders.forEach((loader) => {
1906
- loader.remove();
1907
- });
1998
+ removeLoaders();
1908
1999
  resolve(this);
1909
2000
  }).catch((error2) => {
2001
+ removeLoaders();
1910
2002
  dbg("fail to load item resource", this.url, error2);
1911
2003
  if (error2 instanceof Error && error2.jqXHR) {
1912
2004
  this.fail(error2.jqXHR, error2.textStatus || "error", error2.errorThrown || error2);
@@ -1983,13 +2075,12 @@ var KViews = (() => {
1983
2075
  return returnView ? view : this;
1984
2076
  }
1985
2077
  /**
1986
- * Load from a remote API document (format determined by adapter).
1987
- *
2078
+ * Parse a remote API document into canonical item data.
1988
2079
  * @param {object} data - Raw HTTP response body
1989
- * @returns {Item} This instance for chaining
2080
+ * @returns {object} Parsed resource data
2081
+ * @private
1990
2082
  */
1991
- loadFromRemoteDoc(data) {
1992
- dbg("Load from remote doc", data);
2083
+ _parseRemoteDoc(data) {
1993
2084
  if (this.collection && !this.collection.type) {
1994
2085
  const inferredType = this.adapter.inferItemType(data);
1995
2086
  if (inferredType) {
@@ -1997,11 +2088,97 @@ var KViews = (() => {
1997
2088
  }
1998
2089
  }
1999
2090
  this.adapter.validateItemRemoteDoc(data, { collection: this.collection });
2000
- const parsedData = this.adapter.parseItemResponse(data, { collection: this.collection });
2091
+ return this.adapter.parseItemResponse(data, { collection: this.collection });
2092
+ }
2093
+ /**
2094
+ * Apply parsed remote data to this item.
2095
+ * @param {object} parsedData
2096
+ * @private
2097
+ */
2098
+ _applyParsedRemoteData(parsedData) {
2001
2099
  Object.assign(this, parsedData);
2002
2100
  if (this.url) {
2003
2101
  this.url = createURL(this.url);
2004
2102
  }
2103
+ }
2104
+ /**
2105
+ * Compare current item state with parsed remote data.
2106
+ * @param {object} parsedData
2107
+ * @returns {boolean}
2108
+ * @private
2109
+ */
2110
+ _remoteDataEquals(parsedData) {
2111
+ if (this.id == null && (!this.attributes || Object.keys(this.attributes).length === 0)) {
2112
+ return false;
2113
+ }
2114
+ if (String(this.id ?? "") !== String(parsedData.id ?? "")) {
2115
+ return false;
2116
+ }
2117
+ if ((this.type ?? null) !== (parsedData.type ?? null)) {
2118
+ return false;
2119
+ }
2120
+ if (!deepEqual(this.attributes ?? {}, parsedData.attributes ?? {})) {
2121
+ return false;
2122
+ }
2123
+ return deepEqual(
2124
+ this._normalizeRelationshipsForCompare(this.relationships),
2125
+ this._normalizeRelationshipsForCompare(parsedData.relationships)
2126
+ );
2127
+ }
2128
+ /**
2129
+ * Normalize relationships to plain comparable objects.
2130
+ * @param {object} relationships
2131
+ * @returns {object}
2132
+ * @private
2133
+ */
2134
+ _normalizeRelationshipsForCompare(relationships) {
2135
+ if (!relationships || typeof relationships !== "object") {
2136
+ return {};
2137
+ }
2138
+ const normalized = {};
2139
+ Object.keys(relationships).sort().forEach((name) => {
2140
+ normalized[name] = this._normalizeRelationshipValueForCompare(relationships[name]);
2141
+ });
2142
+ return normalized;
2143
+ }
2144
+ /**
2145
+ * @param {*} rel
2146
+ * @returns {*}
2147
+ * @private
2148
+ */
2149
+ _normalizeRelationshipValueForCompare(rel) {
2150
+ if (rel == null) {
2151
+ return null;
2152
+ }
2153
+ if (Array.isArray(rel)) {
2154
+ return rel.map((item) => this._normalizeRelationshipValueForCompare(item));
2155
+ }
2156
+ if (typeof rel !== "object") {
2157
+ return rel;
2158
+ }
2159
+ const normalized = {
2160
+ id: rel.id ?? null,
2161
+ type: rel.type ?? null,
2162
+ attributes: rel.attributes ?? {}
2163
+ };
2164
+ if (rel.relationships) {
2165
+ normalized.relationships = this._normalizeRelationshipsForCompare(rel.relationships);
2166
+ }
2167
+ return normalized;
2168
+ }
2169
+ /**
2170
+ * Load from a remote API document (format determined by adapter).
2171
+ *
2172
+ * @param {object} data - Raw HTTP response body
2173
+ * @returns {Item} This instance for chaining
2174
+ */
2175
+ loadFromRemoteDoc(data) {
2176
+ dbg("Load from remote doc", data);
2177
+ const parsedData = this._parseRemoteDoc(data);
2178
+ if (this._remoteDataEquals(parsedData)) {
2179
+ return this;
2180
+ }
2181
+ this._applyParsedRemoteData(parsedData);
2005
2182
  return this;
2006
2183
  }
2007
2184
  /**
@@ -2554,8 +2731,9 @@ var KViews = (() => {
2554
2731
  }
2555
2732
  /**
2556
2733
  * Render the collection view
2734
+ * @private
2557
2735
  */
2558
- render() {
2736
+ _render() {
2559
2737
  dbg("Render _collectionView", this.collection);
2560
2738
  if (this.collection && this.collection.navtype === "page") {
2561
2739
  this.reset();
@@ -2873,6 +3051,10 @@ var KViews = (() => {
2873
3051
  configurable: true
2874
3052
  });
2875
3053
  let options = Object.assign({}, opts);
3054
+ const explicitListQuery = {
3055
+ pageSize: options.hasOwnProperty("pageSize"),
3056
+ offset: options.hasOwnProperty("offset")
3057
+ };
2876
3058
  Object.assign(this, options);
2877
3059
  if (options.hasOwnProperty("paging") && $(options.paging).length) {
2878
3060
  this.paging = new Paging($(options.paging)[0], this);
@@ -2899,6 +3081,9 @@ var KViews = (() => {
2899
3081
  throw new Error("Invalid navigations type. Should be page or scroll");
2900
3082
  }
2901
3083
  this.adapter = resolveAdapter(opts.adapter);
3084
+ if (this.url) {
3085
+ this._syncListQueryFromUrl(this.url, explicitListQuery);
3086
+ }
2902
3087
  this.storage = opts.hasOwnProperty("storage") ? opts.storage : new Storage(
2903
3088
  (() => {
2904
3089
  const storageOpts = Object.assign({}, opts.ajaxOpts || {});
@@ -3079,10 +3264,31 @@ var KViews = (() => {
3079
3264
  this.deleteUrl = typeof this.deleteUrl == "string" ? createURL(this.deleteUrl) : this.deleteUrl ?? createURL(this.url);
3080
3265
  this.updateUrl = typeof this.updateUrl == "string" ? createURL(this.updateUrl) : this.updateUrl ?? createURL(this.url);
3081
3266
  this.insertUrl = typeof this.insertUrl == "string" ? createURL(this.insertUrl) : this.insertUrl ?? createURL(this.url);
3267
+ if (this.adapter) {
3268
+ this._syncListQueryFromUrl(this.url);
3269
+ }
3082
3270
  break;
3083
3271
  }
3084
3272
  return this;
3085
3273
  }
3274
+ /**
3275
+ * Apply pagination params from URL query string to collection state.
3276
+ * @private
3277
+ * @param {import('./URL.js').URL} url
3278
+ * @param {{ pageSize?: boolean, offset?: boolean }} [explicit] - Options explicitly set at init
3279
+ */
3280
+ _syncListQueryFromUrl(url, explicit = {}) {
3281
+ if (!url || !this.adapter || typeof this.adapter.extractListQueryFromUrl !== "function") {
3282
+ return;
3283
+ }
3284
+ const fromUrl = this.adapter.extractListQueryFromUrl(url, { type: this.type });
3285
+ if (fromUrl.pageSize != null && !explicit.pageSize) {
3286
+ this.setPageSize(fromUrl.pageSize);
3287
+ }
3288
+ if (fromUrl.offset != null && !explicit.offset) {
3289
+ this.offset = fromUrl.offset * 1;
3290
+ }
3291
+ }
3086
3292
  /**
3087
3293
  * Receive remote data
3088
3294
  *
@@ -3172,7 +3378,7 @@ var KViews = (() => {
3172
3378
  this.loadItem(item);
3173
3379
  });
3174
3380
  if (this.view) {
3175
- this.view.render();
3381
+ this.view._render();
3176
3382
  } else {
3177
3383
  dbg("collection does not have a view ", this);
3178
3384
  }
@@ -3204,7 +3410,7 @@ var KViews = (() => {
3204
3410
  clear() {
3205
3411
  this.items = [];
3206
3412
  if (this.view) {
3207
- this.view.render();
3413
+ this.view._render();
3208
3414
  }
3209
3415
  this._trigger("update", this);
3210
3416
  return this;
@@ -3214,7 +3420,7 @@ var KViews = (() => {
3214
3420
  */
3215
3421
  render() {
3216
3422
  if (this.view) {
3217
- this.view.render();
3423
+ this.view._render();
3218
3424
  }
3219
3425
  this._trigger("afterrender", this);
3220
3426
  return this;
@@ -3235,15 +3441,21 @@ var KViews = (() => {
3235
3441
  * @private
3236
3442
  */
3237
3443
  loadFromDataSource() {
3238
- const overlay = createOverlay(this);
3239
3444
  let loader = null;
3240
- if (this.view && this.view.el) {
3445
+ const showLoader = this.showLoader !== false;
3446
+ if (showLoader && this.view && this.view.el) {
3447
+ const overlay = createOverlay(this);
3241
3448
  loader = $(overlay).clone().insertBefore(this.view.el).width($(this.view.el).width()).height($(this.view.el).height());
3242
3449
  }
3450
+ const removeLoader = () => {
3451
+ if (loader) {
3452
+ $(loader).remove();
3453
+ }
3454
+ };
3243
3455
  this._trigger("beforeload", this);
3244
3456
  return new Promise((resolve, reject) => {
3245
3457
  if (!this.url) {
3246
- loader.remove();
3458
+ removeLoader();
3247
3459
  reject(new Error("No valid URL provided"));
3248
3460
  return;
3249
3461
  }
@@ -3259,9 +3471,7 @@ var KViews = (() => {
3259
3471
  }
3260
3472
  this.receiveRemoteData(res.data);
3261
3473
  this._trigger("load", this);
3262
- if (loader) {
3263
- $(loader).remove();
3264
- }
3474
+ removeLoader();
3265
3475
  if (this.paging) {
3266
3476
  this.paging.render();
3267
3477
  }
@@ -3269,15 +3479,15 @@ var KViews = (() => {
3269
3479
  }).catch((error2) => {
3270
3480
  if (error2 instanceof Error && error2.jqXHR) {
3271
3481
  this.fail(error2.jqXHR, error2.textStatus || "error", error2.errorThrown || error2);
3272
- loader.remove();
3482
+ removeLoader();
3273
3483
  reject(error2);
3274
3484
  } else if (error2 && error2.jqXHR) {
3275
3485
  this.fail(error2.jqXHR, error2.textStatus, error2.errorThrown);
3276
- loader.remove();
3486
+ removeLoader();
3277
3487
  reject(error2);
3278
3488
  } else {
3279
3489
  this.fail(null, "error", error2);
3280
- loader.remove();
3490
+ removeLoader();
3281
3491
  reject(error2);
3282
3492
  }
3283
3493
  });