@cable8mm/jquery-infinite-with-template 1.0.4 → 1.1.2

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/examples/generate CHANGED
@@ -2,36 +2,80 @@
2
2
  <?php
3
3
 
4
4
  /**
5
- * @example ./generate > data_sources.ajax generate new data sources
5
+ * jQuery Infinite With Template - Demo Data Generator
6
+ *
7
+ * @example ./generate > data_sources.json
8
+ * @description Generates random demo data for testing infinite scroll
6
9
  */
7
10
 
8
- $response['data'] = [];
11
+ // Enable error reporting for debugging
12
+ error_reporting(E_ALL);
13
+ ini_set('display_errors', 0);
9
14
 
10
- $query = $_GET['word'] ?? 'None';
15
+ // Set JSON header
16
+ header('Content-Type: application/json');
11
17
 
12
- for ($i = 0; $i < 20; $i++) {
13
- $response['data'][] = faker($query);
14
- }
18
+ // Get query parameter (default to 'None' if not provided)
19
+ $query = isset($_GET['word']) ? $_GET['word'] : 'None';
15
20
 
16
- header('Content-Type: application/json');
21
+ // Generate response
22
+ $response = [
23
+ 'data' => [],
24
+ 'page' => isset($_GET['page']) ? (int)$_GET['page'] : 1,
25
+ 'total' => 100 // Simulate total count
26
+ ];
17
27
 
18
- echo json_encode($response, JSON_PRETTY_PRINT);
28
+ // Generate 20 items per page
29
+ $itemsPerPage = 30;
30
+ for ($i = 0; $i < $itemsPerPage; $i++) {
31
+ $response['data'][] = generateItem($query, $response['page'], $i);
32
+ }
19
33
 
20
- function faker($query)
34
+ // Output JSON
35
+ echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
36
+
37
+ /**
38
+ * Generate a single fake item
39
+ *
40
+ * @param string $query Search query
41
+ * @param int $page Current page number
42
+ * @param int $index Item index within page
43
+ * @return array Generated item
44
+ */
45
+ function generateItem($query, $page, $index)
21
46
  {
47
+ static $usedIds = [];
48
+
49
+ // Generate unique ID
50
+ do {
51
+ $id = rand(1, 10000);
52
+ } while (in_array($id, $usedIds));
53
+
54
+ $usedIds[] = $id;
55
+
22
56
  return [
23
- 'id' => rand(1, 1000),
24
- 'title' => generateRandomString(30)."(word=$query)",
57
+ 'id' => $id,
58
+ 'title' => generateRandomString(30) . "(word=$query, page=$page)",
59
+ 'page' => $page,
60
+ 'index' => $index
25
61
  ];
26
62
  }
27
63
 
64
+ /**
65
+ * Generate random string of specified length
66
+ *
67
+ * @param int $length String length
68
+ * @return string Random string
69
+ */
28
70
  function generateRandomString($length = 10)
29
71
  {
30
72
  $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
31
73
  $charactersLength = strlen($characters);
32
74
  $randomString = '';
75
+
33
76
  for ($i = 0; $i < $length; $i++) {
34
77
  $randomString .= $characters[rand(0, $charactersLength - 1)];
35
78
  }
79
+
36
80
  return $randomString;
37
81
  }
@@ -1,4 +1,4 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
@@ -10,7 +10,7 @@
10
10
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jsrender/1.0.10/jsrender.min.js"></script>
11
11
 
12
12
  <!-- Load InfiniteScrollWithTemplate -->
13
- <script src="/jquery.infiniteScrollWithTemplate.js"></script>
13
+ <script src="../jquery.infiniteScrollWithTemplate.js"></script>
14
14
  </head>
15
15
 
16
16
  <body>
@@ -18,7 +18,7 @@
18
18
  $(function () {
19
19
  $("#result").infiniteTemplate({
20
20
  templateSelector: "#test-tmpl",
21
- dataPath: "data_sources.ajax",
21
+ dataPath: "./data_sources.json",
22
22
  query: "word=ajax",
23
23
  zeroCallback: function () {
24
24
  alert("zero alert");
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ testEnvironment: "jsdom",
3
+ testMatch: ["**/tests/**/*.test.js"],
4
+ setupFilesAfterEnv: ["<rootDir>/tests/setup.js"],
5
+ collectCoverageFrom: ["jquery.infiniteScrollWithTemplate.js"],
6
+ coverageDirectory: "coverage",
7
+ verbose: true,
8
+ };
@@ -0,0 +1,65 @@
1
+ declare namespace JQuery {
2
+ interface InfiniteTemplateOptions {
3
+ /** jsRender template selector (required) */
4
+ templateSelector: string;
5
+
6
+ /** URL to load more data via AJAX (required) */
7
+ dataPath: string;
8
+
9
+ /** Additional query parameters (optional) */
10
+ query?: string;
11
+
12
+ /** Key for data array in JSON response (default: "data") */
13
+ key?: string;
14
+
15
+ /** HTTP method (default: "GET") */
16
+ method?: string;
17
+
18
+ /** Helper data to merge with each item (optional) */
19
+ templateHelpers?: Record<string, any>;
20
+
21
+ /** Load data on initialization (default: true) */
22
+ loadAtStart?: boolean;
23
+
24
+ /** Selector for click-to-load button (optional) */
25
+ loadSelector?: string;
26
+
27
+ /** Initial page number (default: 1) */
28
+ initialPage?: number;
29
+
30
+ /** Add timestamp to prevent caching (default: false) */
31
+ preventCache?: boolean;
32
+
33
+ /** Callback when no data returned (optional) */
34
+ zeroCallback?: () => void;
35
+
36
+ /** Callback on AJAX/JSON error (optional) */
37
+ errorCallback?: (error: any) => void;
38
+
39
+ /** Callback when loading starts (optional) */
40
+ loadingCallback?: () => void;
41
+
42
+ /** Callback when loading completes (optional) */
43
+ loadedCallback?: () => void;
44
+
45
+ /** Scroll threshold multiplier (default: 2) */
46
+ scrollThreshold?: number;
47
+ }
48
+
49
+ interface InfiniteTemplateStatic {
50
+ (options: InfiniteTemplateOptions): JQuery;
51
+ defaults: InfiniteTemplateOptions;
52
+ }
53
+
54
+ interface JQuery {
55
+ infiniteTemplate: InfiniteTemplateStatic;
56
+ }
57
+ }
58
+
59
+ interface JQueryStatic {
60
+ fn: {
61
+ infiniteTemplate: JQuery.InfiniteTemplateStatic;
62
+ };
63
+ }
64
+
65
+ export {};
@@ -15,19 +15,33 @@
15
15
 
16
16
  var opts = $.extend({}, $.fn.infiniteTemplate.defaults, settings);
17
17
 
18
+ // Validate required options
19
+ if (!opts.templateSelector) {
20
+ console.error("infiniteTemplate: templateSelector is required");
21
+ return $this;
22
+ }
23
+
24
+ if (!opts.dataPath) {
25
+ console.error("infiniteTemplate: dataPath is required");
26
+ return $this;
27
+ }
28
+
18
29
  var currentScrollPage = opts.initialPage;
19
30
  var scrollTriggered = false;
20
31
  var isFinished = false;
32
+ var namespace =
33
+ ".infiniteTemplate_" + Math.random().toString(36).substr(2, 9);
21
34
 
22
35
  if (opts.loadSelector) {
23
- $(document).on("click", opts.loadSelector, function () {
36
+ $(document).on("click" + namespace, opts.loadSelector, function () {
24
37
  triggerDataLoad();
25
38
  });
26
39
  } else {
27
- $(window).on("scroll", function () {
40
+ $(window).on("scroll" + namespace, function () {
28
41
  if (
29
42
  $(this).scrollTop() >
30
- $(document.body).height() - $(this).height() * 2 &&
43
+ $(document.body).height() -
44
+ $(window).height() * (opts.scrollThreshold || 2) &&
31
45
  !isFinished
32
46
  ) {
33
47
  triggerDataLoad();
@@ -42,18 +56,29 @@
42
56
 
43
57
  scrollTriggered = true;
44
58
 
45
- var tmpl = $.templates(opts.templateSelector);
59
+ if (typeof opts.loadingCallback === "function") {
60
+ opts.loadingCallback();
61
+ }
46
62
 
47
- var query = opts.query ? "&" + opts.query : "";
63
+ var tmpl = $.templates(opts.templateSelector);
48
64
 
49
- var timestamp = opts.preventCache ? "&t=" + new Date().getTime() : "";
65
+ var url = buildUrl(opts.dataPath, currentScrollPage);
50
66
 
51
67
  $.ajax({
52
- url: opts.dataPath + "?page=" + currentScrollPage + query + timestamp,
68
+ url: url,
53
69
  method: opts.method,
54
70
  success: function (result) {
55
71
  if (typeof result === "string") {
56
- result = JSON.parse(result);
72
+ try {
73
+ result = JSON.parse(result);
74
+ } catch (e) {
75
+ console.error("JSON parse error:", e);
76
+ scrollTriggered = false;
77
+ if (typeof opts.errorCallback === "function") {
78
+ opts.errorCallback(e);
79
+ }
80
+ return;
81
+ }
57
82
  }
58
83
 
59
84
  if (result) {
@@ -61,7 +86,6 @@
61
86
  isFinished = true;
62
87
 
63
88
  // if zero, call zeroCallback
64
- console.log(currentScrollPage + " " + opts.zeroCallback);
65
89
  if (
66
90
  currentScrollPage == 1 &&
67
91
  typeof opts.zeroCallback === "function"
@@ -69,24 +93,81 @@
69
93
  opts.zeroCallback();
70
94
  }
71
95
  } else {
96
+ var htmlContent = "";
97
+
72
98
  $.each(result[opts.key], function (_, item) {
73
99
  if (opts.templateHelpers) {
74
100
  item = Object.assign(item, opts.templateHelpers);
75
101
  }
76
102
 
77
103
  var html = tmpl.render(item);
78
- $(html).appendTo($this);
104
+ htmlContent += html;
79
105
  });
80
106
 
107
+ $this.append(htmlContent);
108
+
81
109
  currentScrollPage += 1;
82
110
  }
83
111
  }
84
112
 
85
113
  scrollTriggered = false;
86
114
  },
115
+ error: function (xhr, status, error) {
116
+ scrollTriggered = false;
117
+ console.error("AJAX error:", status, error);
118
+ if (typeof opts.errorCallback === "function") {
119
+ opts.errorCallback(error);
120
+ }
121
+ },
122
+ complete: function () {
123
+ if (typeof opts.loadedCallback === "function") {
124
+ opts.loadedCallback();
125
+ }
126
+ },
87
127
  });
88
128
  }
89
129
 
130
+ function buildUrl(baseUrl, page) {
131
+ var url;
132
+
133
+ // Check if baseUrl is absolute or relative
134
+ if (
135
+ baseUrl.indexOf("http://") === 0 ||
136
+ baseUrl.indexOf("https://") === 0
137
+ ) {
138
+ // Absolute URL
139
+ url = new URL(baseUrl);
140
+ } else if (window.location && window.location.href) {
141
+ // Resolve relative URLs from the current page's directory
142
+ var basePath = window.location.href.substring(
143
+ 0,
144
+ window.location.href.lastIndexOf("/") + 1,
145
+ );
146
+ url = new URL(baseUrl, basePath);
147
+ } else {
148
+ // Fallback: treat as relative to root
149
+ url = new URL(baseUrl, "http://localhost/");
150
+ }
151
+
152
+ url.searchParams.set("page", page);
153
+
154
+ if (opts.query) {
155
+ var queryParams = opts.query.split("&");
156
+ $.each(queryParams, function (_, param) {
157
+ var keyValue = param.split("=");
158
+ if (keyValue.length === 2) {
159
+ url.searchParams.set(keyValue[0], keyValue[1]);
160
+ }
161
+ });
162
+ }
163
+
164
+ if (opts.preventCache) {
165
+ url.searchParams.set("t", new Date().getTime());
166
+ }
167
+
168
+ return url.toString();
169
+ }
170
+
90
171
  if (opts.loadAtStart) {
91
172
  triggerDataLoad();
92
173
  }
@@ -107,5 +188,9 @@
107
188
  initialPage: 1,
108
189
  preventCache: false,
109
190
  zeroCallback: null,
191
+ errorCallback: null,
192
+ loadingCallback: null,
193
+ loadedCallback: null,
194
+ scrollThreshold: 2,
110
195
  };
111
196
  })(jQuery);
@@ -1 +1 @@
1
- !function(e){e.fn.infiniteTemplate=function(t){var l=e(this);if(!l.length)return l;var a=e.extend({},e.fn.infiniteTemplate.defaults,t),n=a.initialPage,o=!1,i=!1;function r(){if(!o&&!i){o=!0;var t=e.templates(a.templateSelector),r=a.query?"&"+a.query:"",c=a.preventCache?"&t="+(new Date).getTime():"";e.ajax({url:a.dataPath+"?page="+n+r+c,method:a.method,success:function(r){"string"==typeof r&&(r=JSON.parse(r)),r&&(0==r[a.key].length?(i=!0,console.log(n+" "+a.zeroCallback),1==n&&"function"==typeof a.zeroCallback&&a.zeroCallback()):(e.each(r[a.key],function(n,o){a.templateHelpers&&(o=Object.assign(o,a.templateHelpers));var i=t.render(o);e(i).appendTo(l)}),n+=1)),o=!1}})}}return a.loadSelector?e(document).on("click",a.loadSelector,function(){r()}):e(window).on("scroll",function(){e(this).scrollTop()>e(document.body).height()-2*e(this).height()&&!i&&r()}),a.loadAtStart&&r(),this},e.fn.infiniteTemplate.defaults={dataPath:null,templateSelector:null,query:null,key:"data",method:"GET",templateHelpers:null,loadAtStart:!0,loadSelector:null,initialPage:1,preventCache:!1,zeroCallback:null}}(jQuery);
1
+ !function(e){e.fn.infiniteTemplate=function(t){var l=e(this);if(!l.length)return l;var a=e.extend({},e.fn.infiniteTemplate.defaults,t);if(!a.templateSelector)return console.error("infiniteTemplate: templateSelector is required"),l;if(!a.dataPath)return console.error("infiniteTemplate: dataPath is required"),l;var r=a.initialPage,n=!1,o=!1,i=".infiniteTemplate_"+Math.random().toString(36).substr(2,9);function c(){if(!n&&!o){n=!0,"function"==typeof a.loadingCallback&&a.loadingCallback();var t=e.templates(a.templateSelector),i=function(t,l){var r;if(0===t.indexOf("http://")||0===t.indexOf("https://"))r=new URL(t);else if(window.location&&window.location.href){var n=window.location.href.substring(0,window.location.href.lastIndexOf("/")+1);r=new URL(t,n)}else r=new URL(t,"http://localhost/");if(r.searchParams.set("page",l),a.query){var o=a.query.split("&");e.each(o,function(e,t){var l=t.split("=");2===l.length&&r.searchParams.set(l[0],l[1])})}a.preventCache&&r.searchParams.set("t",(new Date).getTime());return r.toString()}(a.dataPath,r);e.ajax({url:i,method:a.method,success:function(i){if("string"==typeof i)try{i=JSON.parse(i)}catch(e){return console.error("JSON parse error:",e),n=!1,void("function"==typeof a.errorCallback&&a.errorCallback(e))}if(i)if(0==i[a.key].length)o=!0,1==r&&"function"==typeof a.zeroCallback&&a.zeroCallback();else{var c="";e.each(i[a.key],function(e,l){a.templateHelpers&&(l=Object.assign(l,a.templateHelpers));var r=t.render(l);c+=r}),l.append(c),r+=1}n=!1},error:function(e,t,l){n=!1,console.error("AJAX error:",t,l),"function"==typeof a.errorCallback&&a.errorCallback(l)},complete:function(){"function"==typeof a.loadedCallback&&a.loadedCallback()}})}}return a.loadSelector?e(document).on("click"+i,a.loadSelector,function(){c()}):e(window).on("scroll"+i,function(){e(this).scrollTop()>e(document.body).height()-e(window).height()*(a.scrollThreshold||2)&&!o&&c()}),a.loadAtStart&&c(),this},e.fn.infiniteTemplate.defaults={dataPath:null,templateSelector:null,query:null,key:"data",method:"GET",templateHelpers:null,loadAtStart:!0,loadSelector:null,initialPage:1,preventCache:!1,zeroCallback:null,errorCallback:null,loadingCallback:null,loadedCallback:null,scrollThreshold:2}}(jQuery);
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@cable8mm/jquery-infinite-with-template",
3
- "version": "1.0.4",
3
+ "version": "1.1.2",
4
4
  "title": "jQuery Infinite With Template",
5
5
  "description": "JQuery plugin for ajax-enabled infinite page scroll with template. If you like jQuery until now, this little library will help.",
6
6
  "main": "jquery.infiniteScrollWithTemplate.js",
7
+ "types": "jquery.infiniteScrollWithTemplate.d.ts",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "git+https://github.com/cable8mm/jquery-infinite-with-template.git"
@@ -21,9 +22,26 @@
21
22
  "dependencies": {
22
23
  "jsrender": ">= 1.0.0"
23
24
  },
25
+ "devDependencies": {
26
+ "@types/jest": "^29.5.0",
27
+ "http-server": "^14.1.1",
28
+ "jest": "^29.7.0",
29
+ "jest-environment-jsdom": "^29.7.0",
30
+ "jquery": "^3.7.0",
31
+ "jsrender": "^1.0.10",
32
+ "terser": "^5.19.0"
33
+ },
34
+ "scripts": {
35
+ "test": "jest",
36
+ "minify": "terser jquery.infiniteScrollWithTemplate.js -o jquery.infiniteScrollWithTemplate.min.js -c -m",
37
+ "build": "npm run minify && node scripts/prepare-dist.js",
38
+ "prepare-dist": "node scripts/prepare-dist.js",
39
+ "dev": "http-server . -p 8080 -o /examples/index.html",
40
+ "serve": "http-server . -p 8080"
41
+ },
24
42
  "license": "MIT",
25
43
  "bugs": {
26
44
  "url": "https://github.com/cable8mm/jquery-infinite-with-template/issues"
27
45
  },
28
46
  "homepage": "https://github.com/cable8mm/jquery-infinite-with-template#readme"
29
- }
47
+ }
@@ -0,0 +1,72 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const rootDir = path.resolve(__dirname, "..");
5
+ const distDir = path.join(rootDir, "dist");
6
+
7
+ // Clean and create dist directory
8
+ if (fs.existsSync(distDir)) {
9
+ fs.rmSync(distDir, { recursive: true });
10
+ }
11
+ fs.mkdirSync(distDir, { recursive: true });
12
+
13
+ // Copy examples folder to dist
14
+ const examplesDir = path.join(rootDir, "examples");
15
+ const distExamplesDir = path.join(distDir, "examples");
16
+
17
+ copyFolder(examplesDir, distExamplesDir);
18
+
19
+ // Copy main plugin file to dist
20
+ const pluginSource = path.join(rootDir, "jquery.infiniteScrollWithTemplate.js");
21
+ const pluginDest = path.join(distDir, "jquery.infiniteScrollWithTemplate.js");
22
+
23
+ fs.copyFileSync(pluginSource, pluginDest);
24
+
25
+ // Copy minified plugin file if exists
26
+ const minifiedSource = path.join(
27
+ rootDir,
28
+ "jquery.infiniteScrollWithTemplate.min.js",
29
+ );
30
+ if (fs.existsSync(minifiedSource)) {
31
+ const minifiedDest = path.join(
32
+ distDir,
33
+ "jquery.infiniteScrollWithTemplate.min.js",
34
+ );
35
+ fs.copyFileSync(minifiedSource, minifiedDest);
36
+ }
37
+
38
+ // Copy TypeScript definitions if exists
39
+ const typesSource = path.join(
40
+ rootDir,
41
+ "jquery.infiniteScrollWithTemplate.d.ts",
42
+ );
43
+ if (fs.existsSync(typesSource)) {
44
+ const typesDest = path.join(
45
+ distDir,
46
+ "jquery.infiniteScrollWithTemplate.d.ts",
47
+ );
48
+ fs.copyFileSync(typesSource, typesDest);
49
+ }
50
+
51
+ console.log("✓ Dist folder prepared successfully");
52
+ console.log(" - examples/ folder copied");
53
+ console.log(" - jquery.infiniteScrollWithTemplate.js copied");
54
+ console.log(" - jquery.infiniteScrollWithTemplate.min.js copied");
55
+ console.log(" - jquery.infiniteScrollWithTemplate.d.ts copied");
56
+
57
+ function copyFolder(src, dest) {
58
+ fs.mkdirSync(dest, { recursive: true });
59
+
60
+ const entries = fs.readdirSync(src, { withFileTypes: true });
61
+
62
+ for (const entry of entries) {
63
+ const srcPath = path.join(src, entry.name);
64
+ const destPath = path.join(dest, entry.name);
65
+
66
+ if (entry.isDirectory()) {
67
+ copyFolder(srcPath, destPath);
68
+ } else {
69
+ fs.copyFileSync(srcPath, destPath);
70
+ }
71
+ }
72
+ }