@okjavis/nodebb-theme-javis 2.0.0 → 2.2.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.
@@ -6,14 +6,186 @@
6
6
  (function() {
7
7
  'use strict';
8
8
 
9
+ var carouselCounter = 0;
10
+
9
11
  // Theme initialization
10
12
  $(document).ready(function() {
11
13
  console.log('JAVIS Community Theme initialized');
12
14
 
13
15
  // Ensure sidebar toggle works (reinitialize if Harmony's handler isn't loaded)
14
16
  initSidebarToggle();
17
+
18
+ // Initialize image carousels for posts with multiple images
19
+ initPostImageCarousels();
20
+
21
+ // Fix bookmark alert - only show when bookmark is meaningfully ahead
22
+ fixBookmarkAlert();
23
+
24
+ // Re-initialize carousels when new posts are loaded (infinite scroll, etc.)
25
+ // Also handle post edits by clearing the processed flag
26
+ $(window).on('action:posts.loaded action:topic.loaded action:ajaxify.end', function() {
27
+ initPostImageCarousels();
28
+ });
29
+
30
+ // Handle post edits - need to re-process the edited post
31
+ $(window).on('action:posts.edited', function(ev, data) {
32
+ if (data && data.post && data.post.pid) {
33
+ // Find the edited post and remove the processed flag so it gets re-scanned
34
+ var $post = $('[data-pid="' + data.post.pid + '"]');
35
+ var $content = $post.find('[component="post/content"]');
36
+ $content.removeAttr('data-carousel-processed');
37
+ // Remove any existing carousel in this post
38
+ $content.find('.post-image-carousel').remove();
39
+ // Re-initialize
40
+ initPostImageCarousels();
41
+ }
42
+ });
15
43
  });
16
44
 
45
+ /**
46
+ * Convert multiple images in post content to Bootstrap carousels
47
+ */
48
+ function initPostImageCarousels() {
49
+ // Find all post content areas that haven't been processed
50
+ $('[component="post/content"]:not([data-carousel-processed])').each(function() {
51
+ var $content = $(this);
52
+ $content.attr('data-carousel-processed', 'true');
53
+
54
+ // Find all images in post content - including those in links (lightbox) and paragraphs
55
+ var $images = $content.find('img').filter(function() {
56
+ var $img = $(this);
57
+
58
+ // Exclude images already in a carousel
59
+ if ($img.closest('.carousel, .post-image-carousel').length) {
60
+ return false;
61
+ }
62
+
63
+ // Exclude emojis and small icons by class
64
+ if ($img.hasClass('emoji') || $img.hasClass('emoji-img') || $img.hasClass('icon') || $img.hasClass('not-responsive')) {
65
+ return false;
66
+ }
67
+
68
+ // Check the image source to determine if it's a content image
69
+ var src = $img.attr('src') || '';
70
+
71
+ // Include images from uploads folder or with image extensions
72
+ var isContentImage = src.indexOf('/assets/uploads/') !== -1 ||
73
+ src.indexOf('/files/') !== -1 ||
74
+ src.match(/\.(jpg|jpeg|png|gif|webp)(\?|$)/i);
75
+
76
+ if (!isContentImage) {
77
+ return false;
78
+ }
79
+
80
+ // Exclude tiny images by checking width/height attributes if present
81
+ var width = $img.attr('width');
82
+ var height = $img.attr('height');
83
+ if ((width && parseInt(width, 10) < 50) || (height && parseInt(height, 10) < 50)) {
84
+ return false;
85
+ }
86
+
87
+ return true;
88
+ });
89
+
90
+ // Only create carousel if 2+ images
91
+ if ($images.length >= 2) {
92
+ createCarousel($content, $images);
93
+ console.log('JAVIS: Found ' + $images.length + ' images, creating carousel');
94
+ } else if ($images.length > 0) {
95
+ console.log('JAVIS: Found ' + $images.length + ' image(s), not enough for carousel');
96
+ }
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Create a Bootstrap 5 carousel from a set of images
102
+ */
103
+ function createCarousel($content, $images) {
104
+ var carouselId = 'post-carousel-' + (++carouselCounter);
105
+
106
+ // Build carousel HTML
107
+ var carouselHtml = '<div id="' + carouselId + '" class="carousel slide post-image-carousel" data-bs-ride="false">';
108
+
109
+ // Indicators (dots)
110
+ carouselHtml += '<div class="carousel-indicators">';
111
+ $images.each(function(index) {
112
+ var activeClass = index === 0 ? 'active' : '';
113
+ var ariaCurrent = index === 0 ? 'aria-current="true"' : '';
114
+ carouselHtml += '<button type="button" data-bs-target="#' + carouselId + '" data-bs-slide-to="' + index + '" class="' + activeClass + '" ' + ariaCurrent + ' aria-label="Slide ' + (index + 1) + '"></button>';
115
+ });
116
+ carouselHtml += '</div>';
117
+
118
+ // Carousel inner (slides)
119
+ carouselHtml += '<div class="carousel-inner">';
120
+ $images.each(function(index) {
121
+ var $img = $(this);
122
+ var src = $img.attr('src');
123
+ var alt = $img.attr('alt') || 'Image ' + (index + 1);
124
+ var activeClass = index === 0 ? 'active' : '';
125
+ carouselHtml += '<div class="carousel-item ' + activeClass + '">';
126
+ carouselHtml += '<img src="' + src + '" class="d-block w-100" alt="' + alt + '" loading="lazy">';
127
+ carouselHtml += '</div>';
128
+ });
129
+ carouselHtml += '</div>';
130
+
131
+ // Navigation arrows
132
+ carouselHtml += '<button class="carousel-control-prev" type="button" data-bs-target="#' + carouselId + '" data-bs-slide="prev">';
133
+ carouselHtml += '<span class="carousel-control-prev-icon" aria-hidden="true"></span>';
134
+ carouselHtml += '<span class="visually-hidden">Previous</span>';
135
+ carouselHtml += '</button>';
136
+ carouselHtml += '<button class="carousel-control-next" type="button" data-bs-target="#' + carouselId + '" data-bs-slide="next">';
137
+ carouselHtml += '<span class="carousel-control-next-icon" aria-hidden="true"></span>';
138
+ carouselHtml += '<span class="visually-hidden">Next</span>';
139
+ carouselHtml += '</button>';
140
+
141
+ carouselHtml += '</div>';
142
+
143
+ // Find the topmost element to insert carousel before
144
+ // Images can be in: <p><img></p>, <p><a><img></a></p>, <a><img></a>, or just <img>
145
+ var $firstImg = $images.first();
146
+ var $insertBefore = $firstImg;
147
+
148
+ // Walk up to find the direct child of $content
149
+ while ($insertBefore.parent().length && !$insertBefore.parent().is($content)) {
150
+ $insertBefore = $insertBefore.parent();
151
+ }
152
+
153
+ // Insert carousel before the first image's container
154
+ $insertBefore.before(carouselHtml);
155
+
156
+ // Collect elements to remove (images and their empty containers)
157
+ var elementsToRemove = [];
158
+ $images.each(function() {
159
+ var $img = $(this);
160
+ var $element = $img;
161
+
162
+ // Walk up to find the direct child of $content
163
+ while ($element.parent().length && !$element.parent().is($content)) {
164
+ $element = $element.parent();
165
+ }
166
+
167
+ // Mark for removal if not already marked
168
+ if (elementsToRemove.indexOf($element[0]) === -1) {
169
+ elementsToRemove.push($element[0]);
170
+ }
171
+ });
172
+
173
+ // Remove the original image containers
174
+ $(elementsToRemove).remove();
175
+
176
+ // Initialize Bootstrap carousel
177
+ var carouselEl = document.getElementById(carouselId);
178
+ if (carouselEl && typeof bootstrap !== 'undefined') {
179
+ new bootstrap.Carousel(carouselEl, {
180
+ interval: false, // Don't auto-slide
181
+ touch: true,
182
+ wrap: true
183
+ });
184
+ }
185
+
186
+ console.log('JAVIS: Created carousel with ' + $images.length + ' images');
187
+ }
188
+
17
189
  function initSidebarToggle() {
18
190
  // Check if the toggle element exists
19
191
  var toggleEl = $('[component="sidebar/toggle"]');
@@ -51,4 +223,47 @@
51
223
  console.log('JAVIS: Sidebar toggle initialized');
52
224
  }
53
225
 
226
+ /**
227
+ * Fix the bookmark alert bug - NodeBB shows "Click here to return to last read post"
228
+ * even when the bookmark position isn't meaningfully ahead of the current position.
229
+ *
230
+ * The bug: NodeBB checks if bookmark exists and postcount > threshold, but doesn't
231
+ * check if the bookmark is actually ahead of where the user currently is.
232
+ *
233
+ * This fix removes the bookmark alert if:
234
+ * 1. The bookmark is at position 1 or 2 (meaningless to "return" to the start)
235
+ * 2. The bookmark is at or behind the current post index
236
+ */
237
+ function fixBookmarkAlert() {
238
+ $(window).on('action:topic.loaded', function() {
239
+ // Small delay to let NodeBB's handleBookmark run first
240
+ setTimeout(function() {
241
+ if (typeof ajaxify === 'undefined' || !ajaxify.data || !ajaxify.data.template || !ajaxify.data.template.topic) {
242
+ return;
243
+ }
244
+
245
+ require(['storage', 'alerts'], function(storage, alerts) {
246
+ var tid = ajaxify.data.tid;
247
+ var bookmark = ajaxify.data.bookmark || storage.getItem('topic:' + tid + ':bookmark');
248
+ var postIndex = ajaxify.data.postIndex || 1;
249
+ var bookmarkInt = parseInt(bookmark, 10) || 0;
250
+ var postIndexInt = parseInt(postIndex, 10) || 1;
251
+
252
+ // Remove bookmark alert if:
253
+ // 1. No meaningful bookmark (position 1 or 2 - essentially the start)
254
+ // 2. Bookmark is at or behind current position (nothing to "return" to)
255
+ // 3. Bookmark is only 1-2 posts ahead (not worth showing notification)
256
+ var shouldRemoveAlert = bookmarkInt <= 2 ||
257
+ bookmarkInt <= postIndexInt ||
258
+ (bookmarkInt - postIndexInt) <= 2;
259
+
260
+ if (shouldRemoveAlert) {
261
+ alerts.remove('bookmark');
262
+ console.log('JAVIS: Removed unnecessary bookmark alert (bookmark: ' + bookmarkInt + ', current: ' + postIndexInt + ')');
263
+ }
264
+ });
265
+ }, 100);
266
+ });
267
+ }
268
+
54
269
  })();
@@ -0,0 +1,48 @@
1
+ <div id="topic-sidebar" class="topic-sidebar">
2
+ <div class="d-flex flex-column gap-2">
3
+ <div class="topic-sidebar-actions d-flex flex-column gap-2 mb-3">
4
+ <!-- Reply Button -->
5
+ <!-- IMPORT partials/topic/reply-button.tpl -->
6
+
7
+ <!-- Mark Unread Button -->
8
+ {{{ if loggedIn }}}
9
+ <button component="topic/mark-unread" class="btn btn-ghost btn-sm d-flex gap-2 align-items-center w-100 justify-content-start">
10
+ <i class="fa fa-fw fa-inbox"></i>
11
+ <span class="fw-semibold text-nowrap">[[topic:mark-unread]]</span>
12
+ </button>
13
+ {{{ end }}}
14
+
15
+ <!-- Watching/Not Watching -->
16
+ <!-- IMPORT partials/topic/watch.tpl -->
17
+
18
+ <!-- Sort Options -->
19
+ <!-- IMPORT partials/topic/sort.tpl -->
20
+
21
+ <!-- Topic Tools (for mods/admins) -->
22
+ <!-- IMPORT partials/topic/tools.tpl -->
23
+ </div>
24
+
25
+ <!-- Post Navigator / Timeline -->
26
+ <div class="pagination-block flex-grow-1">
27
+ <div class="scroller-content d-flex gap-2 flex-column align-items-start">
28
+ <button class="btn btn-ghost btn-sm d-flex gap-2 align-items-center pagetop w-100 justify-content-start">
29
+ <i class="fa fa-fw fa-angle-up"></i>
30
+ <span class="timeago text-xs text-muted text-nowrap" title="{./timestampISO}"></span>
31
+ </button>
32
+ <div class="scroller-container position-relative w-100">
33
+ <div class="scroller-thumb d-flex gap-2 text-nowrap position-relative" style="height: 40px;">
34
+ <div class="scroller-thumb-icon rounded d-inline-block" style="width: 4px; height: 40px;"></div>
35
+ <div class="d-flex flex-column">
36
+ <span class="thumb-text small fw-semibold mb-0"></span>
37
+ <span class="thumb-timestamp timeago text-xs text-muted mb-0"></span>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ <button class="btn btn-ghost btn-sm d-flex gap-2 align-items-center pagebottom w-100 justify-content-start">
42
+ <i class="fa fa-fw fa-angle-down"></i>
43
+ <span class="timeago text-xs text-muted text-nowrap" title="{./lastposttimeISO}"></span>
44
+ </button>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
@@ -0,0 +1,133 @@
1
+ <!-- IMPORT partials/breadcrumbs-json-ld.tpl -->
2
+ {{{ if config.theme.enableBreadcrumbs }}}
3
+ <!-- IMPORT partials/breadcrumbs.tpl -->
4
+ {{{ end }}}
5
+ {{{ if widgets.header.length }}}
6
+ <div data-widget-area="header">
7
+ {{{each widgets.header}}}
8
+ {{widgets.header.html}}
9
+ {{{end}}}
10
+ </div>
11
+ {{{ end }}}
12
+
13
+ <div itemid="{url}" itemscope itemtype="https://schema.org/DiscussionForumPosting">
14
+ <meta itemprop="headline" content="{escape(titleRaw)}">
15
+ <meta itemprop="text" content="{escape(titleRaw)}">
16
+ <meta itemprop="url" content="{url}">
17
+ <meta itemprop="datePublished" content="{timestampISO}">
18
+ <meta itemprop="dateModified" content="{lastposttimeISO}">
19
+ <div itemprop="author" itemscope itemtype="https://schema.org/Person">
20
+ <meta itemprop="name" content="{author.username}">
21
+ {{{ if author.userslug }}}<meta itemprop="url" content="{config.relative_path}/user/{author.userslug}">{{{ end }}}
22
+ </div>
23
+
24
+ <div class="d-flex flex-column gap-3">
25
+ <div class="d-flex flex-wrap">
26
+ <div class="d-flex flex-column gap-3 flex-grow-1">
27
+ <div class="topic-header d-flex align-items-start gap-3">
28
+ <a href="#" onclick="history.back(); return false;" class="topic-back-btn" title="Go back">
29
+ <i class="fa fa-arrow-left"></i>
30
+ </a>
31
+ <h1 component="post/header" class="tracking-tight fw-semibold fs-3 mb-0 text-break {{{ if config.theme.centerHeaderElements }}}text-center{{{ end }}}">
32
+ <span class="topic-title" component="topic/title">{title}</span>
33
+ </h1>
34
+ </div>
35
+
36
+ <div class="topic-info d-flex gap-2 align-items-center flex-wrap {{{ if config.theme.centerHeaderElements }}}justify-content-center{{{ end }}}">
37
+ <span component="topic/labels" class="d-flex gap-2 {{{ if (!scheduled && (!pinned && (!locked && (!oldCid && !icons.length)))) }}}hidden{{{ end }}}">
38
+ <span component="topic/scheduled" class="badge badge border border-gray-300 text-body {{{ if !scheduled }}}hidden{{{ end }}}">
39
+ <i class="fa fa-clock-o"></i> [[topic:scheduled]]
40
+ </span>
41
+ <span component="topic/pinned" class="badge badge border border-gray-300 text-body {{{ if (scheduled || !pinned) }}}hidden{{{ end }}}">
42
+ <i class="fa fa-thumb-tack"></i> {{{ if !pinExpiry }}}[[topic:pinned]]{{{ else }}}[[topic:pinned-with-expiry, {isoTimeToLocaleString(./pinExpiryISO, config.userLang)}]]{{{ end }}}
43
+ </span>
44
+ <span component="topic/locked" class="badge badge border border-gray-300 text-body {{{ if !locked }}}hidden{{{ end }}}">
45
+ <i class="fa fa-lock"></i> [[topic:locked]]
46
+ </span>
47
+ <a component="topic/moved" href="{config.relative_path}/category/{oldCid}" class="badge badge border border-gray-300 text-body text-decoration-none {{{ if !oldCid }}}hidden{{{ end }}}">
48
+ <i class="fa fa-arrow-circle-right"></i> {{{ if privileges.isAdminOrMod }}}[[topic:moved-from, {oldCategory.name}]]{{{ else }}}[[topic:moved]]{{{ end }}}
49
+ </a>
50
+ {{{each icons}}}<span class="lh-1">{@value}</span>{{{end}}}
51
+ </span>
52
+ {function.buildCategoryLabel, category, "a", "border"}
53
+ <div data-tid="{./tid}" component="topic/tags" class="lh-1 tags tag-list d-flex flex-wrap hidden-xs hidden-empty gap-2"><!-- IMPORT partials/topic/tags.tpl --></div>
54
+ <div class="d-flex hidden-xs gap-2"><!-- IMPORT partials/topic/stats.tpl --></div>
55
+ </div>
56
+ </div>
57
+ <div class="d-flex gap-2 align-items-center mt-2 hidden-empty" component="topic/thumb/list"><!-- IMPORT partials/topic/thumbs.tpl --></div>
58
+ </div>
59
+
60
+ <div class="topic-layout d-flex gap-4 mb-4 mb-lg-0">
61
+ <!-- Main Content Area -->
62
+ <div class="topic flex-grow-1" style="min-width: 0;">
63
+ {{{ if merger }}}
64
+ <!-- IMPORT partials/topic/merged-message.tpl -->
65
+ {{{ end }}}
66
+ {{{ if forker }}}
67
+ <!-- IMPORT partials/topic/forked-message.tpl -->
68
+ {{{ end }}}
69
+ {{{ if !scheduled }}}
70
+ <!-- IMPORT partials/topic/deleted-message.tpl -->
71
+ {{{ end }}}
72
+
73
+ <div class="posts-container" style="min-width: 0;">
74
+ <ul component="topic" class="posts timeline list-unstyled mt-sm-2 p-0 py-3" style="min-width: 0;" data-tid="{tid}" data-cid="{cid}">
75
+ {{{ each posts }}}
76
+ <li component="post" class="pt-4 {{{ if posts.deleted }}}deleted{{{ end }}} {{{ if posts.selfPost }}}self-post{{{ end }}} {{{ if posts.topicOwnerPost }}}topic-owner-post{{{ end }}}" <!-- IMPORT partials/data/topic.tpl -->>
77
+ <a component="post/anchor" data-index="{./index}" id="{increment(./index, "1")}"></a>
78
+ <meta itemprop="datePublished" content="{./timestampISO}">
79
+ {{{ if ./editedISO }}}
80
+ <meta itemprop="dateModified" content="{./editedISO}">
81
+ {{{ end }}}
82
+
83
+ <!-- IMPORT partials/topic/post.tpl -->
84
+ </li>
85
+ {{{ if (config.topicPostSort != "most_votes") }}}
86
+ {{{ each ./events}}}<!-- IMPORT partials/topic/event.tpl -->{{{ end }}}
87
+ {{{ end }}}
88
+ {{{ end }}}
89
+ </ul>
90
+ {{{ if browsingUsers }}}
91
+ <div class="visible-xs">
92
+ <!-- IMPORT partials/topic/browsing-users.tpl -->
93
+ <hr/>
94
+ </div>
95
+ {{{ end }}}
96
+ {{{ if config.theme.enableQuickReply }}}
97
+ <!-- IMPORT partials/topic/quickreply.tpl -->
98
+ {{{ end }}}
99
+ </div>
100
+
101
+ {{{ if config.usePagination }}}
102
+ <!-- IMPORT partials/paginator.tpl -->
103
+ {{{ end }}}
104
+ </div>
105
+
106
+ <!-- Right Sidebar with Topic Actions -->
107
+ <div class="topic-sidebar-container d-none d-lg-block" style="width: 200px; flex-shrink: 0;">
108
+ <!-- IMPORT partials/topic/sidebar.tpl -->
109
+
110
+ <!-- Additional Widgets (if any) -->
111
+ {{{ if widgets.sidebar.length }}}
112
+ <div data-widget-area="sidebar" class="mt-4">
113
+ {{{each widgets.sidebar}}}
114
+ {{widgets.sidebar.html}}
115
+ {{{end}}}
116
+ </div>
117
+ {{{ end }}}
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+
123
+ <div data-widget-area="footer">
124
+ {{{each widgets.footer}}}
125
+ {{widgets.footer.html}}
126
+ {{{end}}}
127
+ </div>
128
+
129
+ {{{ if !config.usePagination }}}
130
+ <noscript>
131
+ <!-- IMPORT partials/paginator.tpl -->
132
+ </noscript>
133
+ {{{ end }}}