katalyst-content 2.2.0 → 2.3.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.
- checksums.yaml +4 -4
- data/app/assets/builds/katalyst/content.esm.js +52 -1
- data/app/assets/builds/katalyst/content.js +52 -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/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 +47 -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 +53 -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,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;
|
@@ -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,47 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class TableController extends Controller {
|
4
|
+
static targets = ["content", "input", "update"];
|
5
|
+
|
6
|
+
constructor(config) {
|
7
|
+
super(config);
|
8
|
+
|
9
|
+
this.observer = new MutationObserver(this.change);
|
10
|
+
}
|
11
|
+
|
12
|
+
connect() {
|
13
|
+
this.observer.observe(this.contentTarget, {
|
14
|
+
attributes: true,
|
15
|
+
childList: true,
|
16
|
+
characterData: true,
|
17
|
+
subtree: true,
|
18
|
+
});
|
19
|
+
}
|
20
|
+
|
21
|
+
disconnect() {
|
22
|
+
this.observer.disconnect();
|
23
|
+
}
|
24
|
+
|
25
|
+
change = (mutations) => {
|
26
|
+
this.inputTarget.value = this.table.outerHTML;
|
27
|
+
};
|
28
|
+
|
29
|
+
update = () => {
|
30
|
+
this.updateTarget.click();
|
31
|
+
};
|
32
|
+
|
33
|
+
paste = (e) => {
|
34
|
+
e.preventDefault();
|
35
|
+
|
36
|
+
this.inputTarget.value = e.clipboardData.getData("text/html");
|
37
|
+
|
38
|
+
this.update();
|
39
|
+
};
|
40
|
+
|
41
|
+
/**
|
42
|
+
* @returns {HTMLTableElement} The table element from the content target
|
43
|
+
*/
|
44
|
+
get table() {
|
45
|
+
return this.contentTarget.querySelector("table");
|
46
|
+
}
|
47
|
+
}
|
@@ -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,53 @@
|
|
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
|
+
<div contenteditable="true"
|
31
|
+
data-content--editor--table-target="content"
|
32
|
+
data-action="paste->content--editor--table#paste">
|
33
|
+
<%= content %>
|
34
|
+
</div>
|
35
|
+
<%= form.hidden_field :content, value: content, data: { content__editor__table_target: "input" } %>
|
36
|
+
</div>
|
37
|
+
|
38
|
+
<div class="field">
|
39
|
+
<%= form.label :heading_rows %>
|
40
|
+
<%= form.text_field :heading_rows, type: :number, data: { action: "input->content--editor--table#update" } %>
|
41
|
+
</div>
|
42
|
+
|
43
|
+
<div class="field">
|
44
|
+
<%= form.label :heading_columns %>
|
45
|
+
<%= form.text_field :heading_columns, type: :number, data: { action: "input->content--editor--table#update" } %>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<%= form.submit "Done" %>
|
49
|
+
<%= link_to "Discard", :back %>
|
50
|
+
<%= form.button "Update",
|
51
|
+
formaction: table.persisted? ? content_routes.table_path : content_routes.tables_path,
|
52
|
+
data: { content__editor__table_target: "update" } %>
|
53
|
+
<% end %>
|
data/config/locales/en.yml
CHANGED
data/config/routes.rb
CHANGED
@@ -13,6 +13,7 @@ module Katalyst
|
|
13
13
|
%w[
|
14
14
|
Katalyst::Content::Content
|
15
15
|
Katalyst::Content::Figure
|
16
|
+
Katalyst::Content::Table
|
16
17
|
Katalyst::Content::Section
|
17
18
|
Katalyst::Content::Group
|
18
19
|
Katalyst::Content::Column
|
@@ -26,6 +27,14 @@ module Katalyst
|
|
26
27
|
config_accessor(:errors_component) { "Katalyst::Content::Editor::ErrorsComponent" }
|
27
28
|
|
28
29
|
config_accessor(:base_controller) { "ApplicationController" }
|
30
|
+
|
31
|
+
# Sanitizer
|
32
|
+
config_accessor(:table_sanitizer_allowed_tags) do
|
33
|
+
%w[table thead tbody tr th td caption a strong em span br p text].freeze
|
34
|
+
end
|
35
|
+
config_accessor(:table_sanitizer_allowed_attributes) do
|
36
|
+
%w[colspan rowspan href].freeze
|
37
|
+
end
|
29
38
|
end
|
30
39
|
end
|
31
40
|
end
|
@@ -39,4 +39,26 @@ FactoryBot.define do
|
|
39
39
|
factory :katalyst_content_column, class: "Katalyst::Content::Column" do
|
40
40
|
content_item_defaults
|
41
41
|
end
|
42
|
+
|
43
|
+
factory :katalyst_content_table, class: "Katalyst::Content::Table" do
|
44
|
+
content_item_defaults
|
45
|
+
heading { "Contacts" }
|
46
|
+
heading_style { "none" }
|
47
|
+
content { <<~HTML }
|
48
|
+
<table>
|
49
|
+
<thead>
|
50
|
+
<tr>
|
51
|
+
<th>Name</th>
|
52
|
+
<th>Email</th>
|
53
|
+
</tr>
|
54
|
+
</thead>
|
55
|
+
<tbody>
|
56
|
+
<tr>
|
57
|
+
<td>John Doe</td>
|
58
|
+
<td>john.doe@example.com</td>
|
59
|
+
</tr>
|
60
|
+
</tbody>
|
61
|
+
</table>
|
62
|
+
HTML
|
63
|
+
end
|
42
64
|
end
|