Browse Source

Merge pull request #12121 from abpframework/pr/11961

Add scroll index to blog post page
pull/12133/head
Enis Necipoglu 5 years ago
committed by GitHub
parent
commit
dbc6819058
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/BlogPostScrollIndexFeature.cs
  2. 3
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/GlobalCmsKitFeatures.cs
  3. 2
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json
  4. 2
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json
  5. 197
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPost.cshtml
  6. 17
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/blogPost.css
  7. 46
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/blogpost-scroll-index.js
  8. 60
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/bootstrap-toc.css
  9. 168
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/bootstrap-toc.js

17
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/BlogPostScrollIndexFeature.cs

@ -0,0 +1,17 @@
using JetBrains.Annotations;
using Volo.Abp.GlobalFeatures;
namespace Volo.CmsKit.GlobalFeatures;
[GlobalFeatureName(Name)]
public class BlogPostScrollIndexFeature : GlobalFeature
{
public const string Name = "CmsKit.BlogPost.ScrollIndex";
internal BlogPostScrollIndexFeature(
[NotNull] GlobalCmsKitFeatures cmsKit
) : base(cmsKit)
{
}
}

3
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/GlobalCmsKitFeatures.cs

@ -26,6 +26,8 @@ public class GlobalCmsKitFeatures : GlobalModuleFeatures
public MenuFeature Menu => GetFeature<MenuFeature>();
public GlobalResourcesFeature GlobalResources => GetFeature<GlobalResourcesFeature>();
public BlogPostScrollIndexFeature BlogPostScrollIndex => GetFeature<BlogPostScrollIndexFeature>();
public GlobalCmsKitFeatures([NotNull] GlobalFeatureManager featureManager)
: base(featureManager)
@ -40,5 +42,6 @@ public class GlobalCmsKitFeatures : GlobalModuleFeatures
AddFeature(new CmsUserFeature(this));
AddFeature(new MenuFeature(this));
AddFeature(new GlobalResourcesFeature(this));
AddFeature(new BlogPostScrollIndexFeature(this));
}
}

2
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json

@ -166,6 +166,8 @@
"Script": "Script",
"Style": "Style",
"SavedSuccessfully": "Saved successfully",
"GoToTop": "Go to top",
"InThisDocument": "In this document",
"RemoveCoverImageConfirmationMessage": "Are you sure to remove the cover image?",
"RemoveCoverImage": "Remove the cover image"
}

2
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json

@ -165,6 +165,8 @@
"Script": "Script",
"Style": "Style",
"SavedSuccessfully": "Başarıyla kaydedildi",
"GoToTop": "Yukarı Git",
"InThisDocument": "Bu belgede",
"RemoveCoverImageConfirmationMessage": "Kapak resmini kaldırmak istediğinize emin misiniz?",
"RemoveCoverImage": "Kapak resmini kaldır"
}

197
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPost.cshtml

@ -16,94 +16,133 @@
@inject IMarkdownToHtmlRenderer MarkdownRenderer
@{
string dummyImageSource = "https://dummyimage.com/1280x720/a3a3a3/fff.png?text=" + Model.BlogPost.Title;
var isScrollIndexEnabled = GlobalFeatureManager.Instance.IsEnabled<BlogPostScrollIndexFeature>();
}
@section styles{
<abp-abp-style-bundle>
<abp-style src="/Pages/Public/CmsKit/Blogs/blogPost.css" />
<abp-style type="typeof(HighlightJsStyleContributor)" />
</abp-abp-style-bundle>
@if (isScrollIndexEnabled)
{
<abp-abp-style-bundle>
<abp-style src="/Pages/Public/CmsKit/Blogs/bootstrap-toc.css"/>
</abp-abp-style-bundle>
}
<abp-abp-style-bundle>
<abp-style src="/Pages/Public/CmsKit/Blogs/blogPost.css"/>
<abp-style type="typeof(HighlightJsStyleContributor)"/>
</abp-abp-style-bundle>
}
@section scripts{
<abp-script-bundle>
<abp-script type="typeof(HighlightJsScriptContributor)" />
<abp-script src="/Pages/Public/CmsKit/highlightOnLoad.js" />
</abp-script-bundle>
}
@if (isScrollIndexEnabled)
{
<abp-script-bundle>
<abp-style src="/Pages/Public/CmsKit/Blogs/bootstrap-toc.js"/>
<abp-script src="/Pages/Public/CmsKit/Blogs/blogpost-scroll-index.js"/>
</abp-script-bundle>
}
@{
string dummyImageSource = "https://dummyimage.com/1280x720/a3a3a3/fff.png?text=" + Model.BlogPost.Title;
<abp-script-bundle>
<abp-script type="typeof(HighlightJsScriptContributor)"/>
<abp-script src="/Pages/Public/CmsKit/highlightOnLoad.js"/>
</abp-script-bundle>
}
<abp-card class="mb-4">
<img src="/api/cms-kit/media/@Model.BlogPost.CoverImageMediaId" class="card-img-top" onerror="this.src='@dummyImageSource'" />
<abp-card-body>
<abp-row>
<div class="col-lg-8 col-md-10 mx-auto pb-4">
<h1 class="mt-lg-4 mt-md-3">@Model.BlogPost.Title</h1>
<p class="mb-lg-5 mb-md-3">
<a href="/blogs/@Model.BlogSlug?authorId=@Model.BlogPost.Author.Id">
<span class="font-weight-bold">@@@Model.BlogPost.Author?.UserName</span>
</a>
<small style="opacity:.65;">@Model.BlogPost.CreationTime</small>
</p>
@if(!Model.BlogPost.Content.IsNullOrEmpty())
{
@Html.Raw(await MarkdownRenderer.RenderAsync(Model.BlogPost.Content))
}
<p class="mb-3">
@if (Model.BlogPost.LastModificationTime != null)
{
<small style="opacity:.65;">@L["LastModification"].Value : @Model.BlogPost.LastModificationTime</small>
}
</p>
<hr />
@if (GlobalFeatureManager.Instance.IsEnabled<TagsFeature>())
{
if (Model.TagsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(TagViewComponent), new
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</div>
</abp-row>
<abp-row class="row">
<abp-column size-lg="_6" size-md="_12">
@if (GlobalFeatureManager.Instance.IsEnabled<ReactionsFeature>())
{
if (Model.ReactionsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new
<div class="row">
<div @Html.Raw(isScrollIndexEnabled ? "class=\"col-md-10 col-sm-12\"" : "class=\"col-md-12\"")>
<abp-card class="mb-4">
<img src="/api/cms-kit/media/@Model.BlogPost.CoverImageMediaId" class="card-img-top" onerror="this.src='@dummyImageSource'"/>
<abp-card-body>
<abp-row>
<div class="col-lg-8 col-md-10 mx-auto pb-4">
<h1 class="mt-lg-4 mt-md-3">@Model.BlogPost.Title</h1>
<p class="mb-lg-5 mb-md-3">
<a href="/blogs/@Model.BlogSlug?authorId=@Model.BlogPost.Author.Id">
<span class="font-weight-bold">@@@Model.BlogPost.Author?.UserName</span>
</a>
<small style="opacity:.65;">@Model.BlogPost.CreationTime</small>
</p>
@if (!Model.BlogPost.Content.IsNullOrEmpty())
{
@Html.Raw(await MarkdownRenderer.RenderAsync(Model.BlogPost.Content))
}
<p class="mb-3">
@if (Model.BlogPost.LastModificationTime != null)
{
<small style="opacity:.65;">@L["LastModification"].Value : @Model.BlogPost.LastModificationTime</small>
}
</p>
<hr/>
@if (GlobalFeatureManager.Instance.IsEnabled<TagsFeature>())
{
if (Model.TagsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(TagViewComponent), new
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</div>
</abp-row>
<abp-row class="row">
<abp-column size-lg="_6" size-md="_12">
@if (GlobalFeatureManager.Instance.IsEnabled<ReactionsFeature>())
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</abp-column>
<abp-column size-lg="_6" size-md="_12">
@if (GlobalFeatureManager.Instance.IsEnabled<RatingsFeature>())
{
if (Model.RatingsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(RatingViewComponent), new
if (Model.ReactionsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</abp-column>
<abp-column size-lg="_6" size-md="_12">
@if (GlobalFeatureManager.Instance.IsEnabled<RatingsFeature>())
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</abp-column>
</abp-row>
</abp-card-body>
</abp-card>
if (Model.RatingsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(RatingViewComponent), new
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</abp-column>
</abp-row>
</abp-card-body>
</abp-card>
</div>
@if (isScrollIndexEnabled)
{
<div class="col-md-2 d-sm-none d-md-block">
<div id="scroll-index" class="docs-inner-anchors mt-2">
<h5>@L["InThisDocument"]</h5>
<nav id="blog-post-sticky-index" class="navbar index-scroll pt-0">
</nav>
<div class="row">
<div class="col p-0 py-3">
<a href="#" class="scroll-top-btn">
<i class="fa fa-chevron-up"></i> @L["GoToTop"]
</a>
</div>
</div>
</div>
</div>
}
</div>
@if (GlobalFeatureManager.Instance.IsEnabled<CommentsFeature>())
{

17
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/blogPost.css

@ -20,4 +20,21 @@ span.area-title {
}
.popover {
min-width: 276px;
}
#scroll-index {
position: sticky;
top: 20px;
}
#scroll-index .scroll-top-btn.showup{
display: block;
}
#scroll-index .scroll-top-btn{
display: none;
font-size: .85em;
color: #aaa;
text-decoration: none;
padding-left: 18px;
}

46
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/blogpost-scroll-index.js

@ -0,0 +1,46 @@
$(function () {
var $myNav = $('#blog-post-sticky-index');
var $scrollToTopBtn = $('.scroll-top-btn');
window.Toc.helpers.createNavList = function () {
return $('<ul class="nav nav-pills flex-column"></ul>');
};
window.Toc.helpers.createChildNavList = function ($parent) {
var $childList = this.createNavList();
$parent.append($childList);
return $childList;
};
window.Toc.helpers.generateNavEl = function (anchor, text) {
var $a = $('<a class="nav-link"></a>');
$a.attr('href', '#' + anchor);
$a.text(text);
var $li = $('<li class="nav-item"></li>');
$li.append($a);
return $li;
};
Toc.init($myNav);
$('body').scrollspy({
target: $myNav,
});
$scrollToTopBtn.click(function () {
$('html, body').animate({scrollTop: 0}, 'fast');
});
// When the user scrolls down 20px from the top of the document, show the button
window.onscroll = function () {
scrollFunction()
};
function scrollFunction() {
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
$scrollToTopBtn.addClass('showup');
} else {
$scrollToTopBtn.removeClass('showup');
}
}
});

60
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/bootstrap-toc.css

@ -0,0 +1,60 @@
/*!
* Bootstrap Table of Contents v<%= version %> (http://afeld.github.io/bootstrap-toc/)
* Copyright 2015 Aidan Feldman
* Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */
/* modified from https://github.com/twbs/bootstrap/blob/94b4076dd2efba9af71f0b18d4ee4b163aa9e0dd/docs/assets/css/src/docs.css#L548-L601 */
/* All levels of nav */
nav[data-toggle='toc'] .nav > li > a {
display: block;
padding: 4px 20px;
font-size: 13px;
font-weight: 500;
color: #767676;
}
nav[data-toggle='toc'] .nav > li > a:hover,
nav[data-toggle='toc'] .nav > li > a:focus {
padding-left: 19px;
color: #563d7c;
text-decoration: none;
background-color: transparent;
border-left: 1px solid #563d7c;
}
nav[data-toggle='toc'] .nav-link.active,
nav[data-toggle='toc'] .nav-link.active:hover,
nav[data-toggle='toc'] .nav-link.active:focus {
padding-left: 18px;
font-weight: bold;
color: #563d7c;
background-color: transparent;
border-left: 2px solid #563d7c;
}
/* Nav: second level (shown on .active) */
nav[data-toggle='toc'] .nav-link + ul {
display: none; /* Hide by default, but at >768px, show it */
padding-bottom: 10px;
}
nav[data-toggle='toc'] .nav .nav > li > a {
padding-top: 1px;
padding-bottom: 1px;
padding-left: 30px;
font-size: 12px;
font-weight: normal;
}
nav[data-toggle='toc'] .nav .nav > li > a:hover,
nav[data-toggle='toc'] .nav .nav > li > a:focus {
padding-left: 29px;
}
nav[data-toggle='toc'] .nav .nav > li > .active,
nav[data-toggle='toc'] .nav .nav > li > .active:hover,
nav[data-toggle='toc'] .nav .nav > li > .active:focus {
padding-left: 28px;
font-weight: 500;
}
nav[data-toggle='toc'] .nav-link.active + ul {
display: block;
}

168
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/bootstrap-toc.js

@ -0,0 +1,168 @@
/*!
* Bootstrap Table of Contents v1.0.0 (http://afeld.github.io/bootstrap-toc/)
* Copyright 2015 Aidan Feldman
* Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */
(function ($) {
'use strict';
window.Toc = {
helpers: {
// return all matching elements in the set, or their descendants
findOrFilter: function ($el, selector) {
// http://danielnouri.org/notes/2011/03/14/a-jquery-find-that-also-finds-the-root-element/
// http://stackoverflow.com/a/12731439/358804
var $descendants = $el.find(selector);
return $el
.filter(selector)
.add($descendants)
.filter(':not([data-toc-skip])');
},
generateUniqueIdBase: function (el) {
var text = $(el).text();
var anchor = text
.trim()
.toLowerCase()
.replace(/[^A-Za-z0-9]+/g, '-');
return anchor || el.tagName.toLowerCase();
},
generateUniqueId: function (el) {
var anchorBase = this.generateUniqueIdBase(el);
for (var i = 0; ; i++) {
var anchor = anchorBase;
if (i > 0) {
// add suffix
anchor += '-' + i;
}
// check if ID already exists
if (!document.getElementById(anchor)) {
return anchor;
}
}
},
generateAnchor: function (el) {
if (el.id) {
return el.id;
} else {
var anchor = this.generateUniqueId(el);
el.id = anchor;
return anchor;
}
},
createNavList: function () {
return $('<ul class="nav navbar-nav"></ul>');
},
createChildNavList: function ($parent) {
var $childList = this.createNavList();
$parent.append($childList);
return $childList;
},
generateNavEl: function (anchor, text) {
var $a = $('<a class="nav-link"></a>');
$a.attr('href', '#' + anchor);
$a.text(text);
var $li = $('<li></li>');
$li.append($a);
return $li;
},
generateNavItem: function (headingEl) {
var anchor = this.generateAnchor(headingEl);
var $heading = $(headingEl);
var text = $heading.data('toc-text') || $heading.text();
return this.generateNavEl(anchor, text);
},
// Find the first heading level (`<h1>`, then `<h2>`, etc.) that has more than one element. Defaults to 1 (for `<h1>`).
getTopLevel: function ($scope) {
for (var i = 1; i <= 6; i++) {
var $headings = this.findOrFilter($scope, 'h' + i);
if ($headings.length > 1) {
return i;
}
}
return 1;
},
// returns the elements for the top level, and the next below it
getHeadings: function ($scope, topLevel) {
var topSelector = 'h' + topLevel;
var secondaryLevel = topLevel + 1;
var secondarySelector = 'h' + secondaryLevel;
return this.findOrFilter(
$scope,
topSelector + ',' + secondarySelector
);
},
getNavLevel: function (el) {
return parseInt(el.tagName.charAt(1), 10);
},
populateNav: function ($topContext, topLevel, $headings) {
var $context = $topContext;
var $prevNav;
var helpers = this;
$headings.each(function (i, el) {
var $newNav = helpers.generateNavItem(el);
var navLevel = helpers.getNavLevel(el);
// determine the proper $context
if (navLevel === topLevel) {
// use top level
$context = $topContext;
} else if ($prevNav && $context === $topContext) {
// create a new level of the tree and switch to it
$context = helpers.createChildNavList($prevNav);
} // else use the current $context
$context.append($newNav);
$prevNav = $newNav;
});
},
parseOps: function (arg) {
var opts;
if (arg.jquery) {
opts = {
$nav: arg,
};
} else {
opts = arg;
}
opts.$scope = opts.$scope || $(document.body);
return opts;
},
},
// accepts a jQuery object, or an options object
init: function (opts) {
opts = this.helpers.parseOps(opts);
// ensure that the data attribute is in place for styling
opts.$nav.attr('data-toggle', 'toc');
var $topContext = this.helpers.createChildNavList(opts.$nav);
var topLevel = this.helpers.getTopLevel(opts.$scope);
var $headings = this.helpers.getHeadings(opts.$scope, topLevel);
this.helpers.populateNav($topContext, topLevel, $headings);
},
};
$(function () {
$('nav[data-toggle="toc"]').each(function (i, el) {
var $nav = $(el);
Toc.init($nav);
});
});
})(jQuery);
Loading…
Cancel
Save