katalyst-content 2.2.0 → 2.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/builds/katalyst/content.esm.js +72 -1
- data/app/assets/builds/katalyst/content.js +72 -1
- data/app/assets/builds/katalyst/content.min.js +1 -1
- data/app/assets/builds/katalyst/content.min.js.map +1 -1
- data/app/assets/stylesheets/katalyst/content/editor/_index.scss +3 -27
- data/app/assets/stylesheets/katalyst/content/editor/_item-actions.scss +4 -0
- data/app/assets/stylesheets/katalyst/content/editor/_new-items.scss +4 -0
- data/app/assets/stylesheets/katalyst/content/editor/_table.scss +42 -0
- data/app/assets/stylesheets/katalyst/content/editor/_variables.scss +26 -0
- data/app/components/katalyst/content/editor/item_editor_component.rb +4 -0
- data/app/controllers/katalyst/content/items_controller.rb +5 -0
- data/app/controllers/katalyst/content/tables_controller.rb +24 -0
- data/app/helpers/katalyst/content/editor_helper.rb +2 -0
- data/app/helpers/katalyst/content/frontend_helper.rb +2 -0
- data/app/helpers/katalyst/content/table_helper.rb +143 -0
- data/app/javascript/content/application.js +5 -0
- data/app/javascript/content/editor/list_controller.js +2 -1
- data/app/javascript/content/editor/table_controller.js +67 -0
- data/app/models/katalyst/content/content.rb +2 -0
- data/app/models/katalyst/content/table.rb +55 -0
- data/app/models/katalyst/content/tables/importer.rb +151 -0
- data/app/views/katalyst/content/items/edit.html.erb +5 -0
- data/app/views/katalyst/content/tables/_table.html+form.erb +48 -0
- data/app/views/katalyst/content/tables/_table.html.erb +3 -0
- data/app/views/katalyst/content/tables/update.turbo_stream.erb +3 -0
- data/config/locales/en.yml +2 -0
- data/config/routes.rb +1 -0
- data/lib/katalyst/content/config.rb +9 -0
- data/spec/factories/katalyst/content/items.rb +22 -0
- metadata +12 -2
@@ -0,0 +1,42 @@
|
|
1
|
+
@use "variables" as *;
|
2
|
+
|
3
|
+
[data-content--editor--table-target="content"] {
|
4
|
+
position: relative;
|
5
|
+
min-height: 8rem;
|
6
|
+
|
7
|
+
&::after {
|
8
|
+
content: "Paste table here";
|
9
|
+
display: block;
|
10
|
+
position: absolute;
|
11
|
+
top: 50%;
|
12
|
+
left: 50%;
|
13
|
+
transform: translate(-50%, -50%);
|
14
|
+
opacity: 0.5;
|
15
|
+
}
|
16
|
+
|
17
|
+
&:has(table)::after {
|
18
|
+
content: unset;
|
19
|
+
}
|
20
|
+
|
21
|
+
table {
|
22
|
+
border-collapse: collapse;
|
23
|
+
max-width: 100%;
|
24
|
+
overflow: hidden;
|
25
|
+
}
|
26
|
+
|
27
|
+
th,
|
28
|
+
td {
|
29
|
+
border: 1px solid $grey;
|
30
|
+
padding: 0.25rem 0.5rem;
|
31
|
+
text-align: left;
|
32
|
+
vertical-align: top;
|
33
|
+
}
|
34
|
+
|
35
|
+
thead {
|
36
|
+
background-color: $grey;
|
37
|
+
}
|
38
|
+
|
39
|
+
tbody th {
|
40
|
+
background-color: $grey-light;
|
41
|
+
}
|
42
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
$grey-light: #f4f4f4 !default;
|
2
|
+
$grey: #ececec !default;
|
3
|
+
$grey-dark: #999 !default;
|
4
|
+
$table-hover-background: #fff0eb !default;
|
5
|
+
$primary-color: #ff521f !default;
|
6
|
+
|
7
|
+
$row-inset: 2rem !default;
|
8
|
+
$row-height: 3rem !default;
|
9
|
+
|
10
|
+
$table-header-color: $grey !default;
|
11
|
+
$row-background-color: $grey-light !default;
|
12
|
+
$row-hover-color: $table-hover-background !default;
|
13
|
+
$icon-active-color: $primary-color !default;
|
14
|
+
$icon-passive-color: $grey-dark !default;
|
15
|
+
|
16
|
+
$status-published-background-color: #ebf9eb !default;
|
17
|
+
$status-published-border-color: #4dd45c !default;
|
18
|
+
$status-published-color: #4dd45c !default;
|
19
|
+
|
20
|
+
$status-draft-background-color: #fefaf3 !default;
|
21
|
+
$status-draft-border-color: #ffa800 !default;
|
22
|
+
$status-draft-color: #ffa800 !default;
|
23
|
+
|
24
|
+
$status-dirty-background-color: #eee !default;
|
25
|
+
$status-dirty-border-color: #888 !default;
|
26
|
+
$status-dirty-color: #aaa !default;
|
@@ -6,6 +6,7 @@ module Katalyst
|
|
6
6
|
before_action :set_container, only: %i[new create]
|
7
7
|
before_action :set_item, except: %i[new create]
|
8
8
|
before_action :set_editor_variant
|
9
|
+
before_action :require_kpop, only: %i[new edit]
|
9
10
|
|
10
11
|
attr_reader :container, :item, :editor
|
11
12
|
|
@@ -103,6 +104,10 @@ module Katalyst
|
|
103
104
|
def prefix_partial_path_with_controller_namespace
|
104
105
|
false
|
105
106
|
end
|
107
|
+
|
108
|
+
def kpop_fallback_location
|
109
|
+
main_app.root_path
|
110
|
+
end
|
106
111
|
end
|
107
112
|
end
|
108
113
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
class TablesController < ItemsController
|
6
|
+
def update
|
7
|
+
item.attributes = item_params
|
8
|
+
|
9
|
+
if item.valid?
|
10
|
+
render :update, locals: { item_editor: }
|
11
|
+
else
|
12
|
+
render :update, locals: { item_editor: }, status: :unprocessable_entity
|
13
|
+
end
|
14
|
+
end
|
15
|
+
alias_method :create, :update
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def item_editor
|
20
|
+
editor.item_editor(item:)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module Katalyst
|
4
4
|
module Content
|
5
5
|
module FrontendHelper
|
6
|
+
include TableHelper
|
7
|
+
|
6
8
|
# Render all items from a content version as HTML
|
7
9
|
# @param version [Katalyst::Content::Version] The content version to render
|
8
10
|
# @return [ActiveSupport::SafeBuffer,String,nil] Content as HTML
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
module TableHelper
|
6
|
+
mattr_accessor(:sanitizer, default: Rails::HTML5::Sanitizer.safe_list_sanitizer.new)
|
7
|
+
mattr_accessor(:scrubber)
|
8
|
+
|
9
|
+
# Normalize table content and render to an HTML string (un-sanitised).
|
10
|
+
#
|
11
|
+
# @param table [Katalyst::Content::Table]
|
12
|
+
# @param heading [Boolean] whether to add the heading as a caption
|
13
|
+
# @return [String] un-sanitised HTML as text
|
14
|
+
def normalize_content_table(table, heading: true)
|
15
|
+
TableNormalizer.new(table, heading:).to_html
|
16
|
+
end
|
17
|
+
|
18
|
+
# Sanitize table content and render to an HTML string (un-sanitised).
|
19
|
+
#
|
20
|
+
# @param content [String] un-sanitised HTML as text
|
21
|
+
# @return [ActiveSupport::SafeBuffer] sanitised HTML
|
22
|
+
def sanitize_content_table(content)
|
23
|
+
sanitizer.sanitize(
|
24
|
+
content,
|
25
|
+
tags: content_table_allowed_tags,
|
26
|
+
attributes: content_table_allowed_attributes,
|
27
|
+
scrubber:,
|
28
|
+
).html_safe # rubocop:disable Rails/OutputSafety
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def content_table_allowed_tags
|
34
|
+
Katalyst::Content::Config.table_sanitizer_allowed_tags
|
35
|
+
end
|
36
|
+
|
37
|
+
def content_table_allowed_attributes
|
38
|
+
Katalyst::Content::Config.table_sanitizer_allowed_attributes
|
39
|
+
end
|
40
|
+
|
41
|
+
# rubocop:disable Rails/HelperInstanceVariable
|
42
|
+
class TableNormalizer
|
43
|
+
attr_reader :node, :heading_rows, :heading_columns
|
44
|
+
|
45
|
+
delegate_missing_to :@node
|
46
|
+
|
47
|
+
def initialize(table, heading: true)
|
48
|
+
@table = table
|
49
|
+
@heading = heading
|
50
|
+
|
51
|
+
root = ActionText::Fragment.from_html(table.content&.body&.to_html || "").source
|
52
|
+
@node = root.name == "table" ? root : root.at_css("table")
|
53
|
+
|
54
|
+
return unless @node
|
55
|
+
|
56
|
+
@heading_rows = @table.heading_rows.clamp(0..css("tr").length)
|
57
|
+
@heading_columns = @table.heading_columns.clamp(0..)
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_caption!
|
61
|
+
return if at_css("caption")
|
62
|
+
|
63
|
+
prepend_child("<caption>#{@table.heading}</caption>")
|
64
|
+
end
|
65
|
+
|
66
|
+
# move cells between thead and tbody based on heading_rows
|
67
|
+
def set_header_rows!
|
68
|
+
rows = node.css("tr")
|
69
|
+
|
70
|
+
if heading_rows > thead.css("tr").count
|
71
|
+
rows.slice(0, heading_rows).reject { |row| row.parent == thead }.each do |row|
|
72
|
+
thead.add_child(row)
|
73
|
+
end
|
74
|
+
else
|
75
|
+
rows.slice(heading_rows, rows.length).reject { |row| row.parent == tbody }.reverse_each do |row|
|
76
|
+
tbody.prepend_child(row)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# convert cells between th and td based on heading_columns
|
82
|
+
def set_header_columns!
|
83
|
+
matrix = []
|
84
|
+
|
85
|
+
css("tr").each_with_index do |row, y|
|
86
|
+
row.css("td, th").each_with_index do |cell, x|
|
87
|
+
# step right until we find an empty cell
|
88
|
+
x += 1 until matrix.dig(y, x).nil?
|
89
|
+
|
90
|
+
# update the type of the cell based on the heading configuration
|
91
|
+
set_cell_type!(cell, y, x)
|
92
|
+
|
93
|
+
# record the coordinates that this cell occupies in the matrix
|
94
|
+
# e.g. colspan=2 rowspan=3 would occupy 6 cells
|
95
|
+
row_range(cell, y).each do |ty|
|
96
|
+
col_range(cell, x).each do |tx|
|
97
|
+
matrix[ty] ||= []
|
98
|
+
matrix[ty][tx] = cell.text
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def set_cell_type!(cell, row, col)
|
106
|
+
cell.name = col < heading_columns || row < heading_rows ? "th" : "td"
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_html
|
110
|
+
return "" if @node.nil?
|
111
|
+
|
112
|
+
set_caption! if @heading && @table.heading.present? && !@table.heading_none?
|
113
|
+
set_header_rows!
|
114
|
+
set_header_columns!
|
115
|
+
|
116
|
+
@node.to_html
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def thead
|
122
|
+
@thead ||= at_css("thead") || tbody.add_previous_sibling("<thead>").first
|
123
|
+
end
|
124
|
+
|
125
|
+
def tbody
|
126
|
+
@tbody ||= at_css("tbody") || add_child("<tbody>").last
|
127
|
+
end
|
128
|
+
|
129
|
+
def col_range(cell, from)
|
130
|
+
colspan = cell.attributes["colspan"]&.value&.to_i || 1
|
131
|
+
(from..).take(colspan)
|
132
|
+
end
|
133
|
+
|
134
|
+
def row_range(cell, from)
|
135
|
+
rowspan = cell.attributes["rowspan"]&.value&.to_i || 1
|
136
|
+
(from..).take(rowspan)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# rubocop:enable Rails/HelperInstanceVariable
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -3,6 +3,7 @@ import ItemController from "./editor/item_controller";
|
|
3
3
|
import ListController from "./editor/list_controller";
|
4
4
|
import NewItemController from "./editor/new_item_controller";
|
5
5
|
import StatusBarController from "./editor/status_bar_controller";
|
6
|
+
import TableController from "./editor/table_controller";
|
6
7
|
import TrixController from "./editor/trix_controller";
|
7
8
|
|
8
9
|
const Definitions = [
|
@@ -26,6 +27,10 @@ const Definitions = [
|
|
26
27
|
identifier: "content--editor--status-bar",
|
27
28
|
controllerConstructor: StatusBarController,
|
28
29
|
},
|
30
|
+
{
|
31
|
+
identifier: "content--editor--table",
|
32
|
+
controllerConstructor: TableController,
|
33
|
+
},
|
29
34
|
{
|
30
35
|
identifier: "content--editor--trix",
|
31
36
|
controllerConstructor: TrixController,
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
const EDITOR = `
|
4
|
+
<div class="content--editor--table-editor"
|
5
|
+
contenteditable="true"
|
6
|
+
data-content--editor--table-target="content"
|
7
|
+
data-action="paste->content--editor--table#paste"
|
8
|
+
id="item-content-field">
|
9
|
+
</div>`;
|
10
|
+
|
11
|
+
export default class TableController extends Controller {
|
12
|
+
static targets = ["input", "update"];
|
13
|
+
|
14
|
+
constructor(config) {
|
15
|
+
super(config);
|
16
|
+
|
17
|
+
this.observer = new MutationObserver(this.change);
|
18
|
+
}
|
19
|
+
|
20
|
+
connect() {
|
21
|
+
const template = document.createElement("TEMPLATE");
|
22
|
+
template.innerHTML = EDITOR;
|
23
|
+
this.content = template.content.firstElementChild;
|
24
|
+
this.content.innerHTML = this.inputTarget.value;
|
25
|
+
this.content.className += ` ${this.inputTarget.className}`;
|
26
|
+
this.inputTarget.insertAdjacentElement("beforebegin", this.content);
|
27
|
+
this.inputTarget.hidden = true;
|
28
|
+
|
29
|
+
this.observer.observe(this.content, {
|
30
|
+
attributes: true,
|
31
|
+
childList: true,
|
32
|
+
characterData: true,
|
33
|
+
subtree: true,
|
34
|
+
});
|
35
|
+
}
|
36
|
+
|
37
|
+
disconnect() {
|
38
|
+
this.observer.disconnect();
|
39
|
+
this.content.remove();
|
40
|
+
delete this.content;
|
41
|
+
}
|
42
|
+
|
43
|
+
change = (mutations) => {
|
44
|
+
this.inputTarget.value = this.table?.outerHTML;
|
45
|
+
};
|
46
|
+
|
47
|
+
update = () => {
|
48
|
+
this.updateTarget.click();
|
49
|
+
};
|
50
|
+
|
51
|
+
paste = (e) => {
|
52
|
+
if (e.clipboardData.getData("text/html").indexOf("<table") === -1) return;
|
53
|
+
|
54
|
+
e.preventDefault();
|
55
|
+
|
56
|
+
this.inputTarget.value = e.clipboardData.getData("text/html");
|
57
|
+
|
58
|
+
this.update();
|
59
|
+
};
|
60
|
+
|
61
|
+
/**
|
62
|
+
* @returns {HTMLTableElement} The table element from the content target
|
63
|
+
*/
|
64
|
+
get table() {
|
65
|
+
return this.content.querySelector("table");
|
66
|
+
}
|
67
|
+
}
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
class Table < Item
|
6
|
+
has_rich_text :content
|
7
|
+
|
8
|
+
attribute :heading_rows, :integer
|
9
|
+
attribute :heading_columns, :integer
|
10
|
+
|
11
|
+
validates :content, presence: true
|
12
|
+
|
13
|
+
default_scope { with_rich_text_content }
|
14
|
+
|
15
|
+
after_initialize :set_defaults
|
16
|
+
|
17
|
+
def initialize_copy(source)
|
18
|
+
super
|
19
|
+
|
20
|
+
content.body = source.content&.body if source.content.is_a?(ActionText::RichText)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.permitted_params
|
24
|
+
super + %i[content heading_rows heading_columns]
|
25
|
+
end
|
26
|
+
|
27
|
+
def valid?(context = nil)
|
28
|
+
super(context)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_plain_text
|
32
|
+
content.to_plain_text if visible?
|
33
|
+
end
|
34
|
+
|
35
|
+
def content=(value)
|
36
|
+
Tables::Importer.call(self, value)
|
37
|
+
|
38
|
+
set_defaults
|
39
|
+
|
40
|
+
content
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def set_defaults
|
46
|
+
super
|
47
|
+
|
48
|
+
if content.present? && (fragment = content.body.fragment)
|
49
|
+
self.heading_rows ||= fragment.find_all("thead > tr").count
|
50
|
+
self.heading_columns ||= fragment.find_all("tbody > tr:first-child > th").count
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
module Tables
|
6
|
+
class Importer
|
7
|
+
include ActiveModel::Model
|
8
|
+
|
9
|
+
delegate :config, to: Katalyst::Content::Config
|
10
|
+
|
11
|
+
delegate_missing_to :@node
|
12
|
+
|
13
|
+
attr_reader :table
|
14
|
+
|
15
|
+
# Update a table from an HTML5 fragment and apply normalisation rules.
|
16
|
+
#
|
17
|
+
# @param table [Katalyst::Content::Table] the table to update
|
18
|
+
# @param value [Nokogiri::XML::Node, ActionText::RichText, String] the provided HTML fragment
|
19
|
+
def self.call(table, value)
|
20
|
+
new(table).call(wrap(value))
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.wrap(value)
|
24
|
+
case value
|
25
|
+
when Nokogiri::XML::Node
|
26
|
+
value
|
27
|
+
when ActionText::RichText
|
28
|
+
# clone body to avoid modifying the original
|
29
|
+
Nokogiri::HTML5.fragment(value.body.to_html)
|
30
|
+
else
|
31
|
+
Nokogiri::HTML5.fragment(value.to_s)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(table)
|
36
|
+
super()
|
37
|
+
|
38
|
+
@table = table
|
39
|
+
end
|
40
|
+
|
41
|
+
def call(fragment)
|
42
|
+
@node = fragment.name == "table" ? fragment : fragment.at_css("table")
|
43
|
+
|
44
|
+
unless @node&.name == "table"
|
45
|
+
table.content.body = nil
|
46
|
+
return self
|
47
|
+
end
|
48
|
+
|
49
|
+
# Convert b and i to strong and em
|
50
|
+
normalize_emphasis!
|
51
|
+
|
52
|
+
# Convert `td > strong` to th headings
|
53
|
+
promote_header_cells!
|
54
|
+
|
55
|
+
# Promote any rows with only <th> cells to <thead>
|
56
|
+
# This captures the pattern where a table has a header row but its
|
57
|
+
# in the tbody instead of the thead
|
58
|
+
promote_header_rows!
|
59
|
+
|
60
|
+
# Promote first row to caption if it only has one cell.
|
61
|
+
# This captures the pattern where a table has as single cell that spans
|
62
|
+
# the entire width of the table, and is used as a heading.
|
63
|
+
promote_header_row_caption! if header_row_caption?
|
64
|
+
|
65
|
+
# Update the table heading with the caption, if present, and remove from table
|
66
|
+
set_heading! if caption?
|
67
|
+
|
68
|
+
# Update the table's content with the normalized HTML.
|
69
|
+
table.content.body = to_html
|
70
|
+
|
71
|
+
table
|
72
|
+
end
|
73
|
+
|
74
|
+
def caption?
|
75
|
+
at_css("caption")&.text&.present?
|
76
|
+
end
|
77
|
+
|
78
|
+
def header_row_caption?
|
79
|
+
!at_css("caption") &&
|
80
|
+
(tr = at_css("thead > tr:first-child"))&.elements&.count == 1 &&
|
81
|
+
(tr.elements.first.attributes["colspan"]&.value&.to_i&.> 1)
|
82
|
+
end
|
83
|
+
|
84
|
+
def normalize_emphasis!
|
85
|
+
traverse do |node|
|
86
|
+
case node.name
|
87
|
+
when "b"
|
88
|
+
node.name = "strong"
|
89
|
+
when "i"
|
90
|
+
node.name = "em"
|
91
|
+
else
|
92
|
+
node
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def promote_header_cells!
|
98
|
+
css("td:has(strong)").each { |cell| promote_cell_to_heading!(cell) }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Converts a `td > strong` cell to a `th` cell by updating the cell name
|
102
|
+
# and replacing the cell's content with the strong tag's content.
|
103
|
+
def promote_cell_to_heading!(cell)
|
104
|
+
strong = cell.at_css("strong")
|
105
|
+
|
106
|
+
return unless cell.text.strip == strong.text.strip
|
107
|
+
|
108
|
+
cell.name = "th"
|
109
|
+
|
110
|
+
# remove strong by promoting its children
|
111
|
+
strong.before(strong.children)
|
112
|
+
strong.remove
|
113
|
+
end
|
114
|
+
|
115
|
+
def promote_header_rows!
|
116
|
+
css("tbody > tr").each do |tr|
|
117
|
+
break unless tr.elements.all? { |td| td.name == "th" }
|
118
|
+
|
119
|
+
promote_row_to_head!(tr)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Moves a row from <tbody> to <thead>.
|
124
|
+
# If the table doesn't have a <thead>, one will be created.
|
125
|
+
def promote_row_to_head!(row)
|
126
|
+
at_css("tbody").before("<thead></thead>") unless at_css("thead")
|
127
|
+
at_css("thead") << row
|
128
|
+
end
|
129
|
+
|
130
|
+
# Promotes the first row to a caption if it only has one cell.
|
131
|
+
def promote_header_row_caption!
|
132
|
+
tr = at_css("thead > tr:first-child")
|
133
|
+
cell = tr.elements.first
|
134
|
+
tr.remove
|
135
|
+
thead = at_css("thead")
|
136
|
+
thead.before("<caption></caption>")
|
137
|
+
thead.remove if thead.elements.empty?
|
138
|
+
at_css("caption").inner_html = cell.inner_html.strip
|
139
|
+
end
|
140
|
+
|
141
|
+
# Set heading from caption and remove the caption from the table.
|
142
|
+
def set_heading!
|
143
|
+
caption = at_css("caption")
|
144
|
+
table.heading = caption.text.strip
|
145
|
+
table.heading_style = "default"
|
146
|
+
caption.remove
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -1,3 +1,8 @@
|
|
1
|
+
<%# we're going to submit using multiple paths, turbo needs CSRF header token %>
|
2
|
+
<% content_for :head do %>
|
3
|
+
<%= csrf_meta_tags %>
|
4
|
+
<% end %>
|
5
|
+
|
1
6
|
<%= render Kpop::ModalComponent.new(title: item_editor.title, layout: "side-panel") do %>
|
2
7
|
<%= render item_editor %>
|
3
8
|
<% end %>
|
@@ -0,0 +1,48 @@
|
|
1
|
+
<%= form_with model: table, scope: :item, url: path, data: {
|
2
|
+
controller: "content--editor--table",
|
3
|
+
} do |form| %>
|
4
|
+
<%= render "hidden_fields", form: %>
|
5
|
+
<%= render "form_errors", form: %>
|
6
|
+
|
7
|
+
<div class="field">
|
8
|
+
<%= form.label :heading %>
|
9
|
+
<%= form.text_field :heading %>
|
10
|
+
</div>
|
11
|
+
|
12
|
+
<div class="field">
|
13
|
+
<%= form.label :heading_style %>
|
14
|
+
<%= form.collection_radio_buttons :heading_style, Katalyst::Content.config.heading_styles, :itself, :itself %>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<div class="field">
|
18
|
+
<%= form.label :background %>
|
19
|
+
<%= form.select :background, Katalyst::Content.config.backgrounds %>
|
20
|
+
</div>
|
21
|
+
|
22
|
+
<div class="field">
|
23
|
+
<%= form.label :visible %>
|
24
|
+
<%= form.check_box :visible %>
|
25
|
+
</div>
|
26
|
+
|
27
|
+
<div class="field">
|
28
|
+
<%= form.label :content %>
|
29
|
+
<% content = sanitize_content_table(normalize_content_table(form.object, heading: false)) %>
|
30
|
+
<%= form.hidden_field :content, value: content, data: { content__editor__table_target: "input" } %>
|
31
|
+
</div>
|
32
|
+
|
33
|
+
<div class="field">
|
34
|
+
<%= form.label :heading_rows %>
|
35
|
+
<%= form.text_field :heading_rows, type: :number, data: { action: "input->content--editor--table#update" } %>
|
36
|
+
</div>
|
37
|
+
|
38
|
+
<div class="field">
|
39
|
+
<%= form.label :heading_columns %>
|
40
|
+
<%= form.text_field :heading_columns, type: :number, data: { action: "input->content--editor--table#update" } %>
|
41
|
+
</div>
|
42
|
+
|
43
|
+
<%= form.submit "Done" %>
|
44
|
+
<%= link_to "Discard", :back %>
|
45
|
+
<%= form.button "Update",
|
46
|
+
formaction: table.persisted? ? content_routes.table_path : content_routes.tables_path,
|
47
|
+
data: { content__editor__table_target: "update" } %>
|
48
|
+
<% end %>
|
data/config/locales/en.yml
CHANGED