nodebb-plugin-recent-cards-2 3.3.19

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.
@@ -0,0 +1,368 @@
1
+ 'use strict';
2
+
3
+ $(document).ready(function () {
4
+ const rtl = $('html').attr('data-dir') === 'rtl';
5
+ const arrowClasses = 'link-secondary position-absolute top-50 translate-middle-y p-1 z-1';
6
+ const nextIcon = rtl ? 'fa-chevron-left' : 'fa-chevron-right';
7
+ const prevIcon = rtl ? 'fa-chevron-right' : 'fa-chevron-left';
8
+ const nextArrow = `<a href="#" class="${arrowClasses} slick-next" title="">
9
+ <i class="fa-solid fa-fw ${nextIcon} fa-lg"></i>
10
+ </a>`;
11
+ const prevArrow = `<a href="#" class="${arrowClasses} slick-prev" title="">
12
+ <i class="fa-solid fa-fw ${prevIcon} fa-lg"></i>
13
+ </a>`;
14
+
15
+ async function initSlick(container) {
16
+ if (!container.length || container.hasClass('slick-initialized')) {
17
+ return;
18
+ }
19
+
20
+ if (!config.recentCards || !config.recentCards.enableCarousel) {
21
+ container.removeClass('carousel-mode invisible');
22
+ return;
23
+ }
24
+
25
+ const slideCount = parseInt(config.recentCards.maxSlides, 10) || 4;
26
+ const slideMargin = 16;
27
+ const env = utils.findBootstrapEnvironment();
28
+ if (['xxl', 'xl', 'lg'].includes(env)) {
29
+ const cards = container.find('.recent-card-container .recent-card');
30
+ const cardCount = Math.min(cards.length, slideCount);
31
+ if (cardCount > 0) {
32
+ cards.css({
33
+ width: (container.width() - ((cardCount - 1) * slideMargin)) / cardCount,
34
+ });
35
+ }
36
+ }
37
+ container.slick({
38
+ infinite: false,
39
+ slidesToShow: slideCount,
40
+ slidesToScroll: slideCount,
41
+ rtl: rtl,
42
+ variableWidth: true,
43
+ dots: !!config.recentCards.enableCarouselPagination,
44
+ nextArrow: nextArrow,
45
+ prevArrow: prevArrow,
46
+ responsive: [{
47
+ breakpoint: 992, // md
48
+ settings: {
49
+ slidesToShow: 3,
50
+ slidesToScroll: 2,
51
+ infinite: false,
52
+ },
53
+ }, {
54
+ breakpoint: 768, // sm/xs
55
+ settings: {
56
+ slidesToShow: 2,
57
+ slidesToScroll: 1,
58
+ infinite: false,
59
+ },
60
+ }],
61
+ });
62
+
63
+ container.removeClass('overflow-hidden invisible');
64
+ container.find('.slick-prev').translateAttr('title', '[[global:pagination.previouspage]]');
65
+ container.find('.slick-next').translateAttr('title', '[[global:pagination.nextpage]]');
66
+ }
67
+
68
+ // --- Category Filter ---
69
+
70
+ const isMobile = () => window.innerWidth < 576;
71
+
72
+ function openDropdown(pluginContainer) {
73
+ const dropdown = pluginContainer.find('.rc-filter-dropdown');
74
+ const btn = pluginContainer.find('.rc-filter-btn');
75
+ dropdown.removeClass('d-none');
76
+ btn.attr('aria-expanded', 'true');
77
+
78
+ // On mobile: add backdrop and lock body scroll
79
+ if (isMobile()) {
80
+ const backdrop = $('<div class="rc-backdrop"></div>');
81
+ backdrop.on('click', function () {
82
+ closeAllDropdowns();
83
+ });
84
+ pluginContainer.find('.rc-filter-wrapper').append(backdrop);
85
+ document.body.style.overflow = 'hidden';
86
+ }
87
+ }
88
+
89
+ function closeAllDropdowns() {
90
+ $('.rc-filter-dropdown').addClass('d-none');
91
+ $('.rc-filter-btn').attr('aria-expanded', 'false');
92
+ $('.rc-backdrop').remove();
93
+ document.body.style.overflow = '';
94
+ }
95
+
96
+ function isLightColor(hex) {
97
+ if (!hex) return true;
98
+ hex = hex.replace('#', '');
99
+ if (hex.length === 3) {
100
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
101
+ }
102
+ const r = parseInt(hex.substr(0, 2), 16);
103
+ const g = parseInt(hex.substr(2, 2), 16);
104
+ const b = parseInt(hex.substr(4, 2), 16);
105
+ return (r * 299 + g * 587 + b * 114) / 1000 > 128;
106
+ }
107
+
108
+ function getStorageKey(categories) {
109
+ const sortedCids = categories.map(c => c.cid).sort((a, b) => a - b);
110
+ return 'rc-filter-' + sortedCids.join('-');
111
+ }
112
+
113
+ function loadFilter(storageKey, validCids) {
114
+ try {
115
+ const stored = JSON.parse(localStorage.getItem(storageKey));
116
+ if (!Array.isArray(stored)) return null;
117
+ const filtered = stored.filter(cid => validCids.includes(cid));
118
+ return filtered.length ? filtered : null;
119
+ } catch (e) {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ function saveFilter(storageKey, selectedCids, allCids) {
125
+ if (!selectedCids || selectedCids.length === 0 || selectedCids.length === allCids.length) {
126
+ localStorage.removeItem(storageKey);
127
+ } else {
128
+ localStorage.setItem(storageKey, JSON.stringify(selectedCids));
129
+ }
130
+ }
131
+
132
+ function buildDropdown(pluginContainer, categories, selectedCids) {
133
+ const inner = pluginContainer.find('.rc-filter-dropdown-inner');
134
+ inner.empty();
135
+
136
+ categories.forEach(function (cat) {
137
+ const isChecked = !selectedCids || selectedCids.includes(cat.cid);
138
+ const item = $('<label class="rc-filter-option d-flex align-items-center gap-2 px-3 py-2"></label>');
139
+ const checkbox = $('<input type="checkbox" class="form-check-input mt-0">')
140
+ .attr('data-cid', cat.cid)
141
+ .prop('checked', isChecked);
142
+
143
+ const nameSpan = $('<span class="badge"></span>')
144
+ .css({ backgroundColor: cat.bgColor || '#6c757d', color: cat.color || '#fff' })
145
+ .text(cat.name);
146
+
147
+ item.append(checkbox);
148
+ if (cat.icon) {
149
+ item.append($('<i></i>').addClass('fa').addClass(cat.icon).css('color', cat.bgColor || '#6c757d'));
150
+ }
151
+ item.append(nameSpan);
152
+ inner.append(item);
153
+ });
154
+ }
155
+
156
+ function updateChips(pluginContainer, selectedCids, categories) {
157
+ const chipsContainer = pluginContainer.find('.rc-filter-chips');
158
+ chipsContainer.empty();
159
+
160
+ if (!selectedCids || selectedCids.length === 0 || selectedCids.length === categories.length) {
161
+ return;
162
+ }
163
+
164
+ selectedCids.forEach(function (cid) {
165
+ const cat = categories.find(c => c.cid === cid);
166
+ if (!cat) return;
167
+ const chip = $('<span class="rc-filter-chip badge d-flex align-items-center gap-1"></span>')
168
+ .attr('data-cid', cid)
169
+ .css({ backgroundColor: cat.bgColor || '#6c757d', color: cat.color || '#fff' });
170
+ chip.append($('<span></span>').text(cat.name));
171
+ const closeBtn = $('<button type="button" class="btn-close rc-chip-remove" aria-label="Remove"></button>');
172
+ if (!isLightColor(cat.bgColor)) {
173
+ closeBtn.addClass('btn-close-white');
174
+ }
175
+ chip.append(closeBtn);
176
+ chipsContainer.append(chip);
177
+ });
178
+
179
+ chipsContainer.append(
180
+ $('<button type="button" class="btn btn-link btn-sm rc-clear-all text-decoration-none p-0 ms-1"></button>')
181
+ .text('Temizle')
182
+ );
183
+ }
184
+
185
+ function updateFilterButton(pluginContainer, selectedCids, categories) {
186
+ const btn = pluginContainer.find('.rc-filter-btn');
187
+ const isFiltered = selectedCids && selectedCids.length > 0 && selectedCids.length < categories.length;
188
+ btn.toggleClass('btn-outline-secondary', !isFiltered);
189
+ btn.toggleClass('btn-primary', isFiltered);
190
+ const label = btn.find('.rc-filter-label');
191
+ label.text(isFiltered ? 'Filtrele (' + selectedCids.length + ')' : 'Filtrele');
192
+ }
193
+
194
+ function applyFilter(pluginContainer, selectedCids, categories) {
195
+ const carouselContainer = pluginContainer.find('.recent-cards');
196
+ const isSlickInitialized = carouselContainer.hasClass('slick-initialized');
197
+ const isCarouselMode = config.recentCards && config.recentCards.enableCarousel;
198
+ const showAll = !selectedCids || selectedCids.length === 0 || selectedCids.length === categories.length;
199
+
200
+ // Step 1: Destroy slick if initialized
201
+ if (isSlickInitialized) {
202
+ try {
203
+ carouselContainer.slick('unslick');
204
+ } catch (e) {
205
+ // Slick was not properly initialized
206
+ }
207
+ }
208
+
209
+ // Step 2: Restore any previously detached cards back into the container
210
+ const stash = pluginContainer.data('rc-stashed-cards');
211
+ if (stash && stash.length) {
212
+ carouselContainer.append(stash);
213
+ pluginContainer.removeData('rc-stashed-cards');
214
+ }
215
+
216
+ // Step 3: Detach cards that don't match the filter (not just hide — Slick counts hidden children)
217
+ const toStashElements = [];
218
+ let visibleCount = 0;
219
+ carouselContainer.find('.recent-card-container').each(function () {
220
+ const cardCid = parseInt($(this).attr('data-cid'), 10);
221
+ if (showAll || selectedCids.includes(cardCid)) {
222
+ visibleCount++;
223
+ } else {
224
+ toStashElements.push(this);
225
+ }
226
+ });
227
+
228
+ if (toStashElements.length) {
229
+ const toStash = $(toStashElements).detach();
230
+ pluginContainer.data('rc-stashed-cards', toStash);
231
+ }
232
+
233
+ // Step 4: Handle empty state
234
+ const emptyState = pluginContainer.find('.rc-empty-state');
235
+ if (visibleCount === 0) {
236
+ emptyState.removeClass('d-none');
237
+ carouselContainer.addClass('d-none');
238
+ } else {
239
+ emptyState.addClass('d-none');
240
+ carouselContainer.removeClass('d-none');
241
+ }
242
+
243
+ // Step 5: Reinitialize slick with only the cards still in the DOM
244
+ if (isCarouselMode && visibleCount > 0) {
245
+ carouselContainer.addClass('overflow-hidden invisible');
246
+ initSlick(carouselContainer);
247
+ } else if (!isCarouselMode) {
248
+ carouselContainer.removeClass('carousel-mode invisible');
249
+ }
250
+
251
+ // Step 6: Update UI
252
+ updateChips(pluginContainer, selectedCids, categories);
253
+ updateFilterButton(pluginContainer, selectedCids, categories);
254
+ }
255
+
256
+ function parseCategories(pluginContainer) {
257
+ const scriptTag = pluginContainer.find('script.rc-categories-data');
258
+ if (!scriptTag.length) return [];
259
+ try {
260
+ const parsed = JSON.parse(scriptTag.text());
261
+ return Array.isArray(parsed) ? parsed : [];
262
+ } catch (e) {
263
+ return [];
264
+ }
265
+ }
266
+
267
+ function initFilter(pluginContainer) {
268
+ const categories = parseCategories(pluginContainer);
269
+ const filterBar = pluginContainer.find('.rc-filter-bar');
270
+
271
+ // Hide filter if fewer than 2 categories
272
+ if (categories.length < 2) {
273
+ filterBar.addClass('d-none');
274
+ return false;
275
+ }
276
+
277
+ const allCids = categories.map(c => c.cid);
278
+ const storageKey = getStorageKey(categories);
279
+ const savedFilter = loadFilter(storageKey, allCids);
280
+
281
+ buildDropdown(pluginContainer, categories, savedFilter);
282
+
283
+ // Bind events (use .off first to prevent duplicates from ajaxify)
284
+ pluginContainer.off('.rcFilter');
285
+
286
+ // Toggle dropdown
287
+ pluginContainer.on('click.rcFilter', '.rc-filter-btn', function (e) {
288
+ e.stopPropagation();
289
+ const isOpen = !pluginContainer.find('.rc-filter-dropdown').hasClass('d-none');
290
+ closeAllDropdowns();
291
+ if (!isOpen) {
292
+ openDropdown(pluginContainer);
293
+ }
294
+ });
295
+
296
+ // Checkbox change
297
+ pluginContainer.on('change.rcFilter', '.rc-filter-option input[type="checkbox"]', function () {
298
+ const selected = [];
299
+ pluginContainer.find('.rc-filter-option input:checked').each(function () {
300
+ selected.push(parseInt($(this).attr('data-cid'), 10));
301
+ });
302
+ saveFilter(storageKey, selected, allCids);
303
+ applyFilter(pluginContainer, selected, categories);
304
+ });
305
+
306
+ // Chip remove
307
+ pluginContainer.on('click.rcFilter', '.rc-chip-remove', function (e) {
308
+ e.stopPropagation();
309
+ const chip = $(this).closest('.rc-filter-chip');
310
+ const cidToRemove = parseInt(chip.attr('data-cid'), 10);
311
+ pluginContainer.find('.rc-filter-option input[data-cid="' + cidToRemove + '"]')
312
+ .prop('checked', false).trigger('change');
313
+ });
314
+
315
+ // Clear all
316
+ pluginContainer.on('click.rcFilter', '.rc-clear-all', function (e) {
317
+ e.stopPropagation();
318
+ pluginContainer.find('.rc-filter-option input').prop('checked', true);
319
+ saveFilter(storageKey, allCids, allCids);
320
+ applyFilter(pluginContainer, null, categories);
321
+ });
322
+
323
+ // Apply saved filter if exists
324
+ if (savedFilter) {
325
+ applyFilter(pluginContainer, savedFilter, categories);
326
+ return true; // filter was applied (slick handled inside applyFilter)
327
+ }
328
+
329
+ return false; // no filter applied, caller should init slick
330
+ }
331
+
332
+ function setupWidget(pluginContainer) {
333
+ const filterApplied = initFilter(pluginContainer);
334
+ if (!filterApplied) {
335
+ initSlick(pluginContainer.find('.recent-cards'));
336
+ }
337
+ }
338
+
339
+ // Close dropdown on outside click & Escape
340
+ function bindGlobalHandlers() {
341
+ $(document).off('.rcFilter');
342
+
343
+ $(document).on('click.rcFilter', function (e) {
344
+ if (!$(e.target).closest('.rc-filter-wrapper').length) {
345
+ closeAllDropdowns();
346
+ }
347
+ });
348
+
349
+ $(document).on('keydown.rcFilter', function (e) {
350
+ if (e.key === 'Escape') {
351
+ closeAllDropdowns();
352
+ }
353
+ });
354
+ }
355
+
356
+ // Initialize
357
+ bindGlobalHandlers();
358
+ $('.recent-cards-plugin').each(function () {
359
+ setupWidget($(this));
360
+ });
361
+
362
+ $(window).on('action:ajaxify.contentLoaded', function () {
363
+ bindGlobalHandlers();
364
+ $('.recent-cards-plugin').each(function () {
365
+ setupWidget($(this));
366
+ });
367
+ });
368
+ });
@@ -0,0 +1,113 @@
1
+
2
+ /* modified from https://gist.github.com/vivekkumawat/c8351e28412a194aac977bf996c4a6d8 */
3
+
4
+ /* CSS For Slick Slider */
5
+ /* Note: Don't use slick-theme.css file */
6
+
7
+ /* Adding margin between slides */
8
+ .slick-slide {
9
+ margin: 0 8px;
10
+ }
11
+ .slick-list {
12
+ margin: 0;
13
+ }
14
+
15
+ /* Arrows */
16
+ .slick-prev.slick-disabled,
17
+ .slick-next.slick-disabled
18
+ {
19
+ opacity: .25;
20
+ }
21
+
22
+ /* rtlcss converts these to right/left:0px on RTL */
23
+ .slick-prev { left: 0px; }
24
+ .slick-next { right: 0px; }
25
+
26
+ /* Dots */
27
+ .slick-dotted.slick-slider
28
+ {
29
+ margin-bottom: 30px;
30
+ }
31
+
32
+ .slick-dots
33
+ {
34
+ position: absolute;
35
+ bottom: -25px;
36
+
37
+ display: block;
38
+
39
+ width: 100%;
40
+ padding: 0;
41
+ margin: 0;
42
+
43
+ list-style: none;
44
+
45
+ text-align: center;
46
+ }
47
+ .slick-dots li
48
+ {
49
+ position: relative;
50
+
51
+ display: inline-block;
52
+
53
+ width: 20px;
54
+ height: 20px;
55
+ margin: 0 5px;
56
+ padding: 0;
57
+
58
+ cursor: pointer;
59
+ }
60
+ .slick-dots li button
61
+ {
62
+ font-size: 0;
63
+ line-height: 0;
64
+
65
+ display: block;
66
+
67
+ width: 20px;
68
+ height: 20px;
69
+ padding: 5px;
70
+
71
+ cursor: pointer;
72
+
73
+ color: transparent;
74
+ border: 0;
75
+ outline: none;
76
+ background: transparent;
77
+ }
78
+ .slick-dots li button:hover,
79
+ .slick-dots li button:focus
80
+ {
81
+ outline: none;
82
+ }
83
+ .slick-dots li button:hover:before,
84
+ .slick-dots li button:focus:before
85
+ {
86
+ opacity: 1;
87
+ }
88
+ .slick-dots li button:before
89
+ {
90
+ font-size: 8px;
91
+ line-height: 20px;
92
+
93
+ position: absolute;
94
+ top: 0;
95
+ left: 0;
96
+
97
+ width: 20px;
98
+ height: 20px;
99
+
100
+ content: '•';
101
+ text-align: center;
102
+
103
+ opacity: .25;
104
+ color: black;
105
+
106
+ -webkit-font-smoothing: antialiased;
107
+ -moz-osx-font-smoothing: grayscale;
108
+ }
109
+ .slick-dots li.slick-active button:before
110
+ {
111
+ opacity: .75;
112
+ color: black;
113
+ }
@@ -0,0 +1,118 @@
1
+ /* Slider */
2
+ .slick-slider
3
+ {
4
+ position: relative;
5
+
6
+ display: block;
7
+ box-sizing: border-box;
8
+
9
+ -webkit-user-select: none;
10
+ -moz-user-select: none;
11
+ -ms-user-select: none;
12
+ user-select: none;
13
+
14
+ -webkit-touch-callout: none;
15
+ -khtml-user-select: none;
16
+ -ms-touch-action: pan-y;
17
+ touch-action: pan-y;
18
+ -webkit-tap-highlight-color: transparent;
19
+ }
20
+
21
+ .slick-list
22
+ {
23
+ position: relative;
24
+
25
+ display: block;
26
+ overflow: hidden;
27
+
28
+ margin: 0;
29
+ padding: 0;
30
+ }
31
+ .slick-list:focus
32
+ {
33
+ outline: none;
34
+ }
35
+ .slick-list.dragging
36
+ {
37
+ cursor: pointer;
38
+ cursor: hand;
39
+ }
40
+
41
+ .slick-slider .slick-track,
42
+ .slick-slider .slick-list
43
+ {
44
+ -webkit-transform: translate3d(0, 0, 0);
45
+ -moz-transform: translate3d(0, 0, 0);
46
+ -ms-transform: translate3d(0, 0, 0);
47
+ -o-transform: translate3d(0, 0, 0);
48
+ transform: translate3d(0, 0, 0);
49
+ }
50
+
51
+ .slick-track
52
+ {
53
+ position: relative;
54
+ top: 0;
55
+ left: 0;
56
+
57
+ display: block;
58
+ margin-left: auto;
59
+ margin-right: auto;
60
+ }
61
+ .slick-track:before,
62
+ .slick-track:after
63
+ {
64
+ display: table;
65
+
66
+ content: '';
67
+ }
68
+ .slick-track:after
69
+ {
70
+ clear: both;
71
+ }
72
+ .slick-loading .slick-track
73
+ {
74
+ visibility: hidden;
75
+ }
76
+
77
+ .slick-slide
78
+ {
79
+ display: none;
80
+ float: left;
81
+
82
+ height: 100%;
83
+ min-height: 1px;
84
+ }
85
+ [dir='rtl'] .slick-slide
86
+ {
87
+ float: right;
88
+ }
89
+ .slick-slide.slick-loading img
90
+ {
91
+ display: none;
92
+ }
93
+ .slick-slide .topic-title img {
94
+ display: inline-block
95
+ }
96
+ .slick-slide.dragging img
97
+ {
98
+ pointer-events: none;
99
+ }
100
+ .slick-initialized .slick-slide
101
+ {
102
+ display: block;
103
+ }
104
+ .slick-loading .slick-slide
105
+ {
106
+ visibility: hidden;
107
+ }
108
+ .slick-vertical .slick-slide
109
+ {
110
+ display: block;
111
+
112
+ height: auto;
113
+
114
+ border: 1px solid transparent;
115
+ }
116
+ .slick-arrow.slick-hidden {
117
+ display: none;
118
+ }