dokno 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +130 -0
- data/Rakefile +22 -0
- data/app/assets/config/dokno_manifest.js +3 -0
- data/app/assets/javascripts/dokno.js +144 -0
- data/app/assets/javascripts/dokno/application.js +15 -0
- data/app/assets/javascripts/feather.min.js +13 -0
- data/app/assets/javascripts/init.js +5 -0
- data/app/assets/stylesheets/dokno/application.css +137 -0
- data/app/assets/stylesheets/dokno/tailwind.min.css +1 -0
- data/app/controllers/dokno/application_controller.rb +7 -0
- data/app/controllers/dokno/articles_controller.rb +115 -0
- data/app/controllers/dokno/categories_controller.rb +63 -0
- data/app/controllers/dokno/pagination_concern.rb +15 -0
- data/app/controllers/dokno/user_concern.rb +38 -0
- data/app/helpers/dokno/application_helper.rb +17 -0
- data/app/models/dokno/application_record.rb +5 -0
- data/app/models/dokno/article.rb +215 -0
- data/app/models/dokno/article_slug.rb +7 -0
- data/app/models/dokno/category.rb +100 -0
- data/app/models/dokno/log.rb +7 -0
- data/app/views/dokno/_article_formatting.html.erb +89 -0
- data/app/views/dokno/_article_panel.html.erb +112 -0
- data/app/views/dokno/_panel_formatting.html.erb +95 -0
- data/app/views/dokno/_reset_formatting.html.erb +55 -0
- data/app/views/dokno/articles/_article_form.html.erb +60 -0
- data/app/views/dokno/articles/edit.html.erb +7 -0
- data/app/views/dokno/articles/new.html.erb +7 -0
- data/app/views/dokno/articles/show.html.erb +130 -0
- data/app/views/dokno/categories/_category_form.html.erb +33 -0
- data/app/views/dokno/categories/edit.html.erb +7 -0
- data/app/views/dokno/categories/index.html.erb +96 -0
- data/app/views/dokno/categories/new.html.erb +7 -0
- data/app/views/layouts/dokno/application.html.erb +100 -0
- data/app/views/partials/_form_errors.html.erb +18 -0
- data/app/views/partials/_logs.html.erb +52 -0
- data/app/views/partials/_pagination.html.erb +35 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20201203190330_baseline.rb +62 -0
- data/lib/dokno.rb +4 -0
- data/lib/dokno/config/config.rb +73 -0
- data/lib/dokno/engine.rb +27 -0
- data/lib/dokno/version.rb +3 -0
- data/lib/generators/dokno/install/install_generator.rb +13 -0
- data/lib/generators/dokno/templates/config/dokno_template.md +5 -0
- data/lib/generators/dokno/templates/config/initializers/dokno.rb +14 -0
- data/lib/tasks/dokno_tasks.rake +4 -0
- metadata +253 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
module Dokno
|
2
|
+
class Category < ApplicationRecord
|
3
|
+
belongs_to :parent,
|
4
|
+
class_name: 'Dokno::Category',
|
5
|
+
primary_key: 'id',
|
6
|
+
foreign_key: 'category_id',
|
7
|
+
optional: true,
|
8
|
+
inverse_of: :children
|
9
|
+
has_many :children,
|
10
|
+
class_name: 'Dokno::Category',
|
11
|
+
primary_key: 'id',
|
12
|
+
foreign_key: 'category_id',
|
13
|
+
dependent: :nullify,
|
14
|
+
inverse_of: :parent
|
15
|
+
|
16
|
+
has_and_belongs_to_many :articles
|
17
|
+
|
18
|
+
validates :name, :code, presence: true, uniqueness: true
|
19
|
+
validate :circular_parent_check
|
20
|
+
|
21
|
+
before_validation :set_code
|
22
|
+
|
23
|
+
# The display breadcrumb for the Category
|
24
|
+
def breadcrumb
|
25
|
+
crumbs = [name]
|
26
|
+
parent_category_id = category_id
|
27
|
+
|
28
|
+
loop do
|
29
|
+
break if parent_category_id.blank?
|
30
|
+
|
31
|
+
parent_category = self.class.find(parent_category_id)
|
32
|
+
crumbs.prepend parent_category.name
|
33
|
+
parent_category_id = parent_category.category_id
|
34
|
+
end
|
35
|
+
|
36
|
+
crumbs.join(' > ')
|
37
|
+
end
|
38
|
+
|
39
|
+
# All Articles in the Category, including all child Categories
|
40
|
+
def articles_in_branch(order: :updated)
|
41
|
+
records = Article
|
42
|
+
.includes(:categories_dokno_articles, :categories)
|
43
|
+
.joins(:categories)
|
44
|
+
.where(dokno_categories: { id: self.class.branch(parent_category_id: id).pluck(:id) })
|
45
|
+
|
46
|
+
records = records.updated_order if order == :updated
|
47
|
+
records = records.newest_order if order == :newest
|
48
|
+
records = records.view_order if order == :views
|
49
|
+
records = records.alpha_order if order == :alpha
|
50
|
+
|
51
|
+
records
|
52
|
+
end
|
53
|
+
|
54
|
+
def branch
|
55
|
+
self.class.branch(parent_category_id: id)
|
56
|
+
end
|
57
|
+
|
58
|
+
# The given Category and all child Categories. Useful for filtering associated articles.
|
59
|
+
def self.branch(parent_category_id:, at_top: true)
|
60
|
+
return if parent_category_id.blank?
|
61
|
+
|
62
|
+
categories = []
|
63
|
+
parent_category = find(parent_category_id)
|
64
|
+
child_categories = parent_category.children.to_a
|
65
|
+
|
66
|
+
child_categories.each do |child_category|
|
67
|
+
categories << child_category << branch(parent_category_id: child_category.id, at_top: false)
|
68
|
+
end
|
69
|
+
|
70
|
+
categories.prepend parent_category if at_top
|
71
|
+
categories.flatten
|
72
|
+
end
|
73
|
+
|
74
|
+
# HTML markup for Category SELECT field OPTION lists
|
75
|
+
def self.select_option_markup(selected_category_codes: nil, exclude_category_id: nil)
|
76
|
+
breadcrumbs = all
|
77
|
+
.reject { |category| category.id == exclude_category_id.to_i }
|
78
|
+
.map { |category| { code: category.code, name: category.breadcrumb } }
|
79
|
+
breadcrumbs.sort_by { |category_hash| category_hash[:name] }.map do |category_hash|
|
80
|
+
selected = selected_category_codes&.include?(category_hash[:code])
|
81
|
+
%(<option value="#{category_hash[:code]}" #{'selected="selected"' if selected}>#{category_hash[:name]}</option>)
|
82
|
+
end.join
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Never allow setting of parent to self
|
88
|
+
def circular_parent_check
|
89
|
+
return unless persisted? && id.to_i == category_id.to_i
|
90
|
+
|
91
|
+
errors.add(:category_id, "can't set parent to self")
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_code
|
95
|
+
return unless name.present?
|
96
|
+
|
97
|
+
self.code = name.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
<style>
|
2
|
+
/* Article formatting */
|
3
|
+
.dokno-article-content-markup {
|
4
|
+
font-weight: 300;
|
5
|
+
line-height: 1.75rem;
|
6
|
+
font-size: 1rem;
|
7
|
+
}
|
8
|
+
|
9
|
+
.dokno-article-content-markup code {
|
10
|
+
font-size: 80%;
|
11
|
+
background-color: #eee;
|
12
|
+
padding: 3px 5px;
|
13
|
+
border-radius: 3px;
|
14
|
+
}
|
15
|
+
|
16
|
+
.dokno-article-content-markup b,
|
17
|
+
.dokno-article-content-markup strong {
|
18
|
+
font-weight: 500;
|
19
|
+
}
|
20
|
+
|
21
|
+
.dokno-article-content-markup p,
|
22
|
+
.dokno-article-content-markup ul,
|
23
|
+
.dokno-article-content-markup ol,
|
24
|
+
.dokno-article-content-markup hr,
|
25
|
+
.dokno-article-content-markup table {
|
26
|
+
margin-bottom: 1.25rem;
|
27
|
+
}
|
28
|
+
|
29
|
+
.dokno-article-content-markup h1,
|
30
|
+
.dokno-article-content-markup h2,
|
31
|
+
.dokno-article-content-markup h3,
|
32
|
+
.dokno-article-content-markup h4,
|
33
|
+
.dokno-article-content-markup h5,
|
34
|
+
.dokno-article-content-markup h6 {
|
35
|
+
margin-bottom: 1.25rem;
|
36
|
+
font-weight: 600;
|
37
|
+
}
|
38
|
+
|
39
|
+
.dokno-article-content-markup h1 {
|
40
|
+
font-size: 2rem;
|
41
|
+
}
|
42
|
+
|
43
|
+
.dokno-article-content-markup h2 {
|
44
|
+
font-size: 1.625rem;
|
45
|
+
}
|
46
|
+
|
47
|
+
.dokno-article-content-markup h3 {
|
48
|
+
font-size: 1.25rem;
|
49
|
+
}
|
50
|
+
|
51
|
+
.dokno-article-content-markup h4 {
|
52
|
+
font-size: 1rem;
|
53
|
+
}
|
54
|
+
|
55
|
+
.dokno-article-content-markup blockquote {
|
56
|
+
border-left: 4px solid #edf2f7;
|
57
|
+
padding-left: 1.25rem;
|
58
|
+
}
|
59
|
+
|
60
|
+
.dokno-article-content-markup ul,
|
61
|
+
.dokno-article-content-markup ol {
|
62
|
+
list-style-position: inside;
|
63
|
+
}
|
64
|
+
|
65
|
+
.dokno-article-content-markup ul {
|
66
|
+
list-style-type: disc;
|
67
|
+
}
|
68
|
+
|
69
|
+
.dokno-article-content-markup ol {
|
70
|
+
list-style-type: decimal;
|
71
|
+
}
|
72
|
+
|
73
|
+
.dokno-article-content-markup table {
|
74
|
+
table-layout: auto;
|
75
|
+
}
|
76
|
+
|
77
|
+
.dokno-article-content-markup table th,
|
78
|
+
.dokno-article-content-markup table td {
|
79
|
+
padding-left: 1rem;
|
80
|
+
padding-right: 1rem;
|
81
|
+
padding-top: 0.5rem;
|
82
|
+
padding-bottom: 0.5rem;
|
83
|
+
font-weight: normal;
|
84
|
+
}
|
85
|
+
|
86
|
+
.dokno-article-content-markup table td {
|
87
|
+
border-width: 1px;
|
88
|
+
}
|
89
|
+
</style>
|
@@ -0,0 +1,112 @@
|
|
1
|
+
<!-- DOKNO SLIDE-OUT PANEL -->
|
2
|
+
<!-- This file is injected into the host layout when included via render ` 'dokno/article_panel' `. -->
|
3
|
+
<!-- It provides the necessary markup, CSS, and JS to support in-context flyout article panels. -->
|
4
|
+
|
5
|
+
<!-- Reset styles -->
|
6
|
+
<%= render 'dokno/reset_formatting' %>
|
7
|
+
|
8
|
+
<!-- Slide-out panel styles -->
|
9
|
+
<%= render 'dokno/panel_formatting' %>
|
10
|
+
|
11
|
+
<!-- Article content styles -->
|
12
|
+
<%= render 'dokno/article_formatting' %>
|
13
|
+
|
14
|
+
<!-- Slide-out panel markup -->
|
15
|
+
<div id="dokno-panel-container">
|
16
|
+
<div id="dokno-panel-close" class="dokno-hidden">
|
17
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="#eee" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-circle">
|
18
|
+
<circle cx="12" cy="12" r="10"></circle>
|
19
|
+
<line x1="15" y1="9" x2="9" y2="15"></line>
|
20
|
+
<line x1="9" y1="9" x2="15" y2="15"></line>
|
21
|
+
</svg>
|
22
|
+
</div>
|
23
|
+
<div id="dokno-panel-title"></div>
|
24
|
+
<div id="dokno-panel-summary"></div>
|
25
|
+
<div id="dokno-panel-markdown" class="dokno-article-content-markup"></div>
|
26
|
+
<div id="dokno-panel-footer"></div>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<!-- Slide-out panel behavior -->
|
30
|
+
<script>
|
31
|
+
function doknoOpenPanel(slug) {
|
32
|
+
if (slug == dokno__slug) {
|
33
|
+
doknoClosePanel();
|
34
|
+
return true;
|
35
|
+
}
|
36
|
+
dokno__slug = slug;
|
37
|
+
|
38
|
+
// Can't use fetch API; IE
|
39
|
+
const request = new XMLHttpRequest();
|
40
|
+
|
41
|
+
request.open('GET', '<%= Dokno::Engine.routes.url_helpers.root_path %>article_panel/' + slug, true);
|
42
|
+
request.onload = function() {
|
43
|
+
if (request.readyState == 4 && request.status == 200) {
|
44
|
+
try {
|
45
|
+
var data = JSON.parse(request.responseText);
|
46
|
+
} catch(err) {
|
47
|
+
console.log("Dokno: " + err.message + " in " + request.responseText);
|
48
|
+
return;
|
49
|
+
}
|
50
|
+
|
51
|
+
// Populate panel
|
52
|
+
dokno__id = data.id;
|
53
|
+
dokno__panel_title.innerHTML = data.title;
|
54
|
+
dokno__panel_summary.innerHTML = data.summary;
|
55
|
+
dokno__panel_footer.innerHTML = data.footer;
|
56
|
+
dokno__panel_markdown.innerHTML = data.markdown;
|
57
|
+
|
58
|
+
doknoRevealAppropriatePanelSections(data);
|
59
|
+
|
60
|
+
// Reveal panel
|
61
|
+
dokno__panel.scrollTop = 0;
|
62
|
+
dokno__panel.classList.add('open');
|
63
|
+
document.body.classList.add('dokno-no-scroll');
|
64
|
+
|
65
|
+
// Reveal fixed close icon
|
66
|
+
setTimeout(function() { dokno__close.classList.remove('dokno-hidden'); }, 200);
|
67
|
+
|
68
|
+
// Close on escape
|
69
|
+
document.addEventListener('keydown', dokno__keydown_listener, false);
|
70
|
+
}
|
71
|
+
};
|
72
|
+
|
73
|
+
request.send();
|
74
|
+
}
|
75
|
+
|
76
|
+
function doknoClosePanel() {
|
77
|
+
dokno__close.classList.add('dokno-hidden');
|
78
|
+
dokno__panel.classList.remove('open');
|
79
|
+
document.body.classList.remove('dokno-no-scroll');
|
80
|
+
document.removeEventListener('keydown', dokno__keydown_listener, false);
|
81
|
+
|
82
|
+
dokno__slug = null;
|
83
|
+
}
|
84
|
+
|
85
|
+
function doknoRevealAppropriatePanelSections(data) {
|
86
|
+
const markdown_classes = dokno__panel_markdown.classList;
|
87
|
+
const summary_classes = dokno__panel_summary.classList;
|
88
|
+
|
89
|
+
if (data.markdown != '') {
|
90
|
+
markdown_classes.remove('dokno-hidden');
|
91
|
+
if (data.summary == '') { summary_classes.add('dokno-hidden'); } else { summary_classes.remove('dokno-hidden'); }
|
92
|
+
} else {
|
93
|
+
markdown_classes.add('dokno-hidden');
|
94
|
+
summary_classes.remove('dokno-hidden');
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
const dokno__panel = document.getElementById('dokno-panel-container');
|
99
|
+
const dokno__close = document.querySelector('div#dokno-panel-close');
|
100
|
+
const dokno__panel_title = document.getElementById('dokno-panel-title');
|
101
|
+
const dokno__panel_summary = document.getElementById('dokno-panel-summary');
|
102
|
+
const dokno__panel_footer = document.getElementById('dokno-panel-footer');
|
103
|
+
const dokno__panel_markdown = document.getElementById('dokno-panel-markdown');
|
104
|
+
|
105
|
+
const dokno__keydown_listener = function(e) {
|
106
|
+
if (e.key === 'Escape') { doknoClosePanel(); }
|
107
|
+
}
|
108
|
+
|
109
|
+
dokno__close.onclick = function() { doknoClosePanel(); }
|
110
|
+
|
111
|
+
var dokno__id, dokno__slug;
|
112
|
+
</script>
|
@@ -0,0 +1,95 @@
|
|
1
|
+
<style>
|
2
|
+
.dokno-no-scroll {
|
3
|
+
overflow: hidden !important;
|
4
|
+
}
|
5
|
+
|
6
|
+
.dokno-hidden {
|
7
|
+
display: none !important;
|
8
|
+
}
|
9
|
+
|
10
|
+
div#dokno-panel-container {
|
11
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !important;
|
12
|
+
font-size: 1rem;
|
13
|
+
line-height: 1.5rem;
|
14
|
+
right: -600px;
|
15
|
+
z-index: 9999;
|
16
|
+
position: fixed;
|
17
|
+
top: 0;
|
18
|
+
bottom: 0;
|
19
|
+
width: 600px;
|
20
|
+
max-width: 100vw;
|
21
|
+
overflow: hidden;
|
22
|
+
overflow-y: auto;
|
23
|
+
background-color: #fff;
|
24
|
+
color: #2d3748;
|
25
|
+
transition: all .15s ease-in-out;
|
26
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
27
|
+
}
|
28
|
+
|
29
|
+
div#dokno-panel-container.open {
|
30
|
+
right: 0;
|
31
|
+
}
|
32
|
+
|
33
|
+
div#dokno-panel-close {
|
34
|
+
position: fixed;
|
35
|
+
background-color: transparent;
|
36
|
+
color: #222;
|
37
|
+
cursor: pointer;
|
38
|
+
top: 1.5rem;
|
39
|
+
right: 2.5rem;
|
40
|
+
}
|
41
|
+
|
42
|
+
div#dokno-panel-title {
|
43
|
+
padding: 2.5rem;
|
44
|
+
font-size: 2.25rem;
|
45
|
+
line-height: 2.75rem;
|
46
|
+
font-weight: 300;
|
47
|
+
background-color: rgb(42,67,101);
|
48
|
+
color: #ebf8ff;
|
49
|
+
}
|
50
|
+
|
51
|
+
div#dokno-panel-title div#article-deprecated-alert {
|
52
|
+
font-size: 1rem;
|
53
|
+
line-height: 1.5rem;
|
54
|
+
padding: 1rem;
|
55
|
+
margin-bottom: 1rem;
|
56
|
+
border-top-width: 4px;
|
57
|
+
border-radius: .25rem;
|
58
|
+
border-top-style: solid;
|
59
|
+
border-color: rgb(116,66,16);
|
60
|
+
background-color: rgb(183,121,31);
|
61
|
+
color: rgb(255,255,255);
|
62
|
+
}
|
63
|
+
|
64
|
+
div#dokno-panel-title > span,
|
65
|
+
div#dokno-panel-footer > svg {
|
66
|
+
cursor: pointer;
|
67
|
+
}
|
68
|
+
|
69
|
+
div#dokno-panel-summary {
|
70
|
+
font-size: 1.5rem;
|
71
|
+
line-height: 2.25rem;
|
72
|
+
padding: 2.5rem;
|
73
|
+
font-weight: 300;
|
74
|
+
}
|
75
|
+
|
76
|
+
div#dokno-panel-markdown {
|
77
|
+
padding: 2.5rem;
|
78
|
+
background-color: #f7fafc;
|
79
|
+
}
|
80
|
+
|
81
|
+
div#dokno-panel-footer {
|
82
|
+
text-align: center;
|
83
|
+
position: relative;
|
84
|
+
padding: 2.5rem;
|
85
|
+
color: #999;
|
86
|
+
background-color: #edf2f7;
|
87
|
+
font-weight: 300;
|
88
|
+
font-size: 1rem;
|
89
|
+
line-height: 1.5rem;
|
90
|
+
}
|
91
|
+
|
92
|
+
div#dokno-panel-footer p {
|
93
|
+
margin: .5rem 0;
|
94
|
+
}
|
95
|
+
</style>
|
@@ -0,0 +1,55 @@
|
|
1
|
+
<style>
|
2
|
+
/* Resets */
|
3
|
+
.dokno-article-content-markup blockquote,
|
4
|
+
.dokno-article-content-markup dl,
|
5
|
+
.dokno-article-content-markup dd,
|
6
|
+
.dokno-article-content-markup h1,
|
7
|
+
.dokno-article-content-markup h2,
|
8
|
+
.dokno-article-content-markup h3,
|
9
|
+
.dokno-article-content-markup h4,
|
10
|
+
.dokno-article-content-markup h5,
|
11
|
+
.dokno-article-content-markup h6,
|
12
|
+
.dokno-article-content-markup hr,
|
13
|
+
.dokno-article-content-markup figure,
|
14
|
+
.dokno-article-content-markup p,
|
15
|
+
.dokno-article-content-markup pre {
|
16
|
+
margin: 0;
|
17
|
+
}
|
18
|
+
|
19
|
+
.dokno-article-content-markup h1,
|
20
|
+
.dokno-article-content-markup h2,
|
21
|
+
.dokno-article-content-markup h3,
|
22
|
+
.dokno-article-content-markup h4,
|
23
|
+
.dokno-article-content-markup h5,
|
24
|
+
.dokno-article-content-markup h6 {
|
25
|
+
font-size: inherit;
|
26
|
+
font-weight: inherit;
|
27
|
+
}
|
28
|
+
|
29
|
+
.dokno-article-content-markup ol,
|
30
|
+
.dokno-article-content-markup ul {
|
31
|
+
list-style: none;
|
32
|
+
margin: 0;
|
33
|
+
padding: 0;
|
34
|
+
}
|
35
|
+
|
36
|
+
.dokno-article-content-markup img,
|
37
|
+
.dokno-article-content-markup svg,
|
38
|
+
.dokno-article-content-markup video,
|
39
|
+
.dokno-article-content-markup canvas,
|
40
|
+
.dokno-article-content-markup audio,
|
41
|
+
.dokno-article-content-markup iframe,
|
42
|
+
.dokno-article-content-markup embed,
|
43
|
+
.dokno-article-content-markup object {
|
44
|
+
display: block;
|
45
|
+
vertical-align: middle;
|
46
|
+
}
|
47
|
+
|
48
|
+
.dokno-article-content-markup *,
|
49
|
+
.dokno-article-content-markup ::before,
|
50
|
+
.dokno-article-content-markup ::after {
|
51
|
+
border-width: 0;
|
52
|
+
border-style: solid;
|
53
|
+
border-color: currentColor;
|
54
|
+
}
|
55
|
+
</style>
|