@okjavis/nodebb-theme-javis 2.2.0 → 2.4.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.
- package/package.json +1 -1
- package/scss/_composer.scss +716 -0
- package/scss/_feed.scss +278 -22
- package/scss/_sidebar.scss +1 -1
- package/scss/_topic.scss +202 -156
- package/static/lib/theme.js +146 -0
- package/templates/feed.tpl +163 -0
- package/templates/partials/topic/post-parent.tpl +9 -0
- package/theme.scss +1 -0
package/static/lib/theme.js
CHANGED
|
@@ -21,10 +21,22 @@
|
|
|
21
21
|
// Fix bookmark alert - only show when bookmark is meaningfully ahead
|
|
22
22
|
fixBookmarkAlert();
|
|
23
23
|
|
|
24
|
+
// Initialize parent post click navigation with smooth scroll
|
|
25
|
+
initParentPostNavigation();
|
|
26
|
+
|
|
27
|
+
// Initialize post hover actions (show actions only on directly hovered post)
|
|
28
|
+
initPostHoverActions();
|
|
29
|
+
|
|
30
|
+
// Initialize click handler for composer prompt card (rendered server-side in feed.tpl)
|
|
31
|
+
initFeedComposerPromptHandler();
|
|
32
|
+
|
|
24
33
|
// Re-initialize carousels when new posts are loaded (infinite scroll, etc.)
|
|
25
34
|
// Also handle post edits by clearing the processed flag
|
|
26
35
|
$(window).on('action:posts.loaded action:topic.loaded action:ajaxify.end', function() {
|
|
27
36
|
initPostImageCarousels();
|
|
37
|
+
initParentPostNavigation();
|
|
38
|
+
initPostHoverActions();
|
|
39
|
+
initFeedComposerPromptHandler();
|
|
28
40
|
});
|
|
29
41
|
|
|
30
42
|
// Handle post edits - need to re-process the edited post
|
|
@@ -266,4 +278,138 @@
|
|
|
266
278
|
});
|
|
267
279
|
}
|
|
268
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Initialize parent post click navigation with smooth scroll
|
|
283
|
+
* Clicking the "Replying to" component scrolls to the parent post
|
|
284
|
+
*/
|
|
285
|
+
function initParentPostNavigation() {
|
|
286
|
+
// Use event delegation on the topic container
|
|
287
|
+
$('[component="topic"]').off('click.javis-parent').on('click.javis-parent', '[component="post/parent"]', function(e) {
|
|
288
|
+
// Don't navigate if clicking on a link (username, etc.)
|
|
289
|
+
if ($(e.target).closest('a').length) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
e.preventDefault();
|
|
294
|
+
e.stopPropagation();
|
|
295
|
+
|
|
296
|
+
var parentPid = $(this).attr('data-parent-pid');
|
|
297
|
+
if (!parentPid) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Find the parent post element
|
|
302
|
+
var $parentPost = $('[component="topic"] > [component="post"][data-pid="' + parentPid + '"]');
|
|
303
|
+
|
|
304
|
+
if ($parentPost.length) {
|
|
305
|
+
// Post is on the current page - smooth scroll to it
|
|
306
|
+
smoothScrollToPost($parentPost);
|
|
307
|
+
} else {
|
|
308
|
+
// Post is on a different page - navigate via URL
|
|
309
|
+
window.location.href = config.relative_path + '/post/' + parentPid;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Smooth scroll to a post element with highlight effect
|
|
316
|
+
*/
|
|
317
|
+
function smoothScrollToPost($postElement) {
|
|
318
|
+
if (!$postElement.length) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Calculate scroll position (with offset for header)
|
|
323
|
+
var headerHeight = $('header').outerHeight() || 60;
|
|
324
|
+
var scrollTop = $postElement.offset().top - headerHeight - 20;
|
|
325
|
+
|
|
326
|
+
// Smooth scroll
|
|
327
|
+
$('html, body').animate({
|
|
328
|
+
scrollTop: scrollTop
|
|
329
|
+
}, 400, 'swing', function() {
|
|
330
|
+
// Add highlight effect after scroll completes
|
|
331
|
+
highlightPost($postElement);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Briefly highlight a post to draw attention
|
|
337
|
+
*/
|
|
338
|
+
function highlightPost($postElement) {
|
|
339
|
+
var $container = $postElement.find('.post-container-parent');
|
|
340
|
+
if (!$container.length) {
|
|
341
|
+
$container = $postElement;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Add highlight class
|
|
345
|
+
$container.addClass('post-highlight-flash');
|
|
346
|
+
|
|
347
|
+
// Remove after animation
|
|
348
|
+
setTimeout(function() {
|
|
349
|
+
$container.removeClass('post-highlight-flash');
|
|
350
|
+
}, 1500);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Initialize post hover actions
|
|
355
|
+
* Adds 'post-hovered' class only to the directly hovered post container
|
|
356
|
+
* This prevents CSS hover from bubbling up to parent posts
|
|
357
|
+
*/
|
|
358
|
+
function initPostHoverActions() {
|
|
359
|
+
// Use event delegation for all post containers
|
|
360
|
+
$(document).off('mouseenter.javis-hover mouseleave.javis-hover', '.post-container-parent');
|
|
361
|
+
|
|
362
|
+
$(document).on('mouseenter.javis-hover', '.post-container-parent', function(e) {
|
|
363
|
+
// Stop propagation to prevent parent containers from getting the event
|
|
364
|
+
e.stopPropagation();
|
|
365
|
+
// Remove class from all other containers first
|
|
366
|
+
$('.post-container-parent.post-hovered').removeClass('post-hovered');
|
|
367
|
+
// Add class only to this specific container
|
|
368
|
+
$(this).addClass('post-hovered');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
$(document).on('mouseleave.javis-hover', '.post-container-parent', function(e) {
|
|
372
|
+
e.stopPropagation();
|
|
373
|
+
$(this).removeClass('post-hovered');
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Initialize click handler for composer prompt card on feed page
|
|
379
|
+
* The card HTML is rendered server-side in feed.tpl for instant display (no flicker)
|
|
380
|
+
* This function only sets up the click handlers
|
|
381
|
+
*/
|
|
382
|
+
function initFeedComposerPromptHandler() {
|
|
383
|
+
// Only run on feed page with composer card present
|
|
384
|
+
var $card = $('.composer-prompt-card');
|
|
385
|
+
if (!$card.length) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Skip if already initialized
|
|
390
|
+
if ($card.data('handler-initialized')) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
$card.data('handler-initialized', true);
|
|
394
|
+
|
|
395
|
+
var isLoggedIn = typeof app !== 'undefined' && app.user && app.user.uid;
|
|
396
|
+
|
|
397
|
+
// Bind click handler to open composer
|
|
398
|
+
$card.on('click', '.composer-prompt-input, .composer-prompt-action', function(e) {
|
|
399
|
+
e.preventDefault();
|
|
400
|
+
|
|
401
|
+
if (!isLoggedIn) {
|
|
402
|
+
// Redirect to login
|
|
403
|
+
window.location.href = config.relative_path + '/login';
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Trigger the original New Topic button click
|
|
408
|
+
var $newTopicBtn = $('#new_topic');
|
|
409
|
+
if ($newTopicBtn.length) {
|
|
410
|
+
$newTopicBtn.trigger('click');
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
269
415
|
})();
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<div data-widget-area="header">
|
|
2
|
+
{{{each widgets.header}}}
|
|
3
|
+
{{widgets.header.html}}
|
|
4
|
+
{{{end}}}
|
|
5
|
+
</div>
|
|
6
|
+
<style>.feed .post-body .content > p:last-child { margin-bottom: 0px; }</style>
|
|
7
|
+
<div class="feed">
|
|
8
|
+
<div class="row">
|
|
9
|
+
<div data-widget-area="left" class="col-lg-3 col-sm-12 {{{ if !widgets.left.length }}}hidden{{{ end }}}">
|
|
10
|
+
{{{each widgets.left}}}
|
|
11
|
+
{{widgets.left.html}}
|
|
12
|
+
{{{end}}}
|
|
13
|
+
</div>
|
|
14
|
+
{{{ if ((widgets.left.length && widgets.right.length) || (!widgets.left.length && !widgets.right.length))}}}
|
|
15
|
+
<div class="col-lg-6 col-sm-12 mx-auto">
|
|
16
|
+
{{{ end }}}
|
|
17
|
+
{{{ if (widgets.left.length && !widgets.right.length) }}}
|
|
18
|
+
<div class="col-lg-6 col-sm-12 me-auto">
|
|
19
|
+
{{{ end }}}
|
|
20
|
+
{{{ if (!widgets.left.length && widgets.right.length) }}}
|
|
21
|
+
<div class="col-lg-6 col-sm-12 ms-auto">
|
|
22
|
+
{{{ end }}}
|
|
23
|
+
|
|
24
|
+
<!-- JAVIS: LinkedIn-style Composer Prompt Card -->
|
|
25
|
+
<div class="composer-prompt-card{{{ if !loggedIn }}} composer-prompt-guest{{{ end }}}">
|
|
26
|
+
<div class="composer-prompt-inner">
|
|
27
|
+
<div class="composer-prompt-avatar">
|
|
28
|
+
{{{ if loggedIn }}}
|
|
29
|
+
{buildAvatar(loggedInUser, "48px", true, "avatar avatar-rounded")}
|
|
30
|
+
{{{ else }}}
|
|
31
|
+
<div class="avatar avatar-rounded avatar-placeholder"><i class="fa fa-user"></i></div>
|
|
32
|
+
{{{ end }}}
|
|
33
|
+
</div>
|
|
34
|
+
<button class="composer-prompt-input" type="button" id="composer-prompt-btn">
|
|
35
|
+
<span class="composer-prompt-placeholder">{{{ if loggedIn }}}Start a discussion...{{{ else }}}Sign in to start a discussion...{{{ end }}}</span>
|
|
36
|
+
</button>
|
|
37
|
+
{{{ if loggedIn }}}
|
|
38
|
+
<div class="composer-prompt-actions">
|
|
39
|
+
<button class="composer-prompt-action" type="button" title="Add image"><i class="fa fa-image"></i></button>
|
|
40
|
+
<button class="composer-prompt-action" type="button" title="Add link"><i class="fa fa-link"></i></button>
|
|
41
|
+
</div>
|
|
42
|
+
{{{ end }}}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- Hidden controls row - New Topic button kept for JS functionality -->
|
|
47
|
+
<div class="d-flex justify-content-between py-2 mb-2 gap-1 feed-controls-row">
|
|
48
|
+
{{{ if canPost }}}
|
|
49
|
+
<button id="new_topic" class="btn btn-primary btn-sm d-none">[[category:new-topic-button]]</button>
|
|
50
|
+
{{{ end }}}
|
|
51
|
+
{{{ if (!loggedIn && !canPost) }}}
|
|
52
|
+
<a href="{config.relative_path}/login" class="btn btn-primary btn-sm d-none">[[category:guest-login-post]]</a>
|
|
53
|
+
{{{ end }}}
|
|
54
|
+
|
|
55
|
+
<div class="d-flex justify-content-end gap-1 ms-auto">
|
|
56
|
+
<!-- IMPORT partials/category/filter-dropdown-right.tpl -->
|
|
57
|
+
|
|
58
|
+
<div id="options-dropdown" class="btn-group dropdown dropdown-right bottom-sheet">
|
|
59
|
+
<button type="button" class="btn btn-ghost btn-sm d-flex align-items-center gap-2 ff-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
60
|
+
<i class="fa fa-fw fa-gear text-primary"></i>
|
|
61
|
+
</button>
|
|
62
|
+
<ul class="dropdown-menu p-1 text-sm" role="menu">
|
|
63
|
+
<li class="py-1 px-3">
|
|
64
|
+
<div class="form-check form-switch d-flex px-0 align-items-center justify-content-between gap-3">
|
|
65
|
+
<label class="form-check-label text-nowrap" for="showAllPosts">[[feed:show-all-posts]]</label>
|
|
66
|
+
<input class="form-check-input float-none m-0 pointer" type="checkbox" role="switch" id="showAllPosts" {{{ if showAllPosts }}}checked{{{ end }}}>
|
|
67
|
+
</div>
|
|
68
|
+
</li>
|
|
69
|
+
{{{ if loggedIn }}}
|
|
70
|
+
<li class="py-1 px-3">
|
|
71
|
+
<div class="form-check form-switch d-flex px-0 align-items-center justify-content-between gap-3">
|
|
72
|
+
<label class="form-check-label text-nowrap" for="showFollowedUsers">[[feed:followed-users-only]]</label>
|
|
73
|
+
<input class="form-check-input float-none m-0 pointer" type="checkbox" role="switch" id="showFollowedUsers" {{{ if showFollowed }}}checked{{{ end }}}>
|
|
74
|
+
</div>
|
|
75
|
+
</li>
|
|
76
|
+
{{{ end }}}
|
|
77
|
+
</ul>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{{{ if !posts.length }}}
|
|
83
|
+
<div class="alert alert-warning text-center">[[feed:no-posts-found]] {{{ if !following.length }}}[[feed:are-you-following-anyone]] {{{ end }}}</div>
|
|
84
|
+
{{{ end }}}
|
|
85
|
+
|
|
86
|
+
<ul component="posts" class="list-unstyled" data-nextstart="{nextStart}">
|
|
87
|
+
{{{ each posts }}}
|
|
88
|
+
<li component="post" class="shadow-sm mb-3 rounded-2 border posts-list-item {{{ if ./deleted }}} deleted{{{ else }}}{{{ if ./topic.deleted }}} deleted{{{ end }}}{{{ end }}}{{{ if ./topic.scheduled }}} scheduled{{{ end }}}" data-pid="{./pid}" data-uid="{./uid}">
|
|
89
|
+
|
|
90
|
+
{{{ if (showThumbs && ./topic.thumbs.length)}}}
|
|
91
|
+
<div class="p-1 position-relative">
|
|
92
|
+
<div class="overflow-hidden rounded-1" style="max-height: 300px;">
|
|
93
|
+
<a href="{config.relative_path}/topic/{./topic.slug}">
|
|
94
|
+
<img class="w-100" src="{./topic.thumbs.0.url}">
|
|
95
|
+
</a>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="position-absolute end-0 bottom-0 p-3 d-flex gap-2 align-items-center pe-none">
|
|
99
|
+
{{{ each ./topic.thumbs }}}
|
|
100
|
+
{{{ if (@index != 0) }}}
|
|
101
|
+
<img class="rounded-1" style="max-height: 64px; object-fit: contain;" src="{./url}">
|
|
102
|
+
{{{ end }}}
|
|
103
|
+
{{{ end }}}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
{{{ end }}}
|
|
107
|
+
|
|
108
|
+
<div class="d-flex gap-2 p-3">
|
|
109
|
+
<div class="d-none d-lg-block">
|
|
110
|
+
<a class="lh-1 text-decoration-none" href="{config.relative_path}/user/{./user.userslug}">{buildAvatar(./user, "40px", true, "not-responsive")}</a>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="post-body d-flex flex-column gap-2 flex-grow-1 hover-parent" style="min-width: 0px;">
|
|
113
|
+
{{{ if ./isMainPost }}}
|
|
114
|
+
<a class="lh-1 topic-title fw-semibold fs-5 text-reset text-break d-block" href="{config.relative_path}/topic/{./topic.slug}">
|
|
115
|
+
{./topic.title}
|
|
116
|
+
</a>
|
|
117
|
+
{{{ end }}}
|
|
118
|
+
|
|
119
|
+
<div class="d-flex gap-1 post-info text-sm align-items-center">
|
|
120
|
+
<div class="post-author d-flex align-items-center gap-1">
|
|
121
|
+
<a class="d-inline d-lg-none lh-1 text-decoration-none" href="{config.relative_path}/user/{./user.userslug}">{buildAvatar(./user, "16px", true, "not-responsive")}</a>
|
|
122
|
+
<a class="lh-normal fw-semibold text-nowrap" href="{config.relative_path}/user/{./user.userslug}">{./user.displayname}</a>
|
|
123
|
+
</div>
|
|
124
|
+
{{{ if !./isMainPost}}}{./repliedString}{{{ else }}}<span class="timeago text-muted lh-normal" title="{./timestampISO}"></span>{{{ end}}}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div component="post/content" class="content text-sm text-break position-relative truncate-post-content">
|
|
128
|
+
<a href="{config.relative_path}/post/{./pid}" class="stretched-link"></a>
|
|
129
|
+
{./content}
|
|
130
|
+
</div>
|
|
131
|
+
<div class="position-relative hover-visible">
|
|
132
|
+
<button component="show/more" class="btn btn-light btn-sm rounded-pill position-absolute start-50 translate-middle-x bottom-0 z-1 hidden ff-secondary">[[feed:see-more]]</button>
|
|
133
|
+
</div>
|
|
134
|
+
<hr class="my-2"/>
|
|
135
|
+
<div class="d-flex justify-content-between">
|
|
136
|
+
<a href="{config.relative_path}/post/{{{ if ./topic.teaserPid }}}{./topic.teaserPid}{{{ else }}}{./pid}{{{ end }}}" class="btn btn-link btn-sm text-body {{{ if !./isMainPost }}}invisible{{{ end }}}"><i class="fa-fw fa-regular fa-message text-muted"></i> {humanReadableNumber(./topic.postcount)}</a>
|
|
137
|
+
|
|
138
|
+
<a href="#" data-pid="{./pid}" data-action="bookmark" data-bookmarked="{./bookmarked}" data-bookmarks="{./bookmarks}" class="btn btn-link btn-sm text-body"><i class="fa-fw fa-bookmark {{{ if ./bookmarked }}}fa text-primary{{{ else }}}fa-regular text-muted{{{ end }}}"></i> <span component="bookmark-count">{humanReadableNumber(./bookmarks)}</span></a>
|
|
139
|
+
|
|
140
|
+
<a href="#" data-pid="{./pid}" data-action="upvote" data-upvoted="{./upvoted}" data-upvotes="{./upvotes}" class="btn btn-link btn-sm text-body"><i class="fa-fw fa-heart {{{ if ./upvoted }}}fa text-danger{{{ else }}}fa-regular text-muted{{{ end }}}"></i> <span component="upvote-count">{humanReadableNumber(./upvotes)}</span></a>
|
|
141
|
+
|
|
142
|
+
<a href="#" data-pid="{./pid}" data-is-main="{./isMainPost}" data-tid="{./tid}" data-action="reply" class="btn btn-link btn-sm text-body"><i class="fa-fw fa fa-reply text-muted"></i> [[topic:reply]]</a>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</li>
|
|
147
|
+
{{{ end }}}
|
|
148
|
+
</ul>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div data-widget-area="right" class="col-lg-3 col-sm-12 {{{ if !widgets.right.length }}}hidden{{{ end }}}">
|
|
152
|
+
{{{each widgets.right}}}
|
|
153
|
+
{{widgets.right.html}}
|
|
154
|
+
{{{end}}}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div data-widget-area="footer">
|
|
160
|
+
{{{each widgets.footer}}}
|
|
161
|
+
{{widgets.footer.html}}
|
|
162
|
+
{{{end}}}
|
|
163
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div component="post/parent" data-parent-pid="{./parent.pid}" data-uid="{./parent.uid}" class="btn btn-ghost btn-sm d-flex gap-2 text-start flex-row mb-2 post-parent-clickable" style="font-size: 13px;">
|
|
2
|
+
<div class="d-flex gap-2 text-nowrap">
|
|
3
|
+
<div class="d-flex flex-nowrap gap-1 align-items-center">
|
|
4
|
+
<a href="{config.relative_path}/user/{./parent.user.userslug}" class="text-decoration-none lh-1" onclick="event.stopPropagation();">{buildAvatar(./parent.user, "16px", true, "not-responsive align-middle")}</a>
|
|
5
|
+
<a class="fw-semibold text-truncate" style="max-width: 150px;" href="{config.relative_path}/user/{./parent.user.userslug}" onclick="event.stopPropagation();">{./parent.user.displayname}</a>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
<div component="post/parent/content" class="text-muted line-clamp-1 text-break w-100">{./parent.content}</div>
|
|
9
|
+
</div>
|
package/theme.scss
CHANGED