katalyst-content 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +95 -0
- data/app/assets/config/katalyst-content.js +1 -0
- data/app/assets/javascripts/controllers/content/editor/container_controller.js +113 -0
- data/app/assets/javascripts/controllers/content/editor/item_controller.js +45 -0
- data/app/assets/javascripts/controllers/content/editor/list_controller.js +105 -0
- data/app/assets/javascripts/controllers/content/editor/new_item_controller.js +12 -0
- data/app/assets/javascripts/controllers/content/editor/status_bar_controller.js +22 -0
- data/app/assets/javascripts/utils/content/editor/container.js +52 -0
- data/app/assets/javascripts/utils/content/editor/item.js +245 -0
- data/app/assets/javascripts/utils/content/editor/rules-engine.js +177 -0
- data/app/assets/stylesheets/katalyst/content/_index.scss +31 -0
- data/app/assets/stylesheets/katalyst/content/editor/_icon.scss +17 -0
- data/app/assets/stylesheets/katalyst/content/editor/_index.scss +145 -0
- data/app/assets/stylesheets/katalyst/content/editor/_item-actions.scss +93 -0
- data/app/assets/stylesheets/katalyst/content/editor/_item-rules.scss +19 -0
- data/app/assets/stylesheets/katalyst/content/editor/_new-items.scss +39 -0
- data/app/assets/stylesheets/katalyst/content/editor/_status-bar.scss +87 -0
- data/app/controllers/katalyst/content/application_controller.rb +8 -0
- data/app/controllers/katalyst/content/items_controller.rb +70 -0
- data/app/helpers/katalyst/content/application_helper.rb +8 -0
- data/app/helpers/katalyst/content/editor/base.rb +44 -0
- data/app/helpers/katalyst/content/editor/container.rb +41 -0
- data/app/helpers/katalyst/content/editor/item.rb +67 -0
- data/app/helpers/katalyst/content/editor/list.rb +41 -0
- data/app/helpers/katalyst/content/editor/new_item.rb +53 -0
- data/app/helpers/katalyst/content/editor/status_bar.rb +57 -0
- data/app/helpers/katalyst/content/editor_helper.rb +42 -0
- data/app/models/concerns/katalyst/content/container.rb +100 -0
- data/app/models/concerns/katalyst/content/garbage_collection.rb +31 -0
- data/app/models/concerns/katalyst/content/has_tree.rb +63 -0
- data/app/models/concerns/katalyst/content/version.rb +33 -0
- data/app/models/katalyst/content/content.rb +21 -0
- data/app/models/katalyst/content/item.rb +36 -0
- data/app/models/katalyst/content/node.rb +21 -0
- data/app/models/katalyst/content/types/nodes_type.rb +42 -0
- data/app/views/active_storage/blobs/_blob.html.erb +14 -0
- data/app/views/katalyst/content/contents/_content.html+form.erb +39 -0
- data/app/views/katalyst/content/contents/_content.html.erb +5 -0
- data/app/views/katalyst/content/editor/_item.html.erb +11 -0
- data/app/views/katalyst/content/editor/_list_item.html.erb +14 -0
- data/app/views/katalyst/content/editor/_new_item.html.erb +3 -0
- data/app/views/katalyst/content/editor/_new_items.html.erb +5 -0
- data/app/views/katalyst/content/items/_item.html+form.erb +34 -0
- data/app/views/katalyst/content/items/_item.html.erb +3 -0
- data/app/views/katalyst/content/items/edit.html.erb +4 -0
- data/app/views/katalyst/content/items/new.html.erb +4 -0
- data/app/views/katalyst/content/items/update.turbo_stream.erb +7 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/config/importmap.rb +8 -0
- data/config/locales/en.yml +12 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20220913003839_create_katalyst_content_items.rb +17 -0
- data/lib/katalyst/content/config.rb +18 -0
- data/lib/katalyst/content/engine.rb +36 -0
- data/lib/katalyst/content/version.rb +7 -0
- data/lib/katalyst/content.rb +19 -0
- data/lib/tasks/yarn.rake +18 -0
- data/spec/factories/katalyst/content/items.rb +16 -0
- metadata +103 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
@use "icon" as *;
|
2
|
+
|
3
|
+
[data-controller="content--editor--item"] {
|
4
|
+
[data-tree-accordion-controls] {
|
5
|
+
min-width: 2.5rem;
|
6
|
+
min-height: 2.5rem;
|
7
|
+
display: grid;
|
8
|
+
}
|
9
|
+
|
10
|
+
[data-tree-controls] {
|
11
|
+
display: flex;
|
12
|
+
align-items: center;
|
13
|
+
}
|
14
|
+
|
15
|
+
[data-tree-accordion-controls],
|
16
|
+
[data-tree-controls] {
|
17
|
+
[role="button"] {
|
18
|
+
@extend %icon-block;
|
19
|
+
&::before {
|
20
|
+
@extend %icon;
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
[role="img"][value="link"],
|
26
|
+
[role="img"][value="title"] {
|
27
|
+
width: 1.5rem;
|
28
|
+
height: 1.5rem;
|
29
|
+
display: grid;
|
30
|
+
place-items: center;
|
31
|
+
border-radius: 2px;
|
32
|
+
background: var(--icon-active-color);
|
33
|
+
margin-right: 0.5rem;
|
34
|
+
flex-shrink: 0;
|
35
|
+
|
36
|
+
[data-invisible="true"] & {
|
37
|
+
background: var(--icon-passive-color);
|
38
|
+
}
|
39
|
+
|
40
|
+
&::before {
|
41
|
+
@extend %icon;
|
42
|
+
color: white;
|
43
|
+
font-size: 1.125rem;
|
44
|
+
line-height: 1.125rem;
|
45
|
+
text-align: center;
|
46
|
+
}
|
47
|
+
|
48
|
+
&[value="link"] {
|
49
|
+
&::before {
|
50
|
+
content: "#";
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
&[value="title"] {
|
55
|
+
&::before {
|
56
|
+
content: "T";
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
[role="img"][value="invisible"] {
|
62
|
+
@extend %icon-block;
|
63
|
+
|
64
|
+
&::before {
|
65
|
+
@extend %icon;
|
66
|
+
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_58_2189)'%3E%3Cpath d='M22.1699 0.329991C21.7304 -0.109509 21.0179 -0.109509 20.5784 0.329991L15.8399 5.06849C14.6219 4.69949 13.3334 4.50149 11.9984 4.50149C6.76494 4.50149 2.22744 7.54949 -0.00156052 12.0015C0.962939 13.926 2.35794 15.588 4.05294 16.8555L0.326939 20.5815C-0.112561 21.021 -0.112561 21.7335 0.326939 22.173C0.545939 22.392 0.833939 22.503 1.12194 22.503C1.40994 22.503 1.69794 22.3935 1.91694 22.173L22.1669 1.92299C22.6064 1.48349 22.6064 0.770991 22.1669 0.331491L22.1699 0.329991ZM9.74994 7.49999C10.7399 7.49999 11.5799 8.13899 11.8814 9.02849L9.02844 11.8815C8.14044 11.58 7.49994 10.74 7.49994 9.74999C7.49994 8.50799 8.50794 7.49999 9.74994 7.49999ZM2.58144 12C3.47844 10.581 4.67394 9.37649 6.08394 8.47799C6.17544 8.41949 6.26844 8.36249 6.36144 8.30699C6.12744 8.94749 5.99994 9.63899 5.99994 10.3605C5.99994 11.6475 6.40494 12.8385 7.09494 13.815L5.66694 15.243C4.43844 14.379 3.38844 13.2765 2.58144 12V12Z' fill='%2358607A'/%3E%3Cpath d='M18.0001 10.3589C18.0001 9.72295 17.9011 9.10945 17.7166 8.53345L10.1746 16.0754C10.7506 16.2599 11.3641 16.3589 12.0001 16.3589C15.3136 16.3589 18.0001 13.6724 18.0001 10.3589V10.3589Z' fill='%2358607A'/%3E%3Cpath d='M19.4536 6.79651L17.8276 8.42251C17.8576 8.44051 17.8876 8.45851 17.9161 8.47801C19.3261 9.37801 20.5216 10.5825 21.4186 12C20.5216 13.419 19.3261 14.6235 17.9161 15.522C16.1446 16.6515 14.0986 17.25 12.0001 17.25C11.0941 17.25 10.1971 17.139 9.3286 16.9215L7.5271 18.723C8.9266 19.2255 10.4326 19.5 12.0001 19.5C17.2336 19.5 21.7711 16.452 24.0001 12C22.9456 9.89251 21.3721 8.10001 19.4536 6.79651V6.79651Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_58_2189'%3E%3Crect width='24' height='24' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
[role="button"][value="collapse"]::before {
|
71
|
+
background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1454)'%3E%3Cpath d='M15 8.33333L13.825 7.15833L10 10.975L6.175 7.15833L5 8.33333L10 13.3333L15 8.33333Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1454'%3E%3Crect width='20' height='20' fill='white' transform='translate(20) rotate(90)'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
|
72
|
+
}
|
73
|
+
|
74
|
+
[role="button"][value="expand"]::before {
|
75
|
+
background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1454)'%3E%3Cpath d='M8.3332 5L7.1582 6.175L10.9749 10L7.1582 13.825L8.3332 15L13.3332 10L8.3332 5Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1454'%3E%3Crect width='20' height='20' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
|
76
|
+
}
|
77
|
+
|
78
|
+
[role="button"][value="de-nest"]::before {
|
79
|
+
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1454)'%3E%3Cpath d='M11.6668 15L12.8418 13.825L9.02513 10L12.8418 6.175L11.6668 5L6.6668 10L11.6668 15Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1454'%3E%3Crect width='20' height='20' fill='white' transform='translate(20 20) rotate(180)'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
|
80
|
+
}
|
81
|
+
|
82
|
+
[role="button"][value="nest"]::before {
|
83
|
+
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1454)'%3E%3Cpath d='M8.3332 5L7.1582 6.175L10.9749 10L7.1582 13.825L8.3332 15L13.3332 10L8.3332 5Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1454'%3E%3Crect width='20' height='20' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
|
84
|
+
}
|
85
|
+
|
86
|
+
[role="button"][value="edit"]::before {
|
87
|
+
background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1406)'%3E%3Cpath d='M2.25 12.9375V15.75H5.0625L13.3575 7.45504L10.545 4.64254L2.25 12.9375ZM15.5325 5.28004C15.825 4.98754 15.825 4.51504 15.5325 4.22254L13.7775 2.46754C13.485 2.17504 13.0125 2.17504 12.72 2.46754L11.3475 3.84004L14.16 6.65254L15.5325 5.28004V5.28004Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1406'%3E%3Crect width='18' height='18' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
|
88
|
+
}
|
89
|
+
|
90
|
+
[role="button"][value="remove"]::before {
|
91
|
+
background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1409)'%3E%3Cpath d='M4.5 14.25C4.5 15.075 5.175 15.75 6 15.75H12C12.825 15.75 13.5 15.075 13.5 14.25V5.25H4.5V14.25ZM14.25 3H11.625L10.875 2.25H7.125L6.375 3H3.75V4.5H14.25V3Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1409'%3E%3Crect width='18' height='18' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
|
92
|
+
}
|
93
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
[data-controller="content--editor--list"] {
|
2
|
+
// Lower opacity for buttons you can't use
|
3
|
+
[data-deny-de-nest] [role="button"][value="de-nest"],
|
4
|
+
[data-deny-nest] [role="button"][value="nest"],
|
5
|
+
[data-deny-remove] [role="button"][value="remove"],
|
6
|
+
[data-deny-drag] [role="button"][value="drag"],
|
7
|
+
[data-deny-edit] [role="button"][value="edit"] {
|
8
|
+
opacity: 0.2;
|
9
|
+
pointer-events: none;
|
10
|
+
}
|
11
|
+
|
12
|
+
// Only show 1 of the collapse / expand button
|
13
|
+
[data-deny-collapse] [role="button"][value="collapse"],
|
14
|
+
[data-deny-expand] [role="button"][value="expand"],
|
15
|
+
[data-invisible="false"] [role="img"][value="invisible"] {
|
16
|
+
display: none !important;
|
17
|
+
pointer-events: none;
|
18
|
+
}
|
19
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
@use "icon" as *;
|
2
|
+
|
3
|
+
.content--editor--new-items {
|
4
|
+
display: grid;
|
5
|
+
grid-template-columns: repeat(3, 1fr);
|
6
|
+
gap: 0.5rem;
|
7
|
+
|
8
|
+
[role="listitem"] {
|
9
|
+
display: flex;
|
10
|
+
flex-direction: column;
|
11
|
+
justify-content: center;
|
12
|
+
align-items: center;
|
13
|
+
transform: translate3d(0, 0, 0);
|
14
|
+
cursor: grab;
|
15
|
+
background: white;
|
16
|
+
box-shadow: rgb(0 0 0 / 25%) 0 1px 1px, rgb(0 0 0 / 31%) 0 0 2px;
|
17
|
+
min-height: 5rem;
|
18
|
+
padding: 1.5rem;
|
19
|
+
|
20
|
+
&:hover {
|
21
|
+
box-shadow: rgb(0 0 0 / 25%) 0 1px 2px, rgb(0 0 0 / 31%) 0 0 5px;
|
22
|
+
}
|
23
|
+
|
24
|
+
&::before {
|
25
|
+
@extend %icon;
|
26
|
+
width: 2.5rem;
|
27
|
+
height: 2.5rem;
|
28
|
+
position: unset;
|
29
|
+
}
|
30
|
+
|
31
|
+
&[data-item-type="link"]:before {
|
32
|
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 48 48' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_67_1273)'%3E%3Cpath d='M16 22H32V26H16V22ZM40.2 24H44C44 18.48 39.52 14 34 14H26V17.8H34C37.42 17.8 40.2 20.58 40.2 24ZM7.8 24C7.8 20.58 10.58 17.8 14 17.8H22V14H14C8.48 14 4 18.48 4 24C4 29.52 8.48 34 14 34H22V30.2H14C10.58 30.2 7.8 27.42 7.8 24ZM38 24H34V30H28V34H34V40H38V34H44V30H38V24Z' /%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_67_1273'%3E%3Crect width='48' height='48' /%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
|
33
|
+
}
|
34
|
+
|
35
|
+
&[data-item-type="button"]:before {
|
36
|
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 48 48' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_67_1276)'%3E%3Cpath d='M44 18V30C44 32.2 42.2 34 40 34H38V30H40V18H8V30H20V34H8C5.8 34 4 32.2 4 30V18C4 15.8 5.8 14 8 14H40C42.2 14 44 15.8 44 18ZM29 38L31.18 33.18L36 31L31.18 28.82L29 24L26.82 28.82L22 31L26.82 33.18L29 38ZM34 28L35.24 25.24L38 24L35.24 22.76L34 20L32.76 22.76L30 24L32.76 25.24L34 28ZM29 38L31.18 33.18L36 31L31.18 28.82L29 24L26.82 28.82L22 31L26.82 33.18L29 38ZM34 28L35.24 25.24L38 24L35.24 22.76L34 20L32.76 22.76L30 24L32.76 25.24L34 28Z' /%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_67_1276'%3E%3Crect width='48' height='48' /%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
@@ -0,0 +1,87 @@
|
|
1
|
+
[data-controller="content--editor--status-bar"] {
|
2
|
+
min-height: 3rem;
|
3
|
+
line-height: 3rem;
|
4
|
+
padding: 0 1.25rem 0 1.5rem;
|
5
|
+
background: var(--background);
|
6
|
+
color: var(--color);
|
7
|
+
border: 1px solid var(--border);
|
8
|
+
margin-bottom: 1rem;
|
9
|
+
|
10
|
+
display: grid;
|
11
|
+
grid-template-columns: 1fr auto;
|
12
|
+
grid-template-areas: "status actions";
|
13
|
+
align-items: baseline;
|
14
|
+
grid-column-gap: 2rem;
|
15
|
+
|
16
|
+
.status-text {
|
17
|
+
display: none;
|
18
|
+
grid-area: status;
|
19
|
+
font-weight: bold;
|
20
|
+
}
|
21
|
+
|
22
|
+
&[data-state="published"] .status-text[data-published],
|
23
|
+
&[data-state="draft"] .status-text[data-draft],
|
24
|
+
&[data-state="dirty"] .status-text[data-dirty] {
|
25
|
+
display: unset;
|
26
|
+
}
|
27
|
+
|
28
|
+
menu {
|
29
|
+
display: inline;
|
30
|
+
grid-area: actions;
|
31
|
+
margin: 0;
|
32
|
+
padding: 0;
|
33
|
+
}
|
34
|
+
|
35
|
+
menu > li {
|
36
|
+
display: inline;
|
37
|
+
}
|
38
|
+
|
39
|
+
.button {
|
40
|
+
color: inherit;
|
41
|
+
line-height: 1rem;
|
42
|
+
margin-left: 0.5rem;
|
43
|
+
}
|
44
|
+
|
45
|
+
.button--primary {
|
46
|
+
background: var(--color);
|
47
|
+
border: 1px solid var(--border);
|
48
|
+
color: white;
|
49
|
+
|
50
|
+
&[disabled] {
|
51
|
+
background: var(--color);
|
52
|
+
opacity: 0.8;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
.button--secondary {
|
57
|
+
background: var(--background);
|
58
|
+
border: 1px solid var(--border);
|
59
|
+
color: var(--color);
|
60
|
+
|
61
|
+
&[disabled] {
|
62
|
+
background: var(--background);
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
&[data-state="published"] {
|
67
|
+
[value="publish"],
|
68
|
+
[value="save"],
|
69
|
+
[value="revert"],
|
70
|
+
[value="discard"] {
|
71
|
+
display: none;
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
&[data-state="draft"] {
|
76
|
+
[value="save"],
|
77
|
+
[value="discard"] {
|
78
|
+
display: none;
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
&[data-state="dirty"] {
|
83
|
+
[value="revert"] {
|
84
|
+
display: none;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
}
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
class ItemsController < ApplicationController
|
6
|
+
before_action :set_container, only: %i[new create]
|
7
|
+
before_action :set_item, except: %i[new create]
|
8
|
+
before_action :set_editor_variant
|
9
|
+
|
10
|
+
helper EditorHelper
|
11
|
+
|
12
|
+
def new
|
13
|
+
render locals: { item: @container.items.build(item_params) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def create
|
17
|
+
item = @container.items.build(item_params)
|
18
|
+
if item.save
|
19
|
+
render :update, locals: { item: item, previous: @container.items.build(type: item.type) }
|
20
|
+
else
|
21
|
+
render :new, status: :unprocessable_entity, locals: { item: item }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def edit
|
26
|
+
render locals: { item: @item }
|
27
|
+
end
|
28
|
+
|
29
|
+
def update
|
30
|
+
@item.attributes = item_params
|
31
|
+
|
32
|
+
if @item.valid?
|
33
|
+
previous = @item
|
34
|
+
@item = @item.dup.tap(&:save!)
|
35
|
+
render locals: { item: @item, previous: previous }
|
36
|
+
else
|
37
|
+
render :edit, status: :unprocessable_entity, locals: { item: @item }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def item_params_type
|
44
|
+
type = params.require(:item).fetch(:type, "")
|
45
|
+
if Katalyst::Content.config.items.include?(type)
|
46
|
+
type.safe_constantize
|
47
|
+
else
|
48
|
+
Item
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def item_params
|
53
|
+
params.require(:item).permit(item_params_type.permitted_params)
|
54
|
+
end
|
55
|
+
|
56
|
+
def set_container
|
57
|
+
@container = Item.new(item_params).container
|
58
|
+
raise ActiveRecord::RecordNotFound unless @container
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_item
|
62
|
+
@item = Item.find(params[:id])
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_editor_variant
|
66
|
+
request.variant << :form
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
module Editor
|
6
|
+
class Base
|
7
|
+
CONTAINER_CONTROLLER = "content--editor--container"
|
8
|
+
LIST_CONTROLLER = "content--editor--list"
|
9
|
+
ITEM_CONTROLLER = "content--editor--item"
|
10
|
+
STATUS_BAR_CONTROLLER = "content--editor--status-bar"
|
11
|
+
NEW_ITEM_CONTROLLER = "content--editor--new-item"
|
12
|
+
|
13
|
+
TURBO_FRAME = "content--editor--item-frame"
|
14
|
+
|
15
|
+
attr_accessor :template, :container
|
16
|
+
|
17
|
+
delegate_missing_to :template
|
18
|
+
|
19
|
+
def initialize(template, container)
|
20
|
+
self.template = template
|
21
|
+
self.container = container
|
22
|
+
end
|
23
|
+
|
24
|
+
def container_form_id
|
25
|
+
dom_id(container, :items)
|
26
|
+
end
|
27
|
+
|
28
|
+
def attributes_scope
|
29
|
+
"#{container.model_name.param_key}[items_attributes][]"
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def add_option(options, key, *path)
|
35
|
+
if path.length > 1
|
36
|
+
add_option(options[key] ||= {}, *path)
|
37
|
+
else
|
38
|
+
options[key] = [options[key], *path].compact.join(" ")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
module Editor
|
6
|
+
class Container < Base
|
7
|
+
ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
|
8
|
+
submit->#{CONTAINER_CONTROLLER}#reindex
|
9
|
+
content:reindex->#{CONTAINER_CONTROLLER}#reindex
|
10
|
+
content:reset->#{CONTAINER_CONTROLLER}#reset
|
11
|
+
ACTIONS
|
12
|
+
|
13
|
+
def build(options)
|
14
|
+
form_with(model: container, **default_options(id: container_form_id, **options)) do |form|
|
15
|
+
concat hidden_input
|
16
|
+
concat(capture { yield form })
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Hidden input ensures that if the container is empty then the controller
|
23
|
+
# receives an empty array.
|
24
|
+
def hidden_input
|
25
|
+
tag.input(type: "hidden", name: "#{attributes_scope}[id]")
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_options(options)
|
29
|
+
add_option(options, :data, :controller, CONTAINER_CONTROLLER)
|
30
|
+
add_option(options, :data, :action, ACTIONS)
|
31
|
+
|
32
|
+
# depth = options.delete(:depth) || container.depth
|
33
|
+
#
|
34
|
+
# add_option(options, :data, :"#{CONTAINER_CONTROLLER}-max-depth-value", depth) if depth
|
35
|
+
|
36
|
+
options
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
module Editor
|
6
|
+
class Item < Base
|
7
|
+
attr_accessor :item
|
8
|
+
|
9
|
+
def build(item, **options, &block)
|
10
|
+
self.item = item
|
11
|
+
tag.div **default_options(id: dom_id(item), **options) do
|
12
|
+
concat(capture { yield self }) if block
|
13
|
+
concat fields(item)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def accordion_actions
|
18
|
+
tag.div role: "toolbar", data: { tree_accordion_controls: "" } do
|
19
|
+
concat tag.span(role: "button", value: "collapse",
|
20
|
+
data: { action: "click->#{CONTAINER_CONTROLLER}#collapse", title: "Collapse tree" })
|
21
|
+
concat tag.span(role: "button", value: "expand",
|
22
|
+
data: { action: "click->#{CONTAINER_CONTROLLER}#expand", title: "Expand tree" })
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def item_actions
|
27
|
+
tag.div role: "toolbar", data: { tree_controls: "" } do
|
28
|
+
concat tag.span(role: "button", value: "de-nest",
|
29
|
+
data: { action: "click->#{CONTAINER_CONTROLLER}#deNest", title: "Outdent" })
|
30
|
+
concat tag.span(role: "button", value: "nest",
|
31
|
+
data: { action: "click->#{CONTAINER_CONTROLLER}#nest", title: "Indent" })
|
32
|
+
concat link_to("", edit_item_link,
|
33
|
+
role: "button", title: "Edit", value: "edit",
|
34
|
+
data: { turbo_frame: TURBO_FRAME })
|
35
|
+
concat tag.span(role: "button", value: "remove",
|
36
|
+
data: { action: "click->#{CONTAINER_CONTROLLER}#remove", title: "Remove" })
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def edit_item_link
|
41
|
+
if item.persisted?
|
42
|
+
katalyst_content.edit_item_path(item)
|
43
|
+
else
|
44
|
+
katalyst_content.new_item_path(item: item.attributes.slice("type", "container_id",
|
45
|
+
"container_type").compact)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def fields(item)
|
50
|
+
template.fields(attributes_scope, model: item, index: nil, skip_default_ids: true) do |f|
|
51
|
+
concat f.hidden_field(:id)
|
52
|
+
concat f.hidden_field(:depth)
|
53
|
+
concat f.hidden_field(:index)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def default_options(options)
|
60
|
+
add_option(options, :data, :controller, ITEM_CONTROLLER)
|
61
|
+
|
62
|
+
options
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
module Editor
|
6
|
+
class List < Base
|
7
|
+
ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
|
8
|
+
dragstart->#{LIST_CONTROLLER}#dragstart
|
9
|
+
dragover->#{LIST_CONTROLLER}#dragover
|
10
|
+
dragenter->#{LIST_CONTROLLER}#dragenter
|
11
|
+
dragleave->#{LIST_CONTROLLER}#dragleave
|
12
|
+
drop->#{LIST_CONTROLLER}#drop
|
13
|
+
dragend->#{LIST_CONTROLLER}#dragend
|
14
|
+
ACTIONS
|
15
|
+
|
16
|
+
def build(options, &_block)
|
17
|
+
tag.ol **default_options(id: container_form_id, **options) do
|
18
|
+
yield self
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def items(*items)
|
23
|
+
render partial: "katalyst/content/editor/item",
|
24
|
+
layout: "katalyst/content/editor/list_item",
|
25
|
+
collection: items,
|
26
|
+
as: :item
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def default_options(options)
|
32
|
+
add_option(options, :data, :controller, LIST_CONTROLLER)
|
33
|
+
add_option(options, :data, :action, ACTIONS)
|
34
|
+
add_option(options, :data, :"#{CONTAINER_CONTROLLER}_target", "container")
|
35
|
+
|
36
|
+
options
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
module Editor
|
6
|
+
class NewItem < Base
|
7
|
+
ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
|
8
|
+
dragstart->#{NEW_ITEM_CONTROLLER}#dragstart
|
9
|
+
ACTIONS
|
10
|
+
|
11
|
+
def build(item, **options, &block)
|
12
|
+
capture do
|
13
|
+
concat(tag.div(**default_options(options)) do
|
14
|
+
concat capture(&block)
|
15
|
+
concat item_template(item)
|
16
|
+
end)
|
17
|
+
concat turbo_replace_placeholder(item)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Remove items that are incomplete when rendering new items, this
|
22
|
+
# causes incomplete items to be removed from the list when the user
|
23
|
+
# cancels adding a new item by pressing 'discard' in the new item form.
|
24
|
+
def turbo_replace_placeholder(item)
|
25
|
+
turbo_stream.replace dom_id(item) do
|
26
|
+
content_editor_item(item: item, data: { delete: "" })
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Template is stored inside the new item dom, and copied into drag
|
31
|
+
# events when the user initiates drag so that it can be copied into the
|
32
|
+
# editor list on drop.
|
33
|
+
def item_template(item)
|
34
|
+
tag.template data: { "#{NEW_ITEM_CONTROLLER}-target" => "template" } do
|
35
|
+
content_editor_items(item: item)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def default_options(options)
|
42
|
+
add_option(options, :draggable, true)
|
43
|
+
add_option(options, :role, "listitem")
|
44
|
+
add_option(options, :data, :turbo_frame, TURBO_FRAME)
|
45
|
+
add_option(options, :data, :controller, NEW_ITEM_CONTROLLER)
|
46
|
+
add_option(options, :data, :action, ACTIONS)
|
47
|
+
|
48
|
+
options
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
module Editor
|
6
|
+
class StatusBar < Base
|
7
|
+
ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
|
8
|
+
content:change@document->#{STATUS_BAR_CONTROLLER}#change
|
9
|
+
ACTIONS
|
10
|
+
|
11
|
+
def build(**options)
|
12
|
+
tag.div **default_options(**options) do
|
13
|
+
concat status(:published, last_update: l(container.updated_at, format: :short))
|
14
|
+
concat status(:draft)
|
15
|
+
concat status(:dirty)
|
16
|
+
concat actions
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def status(state, **options)
|
21
|
+
tag.span(t("views.katalyst.content.editor.#{state}_html", **options),
|
22
|
+
class: "status-text",
|
23
|
+
data: { state => "" })
|
24
|
+
end
|
25
|
+
|
26
|
+
def actions
|
27
|
+
tag.menu do
|
28
|
+
concat action(:discard, class: "button button--text")
|
29
|
+
concat action(:revert, class: "button button--text") if container.state == :draft
|
30
|
+
concat action(:save, class: "button button--secondary")
|
31
|
+
concat action(:publish, class: "button button--primary")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def action(action, **options)
|
36
|
+
tag.li do
|
37
|
+
button_tag(t("views.katalyst.content.editor.#{action}"),
|
38
|
+
name: "commit",
|
39
|
+
value: action,
|
40
|
+
form: container_form_id,
|
41
|
+
**options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def default_options(**options)
|
48
|
+
add_option(options, :data, :controller, STATUS_BAR_CONTROLLER)
|
49
|
+
add_option(options, :data, :action, ACTIONS)
|
50
|
+
add_option(options, :data, :state, container.state)
|
51
|
+
|
52
|
+
options
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
module EditorHelper
|
6
|
+
def content_editor_new_items(container:)
|
7
|
+
Katalyst::Content.config.items.map do |item_class|
|
8
|
+
item_class = item_class.is_a?(String) ? item_class.safe_constantize : item_class
|
9
|
+
item_class.new(container: container)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def content_editor_container(container:, **options, &block)
|
14
|
+
Editor::Container.new(self, container).build(options, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def content_editor_list(container:, items: container.draft_items, **options)
|
18
|
+
Editor::List.new(self, container).build(options) do |list|
|
19
|
+
list.items(*items) if items.present?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Generate items without their list wrapper, similar to form_with/fields
|
24
|
+
def content_editor_items(item:, container: item.container)
|
25
|
+
Editor::List.new(self, container).items(item)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Gene
|
29
|
+
def content_editor_new_item(item:, container: item.container, **options, &block)
|
30
|
+
Editor::NewItem.new(self, container).build(item, **options, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def content_editor_item(item:, container: item.container, **options, &block)
|
34
|
+
Editor::Item.new(self, container).build(item, **options, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def content_editor_status_bar(container:, **options)
|
38
|
+
Editor::StatusBar.new(self, container).build(**options)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|