billy-herrington-utils 1.5.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +2 -2
  2. package/dist/billy-herrington-utils.es.js +5907 -327
  3. package/dist/billy-herrington-utils.es.js.map +1 -1
  4. package/dist/billy-herrington-utils.umd.js +5911 -331
  5. package/dist/billy-herrington-utils.umd.js.map +1 -1
  6. package/dist/index.d.ts +110 -103
  7. package/package.json +7 -5
  8. package/src/index.ts +18 -5
  9. package/src/types/globals.d.ts +0 -2
  10. package/src/userscripts/data-manager/data-filter.ts +110 -0
  11. package/src/userscripts/data-manager/index.ts +77 -174
  12. package/src/userscripts/infinite-scroll/index.ts +72 -71
  13. package/src/userscripts/pagination-parsing/index.ts +21 -29
  14. package/src/userscripts/pagination-parsing/pagination-strategies/PaginationStrategy.ts +4 -12
  15. package/src/userscripts/pagination-parsing/pagination-strategies/PaginationStrategyDataParams.ts +11 -2
  16. package/src/userscripts/pagination-parsing/pagination-strategies/PaginationStrategyPathnameParams.ts +29 -2
  17. package/src/userscripts/pagination-parsing/pagination-strategies/PaginationStrategySearchParams.ts +15 -7
  18. package/src/userscripts/pagination-parsing/pagination-strategies/PaginationStrategyTrash.ts +9 -12
  19. package/src/userscripts/pagination-parsing/pagination-strategies/index.ts +1 -1
  20. package/src/userscripts/pagination-parsing/pagination-utils/index.ts +36 -7
  21. package/src/userscripts/router/router.ts +71 -0
  22. package/src/userscripts/rules/index.ts +227 -3
  23. package/src/userscripts/types/index.ts +19 -0
  24. package/src/utils/arrays/index.ts +3 -1
  25. package/src/utils/async/index.ts +22 -6
  26. package/src/utils/dom/index.ts +35 -68
  27. package/src/utils/dom/observers.ts +76 -0
  28. package/src/utils/events/index.ts +9 -2
  29. package/src/utils/fetch/index.ts +14 -7
  30. package/src/utils/objects/index.ts +25 -0
  31. package/src/utils/observers/index.ts +8 -2
  32. package/src/utils/parsers/index.ts +18 -11
  33. package/src/utils/strings/index.ts +5 -5
  34. package/src/utils/strings/regexes.ts +31 -0
  35. package/src/utils/userscript/index.ts +10 -0
  36. package/src/userscripts/jabroni-outfit-wrap/index.ts +0 -40
@@ -1,192 +1,90 @@
1
+ import type { StoreState } from 'jabroni-outfit';
1
2
  import { LazyImgLoader } from '../../utils/observers';
2
- import { stringToWords } from '../../utils/strings';
3
-
4
- interface DataFilterState {
5
- filterPublic: boolean;
6
- filterPrivate: boolean;
7
- filterHD: boolean;
8
- filterDuration: boolean;
9
- filterDurationFrom: number;
10
- filterDurationTo: number;
11
- filterExclude: boolean;
12
- filterExcludeWords: string;
13
- filterInclude: boolean;
14
- filterIncludeWords: string;
15
- }
16
-
17
- interface FilterResult {
18
- tag: string;
19
- condition: boolean;
20
- }
3
+ import { assignGlobals } from '../../utils/userscript';
4
+ import type { RulesGlobal } from '../rules';
5
+ import { DataFilter, type FilterFunction } from './data-filter';
21
6
 
22
- type FilterInput = Record<string, string | number | boolean | HTMLElement>;
23
- type FilterFunction = (v: FilterInput) => FilterResult;
7
+ export type DataElement = Record<string, string | number | boolean | HTMLElement>;
24
8
 
25
- class DataFilter {
26
- public filters: { [key: string]: () => FilterFunction };
9
+ export class DataManager {
10
+ private data = new Map<string, DataElement>();
11
+ private lazyImgLoader = new LazyImgLoader(
12
+ (target: Element) => !this.isFiltered(target as HTMLElement),
13
+ );
14
+ private dataFilter: DataFilter;
27
15
 
28
16
  constructor(
29
- private rules: IRules,
30
- private state: DataFilterState,
17
+ private rules: RulesGlobal,
18
+ private state: StoreState,
31
19
  ) {
32
- this.state = state;
33
-
34
- const methods = Object.getOwnPropertyNames(this);
35
- this.filters = methods.reduce((acc: { [key: string]: () => FilterFunction }, k) => {
36
- if (k in this.state) {
37
- acc[k] = this[k as keyof DataFilter] as unknown as () => FilterFunction;
38
- GM_addStyle(`.filter-${k.toLowerCase().slice(6)} { display: none !important; }`);
39
- }
40
- return acc;
41
- }, {});
42
- }
43
-
44
- filterPublic = (): FilterFunction => {
45
- return (v: FilterInput) => {
46
- const isPublic = !this.rules.isPrivate(v.element as HTMLElement);
47
- return {
48
- condition: this.state.filterPublic && isPublic,
49
- tag: 'filter-public',
50
- };
51
- };
52
- };
53
-
54
- filterPrivate = (): FilterFunction => {
55
- return (v: FilterInput) => {
56
- const isPrivate = this.rules.isPrivate(v.element as HTMLElement);
57
- return {
58
- condition: this.state.filterPrivate && isPrivate,
59
- tag: 'filter-private',
60
- };
61
- };
62
- };
63
-
64
- filterHD = (): FilterFunction => {
65
- return (v: FilterInput) => {
66
- const isHD = this.rules.isHD(v.element as HTMLElement);
67
- return {
68
- condition: this.state.filterHD && isHD,
69
- tag: 'filter-hd',
70
- };
71
- };
72
- };
73
-
74
- filterDuration = (): FilterFunction => {
75
- return (v: FilterInput) => {
76
- const notInRange =
77
- (v.duration as number) < this.state.filterDurationFrom ||
78
- (v.duration as number) > this.state.filterDurationTo;
79
- return {
80
- condition: this.state.filterDuration && notInRange,
81
- tag: 'filter-duration',
82
- };
83
- };
84
- };
85
-
86
- filterExclude = (): FilterFunction => {
87
- const tags = DataManager.filterDSLToRegex(this.state.filterExcludeWords);
88
- return (v: FilterInput) => {
89
- const containTags = tags.some((tag) => tag.test(v.title as string));
90
- return {
91
- condition: this.state.filterExclude && containTags,
92
- tag: 'filter-exclude',
93
- };
94
- };
95
- };
96
-
97
- filterInclude = (): FilterFunction => {
98
- const tags = DataManager.filterDSLToRegex(this.state.filterIncludeWords);
99
- return (v: FilterInput) => {
100
- const containTagsNot = tags.some((tag) => !tag.test(v.title as string));
101
- return {
102
- condition: this.state.filterInclude && containTagsNot,
103
- tag: 'filter-include',
104
- };
105
- };
106
- };
107
- }
20
+ this.dataFilter = new DataFilter(rules, state);
108
21
 
109
- interface IRules {
110
- getThumbs: (html: HTMLElement) => HTMLElement[];
111
- getThumbUrl: (thumbElement: HTMLElement) => string;
112
- getThumbData: (thumbElement: HTMLElement) => { title: string; duration: number };
113
- getThumbImgData: (thumbElement: HTMLElement) => { img: HTMLElement; imgSrc: string };
114
- container: HTMLElement;
115
- isPrivate: (element: HTMLElement) => boolean;
116
- isHD: (element: HTMLElement) => boolean;
117
- }
118
-
119
- export class DataManager {
120
- private rules: IRules;
121
- private state: DataFilterState;
122
- private data: Map<string, FilterInput>;
123
- private lazyImgLoader: LazyImgLoader;
124
- public dataFilters: { [key: string]: () => FilterFunction };
125
-
126
- constructor(rules: IRules, state: DataFilterState) {
127
- this.rules = rules;
128
- this.state = state;
129
- this.data = new Map();
130
- this.lazyImgLoader = new LazyImgLoader(
131
- (target: Element) => !this.isFiltered(target as HTMLElement),
132
- );
133
- this.dataFilters = new DataFilter(rules, state).filters;
134
-
135
- const targets = [window, (globalThis as any).unsafeWindow].filter(Boolean);
136
- targets.forEach((w: any) => {
137
- Object.assign(w, {
138
- sortByDuration: () => this.sort('duration'),
139
- sortByViews: () => this.sort('view'),
140
- });
22
+ assignGlobals({
23
+ sortByDuration: () => this.sortBy('duration'),
24
+ sortByViews: () => this.sortBy('view'),
141
25
  });
142
26
  }
143
27
 
144
- static filterDSLToRegex(str: string): RegExp[] {
145
- const toFullWord = (w: string) => `(^|\\ )${w}($|\\ )`;
146
- const str_ = str.replace(/f:(\w+)/g, (_, w) => toFullWord(w));
147
- return stringToWords(str_).map((expr: string) => new RegExp(expr, 'i'));
148
- }
149
-
150
- isFiltered(el: HTMLElement): boolean {
28
+ public isFiltered(el: HTMLElement): boolean {
151
29
  return el.className.includes('filtered');
152
30
  }
153
31
 
154
- applyFilters = (filters: { [key: string]: boolean }, offset = 0): void => {
32
+ public applyFilters = (filters: { [key: string]: boolean } = {}, offset = 0): void => {
155
33
  const filtersToApply = Object.keys(filters)
156
- .filter((k) => Object.hasOwn(this.dataFilters, k))
157
- .map((k) => this.dataFilters[k]());
34
+ .filter((k) => this.dataFilter.filters.has(k))
35
+ .map((k) => this.dataFilter.filters.get(k) as () => FilterFunction);
158
36
 
159
37
  if (filtersToApply.length === 0) return;
160
38
 
161
- const updates: (() => void)[] = [];
162
- let offset_counter = 1;
163
- for (const v of this.data.values()) {
164
- if (++offset_counter > offset) {
39
+ const iterator = this.data.values();
40
+ let currentIndex = 0;
41
+
42
+ const runBatch = (deadline: IdleDeadline) => {
43
+ while (currentIndex < offset) {
44
+ const skip = iterator.next();
45
+ if (skip.done) return;
46
+ currentIndex++;
47
+ }
48
+
49
+ const updates: { e: HTMLElement; tag: string; condition: boolean }[] = [];
50
+
51
+ while (deadline.timeRemaining() > 0) {
52
+ const { value, done } = iterator.next();
53
+ if (done) break;
54
+
165
55
  for (const f of filtersToApply) {
166
- const { tag, condition } = f(v as FilterInput);
167
- updates.push(() => (v.element as HTMLElement).classList.toggle(tag, condition));
56
+ const { tag, condition } = f()(value);
57
+ updates.push({ e: value.element as HTMLElement, tag, condition });
168
58
  }
169
59
  }
170
- }
171
60
 
172
- requestAnimationFrame(() => {
173
- updates.forEach((update) => {
174
- update();
175
- });
176
- });
61
+ if (updates.length > 0) {
62
+ requestAnimationFrame(() => {
63
+ updates.forEach((u) => {
64
+ u.e.classList.toggle(u.tag, u.condition);
65
+ });
66
+ });
67
+ }
68
+
69
+ if (currentIndex < this.data.size) {
70
+ requestIdleCallback(runBatch);
71
+ }
72
+ };
73
+
74
+ requestIdleCallback(runBatch);
177
75
  };
178
76
 
179
- filterAll = (offset?: number): void => {
77
+ public filterAll = (offset?: number): void => {
180
78
  const filters = Object.assign(
181
79
  {},
182
- ...Object.keys(this.dataFilters).map((f) => ({
183
- [f]: this.state[f as keyof DataFilterState],
80
+ ...Object.keys(this.dataFilter.filters).map((f) => ({
81
+ [f]: this.state[f as keyof StoreState],
184
82
  })),
185
83
  );
186
84
  this.applyFilters(filters, offset);
187
85
  };
188
86
 
189
- parseData = (
87
+ public parseData = (
190
88
  html: HTMLElement,
191
89
  container?: HTMLElement,
192
90
  removeDuplicates = false,
@@ -194,10 +92,12 @@ export class DataManager {
194
92
  ): void => {
195
93
  const thumbs = this.rules.getThumbs(html);
196
94
  const data_offset = this.data.size;
95
+ const fragment = document.createDocumentFragment();
96
+ const parent = container || this.rules.container;
197
97
 
198
98
  for (const thumbElement of thumbs) {
199
99
  const url = this.rules.getThumbUrl(thumbElement);
200
- if (!url || this.data.has(url)) {
100
+ if (!url || this.data.has(url) || parent.contains(thumbElement)) {
201
101
  if (removeDuplicates) thumbElement.remove();
202
102
  continue;
203
103
  }
@@ -207,32 +107,35 @@ export class DataManager {
207
107
 
208
108
  if (shouldLazify) {
209
109
  const { img, imgSrc } = this.rules.getThumbImgData(thumbElement);
210
- this.lazyImgLoader.lazify(thumbElement, img as HTMLImageElement, imgSrc);
110
+ this.lazyImgLoader.lazify(thumbElement, img, imgSrc);
211
111
  }
212
112
 
213
- const parent = container || this.rules.container;
214
- if (!parent.contains(thumbElement)) parent.appendChild(thumbElement);
113
+ fragment.append(thumbElement);
215
114
  }
216
115
 
116
+ requestAnimationFrame(() => {
117
+ parent.appendChild(fragment);
118
+ });
119
+
217
120
  this.filterAll(data_offset);
218
121
  };
219
122
 
220
- sort(propName: string) {
123
+ public sortBy<K extends keyof DataElement>(key: K): void {
221
124
  if (this.data.size < 2) return;
222
125
 
223
- const sorted = Array.from(this.data.keys()).sort((b, a) => {
224
- return (
225
- ((this.data.get(a) as FilterInput)[propName] as number) -
226
- ((this.data.get(b) as FilterInput)[propName] as number)
227
- );
228
- });
126
+ const sorted: DataElement[] = Array.from(this.data.values()).sort(
127
+ (a: DataElement, b: DataElement) => {
128
+ return (a[key] as number) - (b[key] as number);
129
+ },
130
+ );
229
131
 
230
- const container = ((this.data.get(sorted[0]) as FilterInput).element as HTMLElement)
231
- .parentElement as HTMLElement;
132
+ const container = (sorted[0].element as HTMLElement).parentElement as HTMLElement;
133
+ container.style.visibility = 'hidden';
232
134
 
233
135
  sorted.forEach((s) => {
234
- const e = (this.data.get(s) as FilterInput).element as HTMLElement;
235
- container.append(e);
136
+ container.append(s.element as HTMLElement);
236
137
  });
138
+
139
+ container.style.visibility = 'visible';
237
140
  }
238
141
  }
@@ -1,64 +1,40 @@
1
+ import type { JabronioStore } from 'jabroni-outfit';
1
2
  import { fetchHtml } from '../../utils/fetch';
2
3
  import { Observer } from '../../utils/observers';
4
+ import type { RulesGlobal } from '../rules';
3
5
 
4
- interface IInfiniteScroller {
5
- delay?: number;
6
- enabled?: boolean;
7
- writeHistory?: boolean;
8
- paginationOffset: number;
9
- paginationLast: number;
10
- paginationElement: HTMLElement;
11
- paginationUrlGenerator: (offset: number) => string;
12
- parseData: (document: HTMLElement) => void;
13
- intersectionObservable?: HTMLElement;
14
- alternativeGenerator?: () => OffsetGenerator;
15
- }
6
+ type InfiniteScrollerOptions = Pick<InfiniteScroller, 'rules'> & Partial<InfiniteScroller>;
7
+ type GeneratorResult = { url: string; offset: number };
8
+ export type OffsetGenerator = Generator<GeneratorResult> | AsyncGenerator<GeneratorResult>;
16
9
 
17
- interface GeneratorResult {
18
- url: string;
19
- offset: number;
20
- }
10
+ export class InfiniteScroller {
11
+ public enabled = true;
12
+ public delay = 200;
13
+ public paginationOffset = 1;
14
+ public writeHistory = false;
15
+ public parseData?: (document: HTMLElement) => void;
16
+ public rules: RulesGlobal;
21
17
 
22
- type OffsetGenerator = Generator<GeneratorResult> | AsyncGenerator<GeneratorResult>;
18
+ private observer?: Observer;
19
+ private paginationGenerator: OffsetGenerator;
23
20
 
24
- export class InfiniteScroller {
25
- public paginationGenerator: OffsetGenerator;
26
- public enabled: boolean;
27
- public delay: number;
28
- public paginationOffset: number;
29
- public paginationLast: number;
30
- public writeHistory: boolean;
31
- private parseData: (document: HTMLElement) => void;
32
-
33
- constructor({
34
- enabled = true,
35
- delay = 300,
36
- writeHistory = false,
37
- paginationOffset,
38
- paginationLast,
39
- paginationElement,
40
- paginationUrlGenerator,
41
- parseData,
42
- alternativeGenerator,
43
- intersectionObservable,
44
- }: IInfiniteScroller) {
45
- this.enabled = enabled;
46
- this.delay = delay;
47
- this.writeHistory = writeHistory;
48
- this.paginationOffset = paginationOffset;
49
- this.paginationLast = paginationLast;
50
- this.parseData = parseData;
51
-
52
- this.paginationGenerator =
53
- alternativeGenerator?.() ??
54
- InfiniteScroller.createPaginationGenerator(
55
- paginationOffset,
56
- paginationLast,
57
- paginationUrlGenerator,
58
- );
59
-
60
- const observable = intersectionObservable || paginationElement;
61
- Observer.observeWhile(observable, this.generatorConsumer, this.delay);
21
+ constructor(options: InfiniteScrollerOptions) {
22
+ this.rules = options.rules;
23
+ this.paginationOffset = this.rules.paginationStrategy.getPaginationOffset();
24
+ Object.assign(this, options);
25
+
26
+ this.paginationGenerator = this.createPaginationGenerator();
27
+ this.setObserver(this.rules.observable);
28
+ }
29
+
30
+ public dispose() {
31
+ if (this.observer) this.observer.dispose();
32
+ }
33
+
34
+ public setObserver(observable: HTMLElement) {
35
+ if (this.observer) this.observer.dispose();
36
+ this.observer = Observer.observeWhile(observable, this.generatorConsumer, this.delay);
37
+ return this;
62
38
  }
63
39
 
64
40
  private onScrollCBs: Array<(scroller: InfiniteScroller) => void> = [];
@@ -79,27 +55,52 @@ export class InfiniteScroller {
79
55
  if (!this.enabled) return false;
80
56
  const { value: { url, offset } = {}, done } = await this.paginationGenerator.next();
81
57
  if (!done) {
82
- const nextPageHTML = await fetchHtml(url);
83
- const prevScrollPos = document.documentElement.scrollTop;
84
- this.paginationOffset = offset;
85
- this.parseData(nextPageHTML);
86
- this._onScroll();
87
- window.scrollTo(0, prevScrollPos);
88
- if (this.writeHistory) {
89
- history.replaceState({}, '', url);
90
- }
58
+ await this.doScroll(url, offset);
91
59
  }
92
60
  return !done;
93
61
  };
94
62
 
95
- static *createPaginationGenerator(
96
- currentPage: number,
97
- totalPages: number,
98
- generateURL: (offset: number) => string,
99
- ): OffsetGenerator {
100
- for (let offset = currentPage + 1; offset <= totalPages; offset++) {
101
- const url = generateURL(offset);
63
+ async doScroll(url: string, offset: number) {
64
+ const nextPageHTML = await fetchHtml(url);
65
+ const prevScrollPos = document.documentElement.scrollTop;
66
+ this.paginationOffset = Math.max(this.paginationOffset, offset);
67
+ this.parseData?.(nextPageHTML);
68
+ this._onScroll();
69
+ window.scrollTo(0, prevScrollPos);
70
+ if (this.writeHistory) {
71
+ history.replaceState({}, '', url);
72
+ }
73
+ }
74
+
75
+ private *createPaginationGenerator(): OffsetGenerator {
76
+ const curPage = this.rules.paginationStrategy.getPaginationOffset();
77
+ const lastPage = this.rules.paginationStrategy.getPaginationLast();
78
+ for (let offset = curPage + 1; offset <= lastPage; offset++) {
79
+ const url = this.rules.paginationStrategy.getPaginationUrlGenerator()(offset);
102
80
  yield { url, offset };
103
81
  }
104
82
  }
83
+
84
+ static create(
85
+ store: JabronioStore,
86
+ rules: RulesGlobal,
87
+ parseData: (document: HTMLElement) => void,
88
+ ) {
89
+ const enabled = store.state.infiniteScrollEnabled as boolean;
90
+
91
+ store.state.$paginationLast = rules.paginationStrategy.getPaginationLast();
92
+
93
+ const infiniteScroller = new InfiniteScroller({ enabled, parseData, rules }).onScroll(
94
+ ({ paginationOffset }) => {
95
+ store.state.$aginationOffset = paginationOffset;
96
+ },
97
+ true,
98
+ );
99
+
100
+ store.subscribe(() => {
101
+ infiniteScroller.enabled = store.state.infiniteScrollEnabled as boolean;
102
+ });
103
+
104
+ return infiniteScroller;
105
+ }
105
106
  }
@@ -1,53 +1,39 @@
1
1
  import {
2
- type IPaginationStrategy,
3
2
  PaginationStrategy,
4
3
  PaginationStrategyDataParams,
5
4
  PaginationStrategyPathnameParams,
6
5
  PaginationStrategySearchParams,
7
6
  } from './pagination-strategies';
8
- import { getPaginationLinks, parseURL, upgradePathname } from './pagination-utils';
7
+ import { getPaginationLinks } from './pagination-utils';
9
8
 
10
- export function getPaginationStrategy(options: IPaginationStrategy): PaginationStrategy {
11
- const {
12
- doc = document,
13
- url = location.href,
14
- paginationSelector = '.pagination',
15
- searchParamSelector,
16
- } = options;
9
+ export function getPaginationStrategy(
10
+ options: Partial<PaginationStrategy>,
11
+ ): PaginationStrategy {
12
+ const _paginationStrategy = new PaginationStrategy(options);
13
+ const pagination = _paginationStrategy.getPaginationElement();
17
14
 
18
- const pagination = doc.querySelector(paginationSelector);
15
+ Object.assign(options, { ..._paginationStrategy });
16
+ const { url, searchParamSelector } = options;
19
17
 
20
18
  if (!pagination) {
21
19
  console.error('Found No Pagination');
22
- return new PaginationStrategy(options);
20
+ return _paginationStrategy;
23
21
  }
24
22
 
25
23
  const pageLinks = getPaginationLinks(pagination, url).map((l) => new URL(l));
26
24
 
27
25
  console.log({ pageLinks: pageLinks.map((l) => l.href) });
28
26
 
29
- const getStrategy = (): typeof PaginationStrategy => {
30
- const dataParamLinks = Array.from(pagination.querySelectorAll('[data-parameters *= from]'));
31
- if (dataParamLinks.length > 0) {
32
- console.log('PaginationStrategyDataParams', dataParamLinks);
27
+ const selectStrategy = (): typeof PaginationStrategy => {
28
+ if (PaginationStrategyDataParams.testLinks(pagination)) {
33
29
  return PaginationStrategyDataParams;
34
30
  }
35
31
 
36
- if (pageLinks.some((h) => PaginationStrategySearchParams.checkLink(h, searchParamSelector))) {
37
- const l = pageLinks
38
- .filter((h) => PaginationStrategySearchParams.checkLink(h, searchParamSelector))
39
- .map((h) => h.href);
40
- console.log('PaginationStrategySearchParams', l);
32
+ if (PaginationStrategySearchParams.testLinks(pageLinks, searchParamSelector)) {
41
33
  return PaginationStrategySearchParams;
42
34
  }
43
35
 
44
- if (pageLinks.some((h) => /\/(page\/)?\d+\/?$/.test(h.pathname))) {
45
- const pathnameMatched = pageLinks.filter((h) => /\/(page\/)?\d+\/?$/.test(h.pathname));
46
- console.log(
47
- 'PaginationStrategyPathnameParams',
48
- pathnameMatched.map((h) => h.href),
49
- );
50
- options.url = upgradePathname(parseURL(url), pathnameMatched);
36
+ if (PaginationStrategyPathnameParams.testLinks(pageLinks, options)) {
51
37
  return PaginationStrategyPathnameParams;
52
38
  }
53
39
 
@@ -55,9 +41,15 @@ export function getPaginationStrategy(options: IPaginationStrategy): PaginationS
55
41
  return PaginationStrategy;
56
42
  };
57
43
 
58
- const paginationStrategy = new (getStrategy())(options);
44
+ const PaginationStrategyConstructor = selectStrategy();
45
+ const paginationStrategy = new PaginationStrategyConstructor(options);
59
46
 
60
- console.log('paginationStrategy', paginationStrategy);
47
+ console.log(
48
+ 'paginationStrategy:',
49
+ PaginationStrategyConstructor.name,
50
+ '\n',
51
+ paginationStrategy,
52
+ );
61
53
 
62
54
  return paginationStrategy;
63
55
  }
@@ -1,25 +1,17 @@
1
1
  import { parseURL } from '../pagination-utils';
2
2
 
3
- export interface IPaginationStrategy {
4
- url?: URL | Location | string;
5
- doc?: Document | HTMLElement;
6
- paginationSelector?: string;
7
- fixPaginationLast?: (n: number, offset?: number) => number;
8
- pathnameSelector?: RegExp;
9
- searchParamSelector?: string;
10
- offsetMin?: number;
11
- }
12
-
13
3
  export class PaginationStrategy {
14
4
  public doc = document;
15
5
  public url: URL;
16
6
  public paginationSelector = '.pagination';
17
7
  public searchParamSelector = 'page';
8
+ public static _pathnameSelector = /\/(page\/)?\d+\/?$/;
18
9
  public pathnameSelector = /\/(\d+)\/?$/;
10
+ public dataparamSelector = '[data-parameters *= from]';
19
11
  public fixPaginationLast?: (n: number, offset?: number) => number;
20
12
  public offsetMin = 1;
21
13
 
22
- constructor(options?: IPaginationStrategy) {
14
+ constructor(options?: Partial<PaginationStrategy>) {
23
15
  if (options) {
24
16
  Object.entries(options).forEach(([k, v]) => {
25
17
  Object.assign(this, { [k]: v });
@@ -30,7 +22,7 @@ export class PaginationStrategy {
30
22
  }
31
23
 
32
24
  getPaginationElement() {
33
- return this.doc.querySelector(this.paginationSelector);
25
+ return this.doc.querySelector<HTMLElement>(this.paginationSelector);
34
26
  }
35
27
 
36
28
  get hasPagination() {
@@ -3,7 +3,7 @@ import { PaginationStrategy } from './PaginationStrategy';
3
3
 
4
4
  export class PaginationStrategyDataParams extends PaginationStrategy {
5
5
  getPaginationLast() {
6
- const links = this.getPaginationElement()?.querySelectorAll('[data-parameters *= from]');
6
+ const links = this.getPaginationElement()?.querySelectorAll(this.dataparamSelector);
7
7
  const pages = Array.from(links || [], (l) => {
8
8
  const p = l.getAttribute('data-parameters');
9
9
  const v = p?.match(/from\w*:(\d+)/)?.[1] || this.offsetMin.toString();
@@ -31,7 +31,9 @@ export class PaginationStrategyDataParams extends PaginationStrategy {
31
31
  'a[data-block-id][data-parameters]',
32
32
  );
33
33
  const block_id = parametersElement?.getAttribute('data-block-id') || '';
34
- const parameters = parseDataParams(parametersElement?.getAttribute('data-parameters') || '');
34
+ const parameters = parseDataParams(
35
+ parametersElement?.getAttribute('data-parameters') || '',
36
+ );
35
37
 
36
38
  const attrs: Record<string, string> = {
37
39
  block_id,
@@ -54,4 +56,11 @@ export class PaginationStrategyDataParams extends PaginationStrategy {
54
56
 
55
57
  return paginationUrlGenerator;
56
58
  }
59
+
60
+ static testLinks(doc: HTMLElement | Document = document) {
61
+ const dataParamLinks = Array.from(
62
+ doc.querySelectorAll<HTMLElement>('[data-parameters *= from]'),
63
+ );
64
+ return dataParamLinks.length > 0;
65
+ }
57
66
  }
@@ -1,13 +1,40 @@
1
- import { getPaginationLinks } from '../pagination-utils';
1
+ import { getPaginationLinks, parseURL, upgradePathname } from '../pagination-utils';
2
2
  import { PaginationStrategy } from './PaginationStrategy';
3
3
 
4
4
  export class PaginationStrategyPathnameParams extends PaginationStrategy {
5
5
  extractPage = (a: HTMLAnchorElement | Location | string): number => {
6
6
  const href = typeof a === 'string' ? a : a.href;
7
7
  const { pathname } = new URL(href, this.doc.baseURI || this.url.origin);
8
- return parseInt(pathname.match(this.pathnameSelector)?.pop() || this.offsetMin.toString());
8
+ return parseInt(
9
+ pathname.match(this.pathnameSelector)?.pop() || this.offsetMin.toString(),
10
+ );
9
11
  };
10
12
 
13
+ static checkLink(
14
+ link: URL,
15
+ pathnameSelector: RegExp = PaginationStrategy._pathnameSelector,
16
+ ): boolean {
17
+ return pathnameSelector.test(link.pathname);
18
+ }
19
+
20
+ static testLinks(links: URL[], options: Partial<PaginationStrategy>): boolean {
21
+ const result = links.some((h) =>
22
+ PaginationStrategyPathnameParams.checkLink(h, options.pathnameSelector),
23
+ );
24
+
25
+ if (result) {
26
+ const pathnamesMatched = links.filter((h) =>
27
+ PaginationStrategyPathnameParams.checkLink(h, options.pathnameSelector),
28
+ );
29
+ options.url = upgradePathname(
30
+ parseURL(options.url as unknown as string),
31
+ pathnamesMatched,
32
+ );
33
+ }
34
+
35
+ return result;
36
+ }
37
+
11
38
  getPaginationLast() {
12
39
  const links = getPaginationLinks(
13
40
  (this.getPaginationElement() || document) as HTMLElement,