panda-editor 0.5.0 → 0.8.2
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/.ruby-version +1 -0
- data/CHANGELOG.md +19 -0
- data/README.md +68 -0
- data/app/javascript/panda/editor/application.js +8 -6
- data/app/javascript/panda/editor/controllers/index.js +5 -0
- data/app/javascript/panda/editor/editor_js_config.js +7 -5
- data/app/javascript/panda/editor/editor_js_initializer.js +10 -3
- data/app/javascript/panda/editor/plugins/embed.min.js +2 -0
- data/app/javascript/panda/editor/plugins/header.min.js +9 -0
- data/app/javascript/panda/editor/plugins/nested-list.min.js +2 -0
- data/app/javascript/panda/editor/plugins/paragraph.min.js +9 -0
- data/app/javascript/panda/editor/plugins/quote.min.js +2 -0
- data/app/javascript/panda/editor/plugins/simple-image.min.js +2 -0
- data/app/javascript/panda/editor/plugins/table.min.js +2 -0
- data/app/javascript/panda/editor/rich_text_editor.js +2 -3
- data/app/services/panda/editor/html_to_editor_js_converter.rb +68 -68
- data/app/stylesheets/editor.css +120 -0
- data/config/importmap.rb +22 -11
- data/docs/FOOTNOTES.md +96 -3
- data/lefthook.yml +16 -0
- data/lib/panda/editor/asset_loader.rb +27 -27
- data/lib/panda/editor/blocks/alert.rb +10 -10
- data/lib/panda/editor/blocks/base.rb +1 -1
- data/lib/panda/editor/blocks/header.rb +2 -2
- data/lib/panda/editor/blocks/image.rb +11 -11
- data/lib/panda/editor/blocks/list.rb +25 -6
- data/lib/panda/editor/blocks/paragraph.rb +41 -10
- data/lib/panda/editor/blocks/quote.rb +6 -6
- data/lib/panda/editor/blocks/table.rb +6 -6
- data/lib/panda/editor/content.rb +11 -8
- data/lib/panda/editor/engine.rb +33 -7
- data/lib/panda/editor/footnote_registry.rb +10 -5
- data/lib/panda/editor/html_to_editor_js_converter.rb +159 -0
- data/lib/panda/editor/markdown_to_editor_js_converter.rb +50 -0
- data/lib/panda/editor/renderer.rb +31 -31
- data/lib/panda/editor/version.rb +1 -1
- data/lib/panda/editor.rb +19 -15
- data/lib/panda-editor.rb +5 -0
- data/lib/tasks/assets.rake +27 -27
- data/mise.toml +2 -0
- data/panda-editor.gemspec +25 -23
- metadata +49 -3
|
@@ -9,78 +9,78 @@ module Panda
|
|
|
9
9
|
return {} if html.blank?
|
|
10
10
|
|
|
11
11
|
# If it's already in EditorJS format, return as is
|
|
12
|
-
return html if html.is_a?(Hash) && (html[
|
|
12
|
+
return html if html.is_a?(Hash) && (html["blocks"].present? || html[:blocks].present?)
|
|
13
13
|
|
|
14
14
|
begin
|
|
15
15
|
# Parse the HTML content
|
|
16
16
|
doc = Nokogiri::HTML.fragment(html.to_s)
|
|
17
|
-
raise ConversionError,
|
|
17
|
+
raise ConversionError, "Failed to parse HTML content" unless doc
|
|
18
18
|
|
|
19
19
|
blocks = []
|
|
20
|
-
current_text =
|
|
20
|
+
current_text = ""
|
|
21
21
|
|
|
22
22
|
doc.children.each do |node|
|
|
23
23
|
case node.name
|
|
24
|
-
when
|
|
24
|
+
when "h1", "h2", "h3", "h4", "h5", "h6"
|
|
25
25
|
# Add any accumulated text as a paragraph before the header
|
|
26
26
|
if current_text.present?
|
|
27
27
|
blocks << create_paragraph_block(current_text)
|
|
28
|
-
current_text =
|
|
28
|
+
current_text = ""
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
blocks << {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
"type" => "header",
|
|
33
|
+
"data" => {
|
|
34
|
+
"text" => node.text.strip,
|
|
35
|
+
"level" => node.name[1].to_i
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
when
|
|
38
|
+
when "p", "div"
|
|
39
39
|
# Add any accumulated text first
|
|
40
40
|
if current_text.present?
|
|
41
41
|
blocks << create_paragraph_block(current_text)
|
|
42
|
-
current_text =
|
|
42
|
+
current_text = ""
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
if node.name ==
|
|
45
|
+
if node.name == "div"
|
|
46
46
|
# Process div children separately
|
|
47
47
|
node.children.each do |child|
|
|
48
48
|
case child.name
|
|
49
|
-
when
|
|
49
|
+
when "h1", "h2", "h3", "h4", "h5", "h6"
|
|
50
50
|
blocks << {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
"type" => "header",
|
|
52
|
+
"data" => {
|
|
53
|
+
"text" => child.text.strip,
|
|
54
|
+
"level" => child.name[1].to_i
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
when
|
|
57
|
+
when "p"
|
|
58
58
|
text = process_inline_elements(child)
|
|
59
59
|
paragraphs = text.split(%r{<br\s*/?>\s*<br\s*/?>}).map(&:strip)
|
|
60
60
|
paragraphs.each do |paragraph|
|
|
61
61
|
blocks << create_paragraph_block(paragraph) if paragraph.present?
|
|
62
62
|
end
|
|
63
|
-
when
|
|
64
|
-
items = child.css(
|
|
63
|
+
when "ul", "ol"
|
|
64
|
+
items = child.css("li").map { |li| process_inline_elements(li) }
|
|
65
65
|
next if items.empty?
|
|
66
66
|
|
|
67
67
|
blocks << {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
"type" => "list",
|
|
69
|
+
"data" => {
|
|
70
|
+
"style" => (child.name == "ul") ? "unordered" : "ordered",
|
|
71
|
+
"items" => items
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
when
|
|
74
|
+
when "blockquote"
|
|
75
75
|
blocks << {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
"type" => "quote",
|
|
77
|
+
"data" => {
|
|
78
|
+
"text" => process_inline_elements(child),
|
|
79
|
+
"caption" => "",
|
|
80
|
+
"alignment" => "left"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
when
|
|
83
|
+
when "text"
|
|
84
84
|
text = child.text.strip
|
|
85
85
|
current_text += text if text.present?
|
|
86
86
|
end
|
|
@@ -93,41 +93,41 @@ module Panda
|
|
|
93
93
|
blocks << create_paragraph_block(paragraph) if paragraph.present?
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
|
-
when
|
|
96
|
+
when "br"
|
|
97
97
|
current_text += "\n\n"
|
|
98
|
-
when
|
|
98
|
+
when "text"
|
|
99
99
|
text = node.text.strip
|
|
100
100
|
current_text += text if text.present?
|
|
101
|
-
when
|
|
101
|
+
when "ul", "ol"
|
|
102
102
|
# Add any accumulated text first
|
|
103
103
|
if current_text.present?
|
|
104
104
|
blocks << create_paragraph_block(current_text)
|
|
105
|
-
current_text =
|
|
105
|
+
current_text = ""
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
items = node.css(
|
|
108
|
+
items = node.css("li").map { |li| process_inline_elements(li) }
|
|
109
109
|
next if items.empty?
|
|
110
110
|
|
|
111
111
|
blocks << {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
"type" => "list",
|
|
113
|
+
"data" => {
|
|
114
|
+
"style" => (node.name == "ul") ? "unordered" : "ordered",
|
|
115
|
+
"items" => items
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
when
|
|
118
|
+
when "blockquote"
|
|
119
119
|
# Add any accumulated text first
|
|
120
120
|
if current_text.present?
|
|
121
121
|
blocks << create_paragraph_block(current_text)
|
|
122
|
-
current_text =
|
|
122
|
+
current_text = ""
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
blocks << {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
"type" => "quote",
|
|
127
|
+
"data" => {
|
|
128
|
+
"text" => process_inline_elements(node),
|
|
129
|
+
"caption" => "",
|
|
130
|
+
"alignment" => "left"
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
end
|
|
@@ -138,11 +138,11 @@ module Panda
|
|
|
138
138
|
|
|
139
139
|
# Return the complete EditorJS structure
|
|
140
140
|
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
"time" => Time.current.to_i * 1000,
|
|
142
|
+
"blocks" => blocks,
|
|
143
|
+
"version" => "2.28.2"
|
|
144
144
|
}
|
|
145
|
-
rescue
|
|
145
|
+
rescue => e
|
|
146
146
|
Rails.logger.error "HTML to EditorJS conversion failed: #{e.message}"
|
|
147
147
|
Rails.logger.error e.backtrace.join("\n")
|
|
148
148
|
raise ConversionError, "Failed to convert HTML to EditorJS format: #{e.message}"
|
|
@@ -151,41 +151,41 @@ module Panda
|
|
|
151
151
|
|
|
152
152
|
def self.create_paragraph_block(text)
|
|
153
153
|
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
"type" => "paragraph",
|
|
155
|
+
"data" => {
|
|
156
|
+
"text" => text.strip
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
def self.process_inline_elements(node)
|
|
162
|
-
result =
|
|
162
|
+
result = ""
|
|
163
163
|
node.children.each do |child|
|
|
164
164
|
case child.name
|
|
165
|
-
when
|
|
166
|
-
result +=
|
|
167
|
-
when
|
|
165
|
+
when "br"
|
|
166
|
+
result += "<br>"
|
|
167
|
+
when "text"
|
|
168
168
|
result += child.text
|
|
169
|
-
when
|
|
169
|
+
when "strong", "b"
|
|
170
170
|
result += "<b>#{child.text}</b>"
|
|
171
|
-
when
|
|
171
|
+
when "em", "i"
|
|
172
172
|
result += "<i>#{child.text}</i>"
|
|
173
|
-
when
|
|
174
|
-
href = child[
|
|
173
|
+
when "a"
|
|
174
|
+
href = child["href"]
|
|
175
175
|
text = child.text.strip
|
|
176
176
|
# Handle email links specially
|
|
177
|
-
if href&.start_with?(
|
|
178
|
-
email = href.sub(
|
|
177
|
+
if href&.start_with?("mailto:")
|
|
178
|
+
email = href.sub("mailto:", "")
|
|
179
179
|
result += "<a href=\"mailto:#{email}\">#{text}</a>"
|
|
180
180
|
else
|
|
181
181
|
result += "<a href=\"#{href}\">#{text}</a>"
|
|
182
182
|
end
|
|
183
183
|
else
|
|
184
184
|
result += if child.text?
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
child.text
|
|
186
|
+
else
|
|
187
|
+
child.to_html
|
|
188
|
+
end
|
|
189
189
|
end
|
|
190
190
|
end
|
|
191
191
|
result.strip
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* Base content styles, where .codex-editor applies them to the Panda editor too */
|
|
2
|
+
@layer components {
|
|
3
|
+
.codex-editor__redactor .ce-block .ce-block__content {
|
|
4
|
+
@apply text-base font-normal font-sans text-dark leading-[1.6] space-y-[1.6rem];
|
|
5
|
+
|
|
6
|
+
h1.ce-header {
|
|
7
|
+
@apply text-3xl md:text-4xl font-semibold font-sans text-[#104071] leading-[1.2] max-w-[85ch];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
h2.ce-header {
|
|
11
|
+
@apply text-2xl font-medium font-sans text-[#104071] leading-[1.3] mb-4 mt-8 max-w-[85ch];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
h3.ce-header {
|
|
15
|
+
@apply text-xl font-normal font-sans text-[#104071] leading-[1.3] mb-4 mt-6 max-w-[85ch];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
p,
|
|
19
|
+
li {
|
|
20
|
+
@apply leading-[1.6] tracking-wide max-w-[85ch];
|
|
21
|
+
|
|
22
|
+
a {
|
|
23
|
+
@apply text-[#1A9597] underline underline-offset-2 hover:text-[#158486] focus:outline-2 focus:outline-offset-2 focus:outline-[#1A9597];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
strong,
|
|
27
|
+
b {
|
|
28
|
+
@apply font-semibold;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
p {
|
|
33
|
+
@apply mb-4;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.cdx-quote {
|
|
37
|
+
@apply bg-[#eef0f3] border-l-inactive border-l-8 p-6 mb-4;
|
|
38
|
+
|
|
39
|
+
.cdx-quote__caption {
|
|
40
|
+
@apply block ml-6 mt-2 text-sm text-dark;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.cdx-quote__text {
|
|
44
|
+
quotes: "\201C" "\201D" "\2018" "\2019";
|
|
45
|
+
@apply pl-6;
|
|
46
|
+
|
|
47
|
+
&:before {
|
|
48
|
+
@apply -ml-8 mr-2 text-dark text-6xl leading-4 align-text-bottom font-serif;
|
|
49
|
+
content: open-quote;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
p {
|
|
53
|
+
@apply inline italic text-lg;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.cdx-list {
|
|
59
|
+
@apply mb-4 pl-6;
|
|
60
|
+
|
|
61
|
+
&--ordered {
|
|
62
|
+
@apply list-decimal;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&--unordered {
|
|
66
|
+
@apply list-disc;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.cdx-list {
|
|
70
|
+
@apply mt-2 mb-0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.cdx-list__item {
|
|
74
|
+
@apply mb-2 pl-2;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.cdx-nested-list {
|
|
79
|
+
@apply mb-4 pl-6;
|
|
80
|
+
|
|
81
|
+
&--ordered {
|
|
82
|
+
@apply list-decimal;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
&--unordered {
|
|
86
|
+
@apply list-disc;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.cdx-nested-list {
|
|
90
|
+
@apply mt-2 mb-0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.cdx-nested-list__item {
|
|
94
|
+
@apply mb-2 pl-2;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.cdx-table {
|
|
99
|
+
@apply w-full border-collapse border-2 border-dark my-6;
|
|
100
|
+
|
|
101
|
+
&__head {
|
|
102
|
+
@apply font-semibold border-dark border-r-2 p-3 bg-light;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&__row {
|
|
106
|
+
@apply border-dark border-b-2;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&__cell {
|
|
110
|
+
@apply border-dark border-r-2 p-3;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.cdx-embed {
|
|
115
|
+
iframe {
|
|
116
|
+
@apply w-full border-none;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
data/config/importmap.rb
CHANGED
|
@@ -2,14 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
# Pin npm packages by running ./bin/importmap
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
pin
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
pin
|
|
12
|
-
pin
|
|
13
|
-
pin
|
|
14
|
-
pin
|
|
15
|
-
pin
|
|
5
|
+
# Entry points (required by panda_core_javascript helper)
|
|
6
|
+
# These must use absolute paths to work with JavaScriptMiddleware
|
|
7
|
+
pin "panda/editor/application", to: "/panda/editor/application.js", preload: true
|
|
8
|
+
pin "panda/editor/controllers/index", to: "/panda/editor/controllers/index.js"
|
|
9
|
+
|
|
10
|
+
# Individual modules used by panda-cms controllers
|
|
11
|
+
pin "panda/editor/editor_js_config", to: "/panda/editor/editor_js_config.js"
|
|
12
|
+
pin "panda/editor/editor_js_initializer", to: "/panda/editor/editor_js_initializer.js"
|
|
13
|
+
pin "panda/editor/resource_loader", to: "/panda/editor/resource_loader.js"
|
|
14
|
+
pin "panda/editor/plain_text_editor", to: "/panda/editor/plain_text_editor.js"
|
|
15
|
+
pin "panda/editor/css_extractor", to: "/panda/editor/css_extractor.js"
|
|
16
|
+
pin "panda/editor/rich_text_editor", to: "/panda/editor/rich_text_editor.js"
|
|
17
|
+
|
|
18
|
+
# EditorJS Core and plugins (from esm.sh - better ES module handling than jsdelivr)
|
|
19
|
+
pin "@editorjs/editorjs", to: "https://esm.sh/@editorjs/editorjs@2.28.2"
|
|
20
|
+
pin "@editorjs/paragraph", to: "https://esm.sh/@editorjs/paragraph@2.11.3"
|
|
21
|
+
pin "@editorjs/header", to: "https://esm.sh/@editorjs/header@2.8.1"
|
|
22
|
+
pin "@editorjs/nested-list", to: "https://esm.sh/@editorjs/nested-list@1.4.2"
|
|
23
|
+
pin "@editorjs/quote", to: "https://esm.sh/@editorjs/quote@2.6.0"
|
|
24
|
+
pin "@editorjs/simple-image", to: "https://esm.sh/@editorjs/simple-image@1.6.0"
|
|
25
|
+
pin "@editorjs/table", to: "https://esm.sh/@editorjs/table@2.3.0"
|
|
26
|
+
pin "@editorjs/embed", to: "https://esm.sh/@editorjs/embed@2.7.0"
|
data/docs/FOOTNOTES.md
CHANGED
|
@@ -29,6 +29,7 @@ The footnote system consists of three main components:
|
|
|
29
29
|
- 📍 **Position-based injection**: Place footnote markers at any character position
|
|
30
30
|
- 🎨 **Collapsible UI**: Clean, accessible sources section
|
|
31
31
|
- 🔗 **Bidirectional links**: Navigate between citations and sources
|
|
32
|
+
- 💬 **Hover tooltips**: Preview footnote content without scrolling to sources
|
|
32
33
|
|
|
33
34
|
## How It Works
|
|
34
35
|
|
|
@@ -181,10 +182,17 @@ Both paragraphs will reference the same footnote number, and the source will app
|
|
|
181
182
|
|
|
182
183
|
### Inline Markers
|
|
183
184
|
|
|
185
|
+
Footnote markers include native browser tooltips and data attributes for custom tooltip implementations:
|
|
186
|
+
|
|
184
187
|
```html
|
|
185
|
-
<p>Climate change has accelerated significantly since 1980<sup id="fnref:1"><a href="#fn:1" class="footnote">1</a></sup></p>
|
|
188
|
+
<p>Climate change has accelerated significantly since 1980<sup id="fnref:1" class="footnote-ref" data-footnote-content="IPCC. (2023). Climate Change 2023: Synthesis Report." title="IPCC. (2023). Climate Change 2023: Synthesis Report."><a href="#fn:1" class="footnote">1</a></sup></p>
|
|
186
189
|
```
|
|
187
190
|
|
|
191
|
+
**Tooltip Attributes:**
|
|
192
|
+
- `class="footnote-ref"` - Identifies footnote markers for styling
|
|
193
|
+
- `data-footnote-content` - Contains processed footnote content (with markdown/HTML if enabled) for custom tooltips
|
|
194
|
+
- `title` - Contains plain text version for native browser tooltips on hover
|
|
195
|
+
|
|
188
196
|
### Sources Section
|
|
189
197
|
|
|
190
198
|
```html
|
|
@@ -442,6 +450,91 @@ def generate_cached_content
|
|
|
442
450
|
end
|
|
443
451
|
```
|
|
444
452
|
|
|
453
|
+
## Tooltips
|
|
454
|
+
|
|
455
|
+
Footnote markers automatically include tooltip support through two mechanisms:
|
|
456
|
+
|
|
457
|
+
### Native Browser Tooltips
|
|
458
|
+
|
|
459
|
+
The `title` attribute provides instant, zero-JavaScript tooltips:
|
|
460
|
+
|
|
461
|
+
```html
|
|
462
|
+
<sup title="IPCC. (2023). Climate Change 2023: Synthesis Report.">
|
|
463
|
+
<a href="#fn:1" class="footnote">1</a>
|
|
464
|
+
</sup>
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
This works immediately in all browsers with no additional code required. However, native tooltips have limitations:
|
|
468
|
+
- Cannot contain HTML formatting
|
|
469
|
+
- Limited styling options
|
|
470
|
+
- Inconsistent behavior across browsers
|
|
471
|
+
|
|
472
|
+
### Custom Tooltips
|
|
473
|
+
|
|
474
|
+
For richer tooltips, use the `data-footnote-content` attribute with your preferred tooltip library:
|
|
475
|
+
|
|
476
|
+
**With Tippy.js:**
|
|
477
|
+
|
|
478
|
+
```javascript
|
|
479
|
+
import tippy from 'tippy.js'
|
|
480
|
+
import 'tippy.js/dist/tippy.css'
|
|
481
|
+
|
|
482
|
+
// Initialize tooltips for all footnote markers
|
|
483
|
+
tippy('[data-footnote-content]', {
|
|
484
|
+
content: (reference) => reference.getAttribute('data-footnote-content'),
|
|
485
|
+
allowHTML: true,
|
|
486
|
+
theme: 'light',
|
|
487
|
+
placement: 'top',
|
|
488
|
+
maxWidth: 400
|
|
489
|
+
})
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**With Bootstrap:**
|
|
493
|
+
|
|
494
|
+
```javascript
|
|
495
|
+
// Initialize Bootstrap tooltips
|
|
496
|
+
document.querySelectorAll('[data-footnote-content]').forEach(element => {
|
|
497
|
+
new bootstrap.Tooltip(element, {
|
|
498
|
+
title: element.getAttribute('data-footnote-content'),
|
|
499
|
+
html: true,
|
|
500
|
+
placement: 'top'
|
|
501
|
+
})
|
|
502
|
+
})
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
**With Custom CSS Tooltips:**
|
|
506
|
+
|
|
507
|
+
```css
|
|
508
|
+
/* Pure CSS tooltip */
|
|
509
|
+
.footnote-ref {
|
|
510
|
+
position: relative;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.footnote-ref::after {
|
|
514
|
+
content: attr(data-footnote-content);
|
|
515
|
+
position: absolute;
|
|
516
|
+
bottom: 100%;
|
|
517
|
+
left: 50%;
|
|
518
|
+
transform: translateX(-50%);
|
|
519
|
+
padding: 0.5rem;
|
|
520
|
+
background: #333;
|
|
521
|
+
color: white;
|
|
522
|
+
border-radius: 0.25rem;
|
|
523
|
+
font-size: 0.875rem;
|
|
524
|
+
white-space: nowrap;
|
|
525
|
+
opacity: 0;
|
|
526
|
+
pointer-events: none;
|
|
527
|
+
transition: opacity 0.2s;
|
|
528
|
+
z-index: 1000;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.footnote-ref:hover::after {
|
|
532
|
+
opacity: 1;
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**Security Note:** The `data-footnote-content` attribute is properly HTML-escaped to prevent XSS attacks. When using `allowHTML: true` with tooltip libraries, the content is safe because special characters are already escaped.
|
|
537
|
+
|
|
445
538
|
## CSS Styling
|
|
446
539
|
|
|
447
540
|
The rendered HTML includes Tailwind CSS classes. You can customize the appearance:
|
|
@@ -621,11 +714,11 @@ bundle exec rspec spec/lib/panda/editor/renderer_spec.rb
|
|
|
621
714
|
|
|
622
715
|
## Future Enhancements
|
|
623
716
|
|
|
624
|
-
|
|
717
|
+
See GitHub issue [#2](https://github.com/tastybamboo/panda-editor/issues/2) for planned improvements including:
|
|
625
718
|
|
|
626
719
|
- [ ] Support for footnotes in other block types (headers, quotes, etc.)
|
|
627
720
|
- [x] Rich text formatting within footnote content (implemented via markdown support)
|
|
628
|
-
- [
|
|
721
|
+
- [x] Footnote tooltips on hover (implemented with native browser tooltips and custom tooltip data attributes)
|
|
629
722
|
- [ ] Customizable footnote markers (*, †, ‡, etc.)
|
|
630
723
|
- [ ] Export footnotes to bibliography formats (BibTeX, etc.)
|
|
631
724
|
- [ ] Footnote management UI in EditorJS
|
data/lefthook.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
assert_lefthook_installed: true
|
|
3
|
+
colors: true
|
|
4
|
+
pre-commit:
|
|
5
|
+
parallel: true
|
|
6
|
+
jobs:
|
|
7
|
+
- name: standardrb
|
|
8
|
+
run: bundle exec standardrb
|
|
9
|
+
glob: "*.rb"
|
|
10
|
+
stage_fixed: true
|
|
11
|
+
|
|
12
|
+
pre-push:
|
|
13
|
+
parallel: true
|
|
14
|
+
jobs:
|
|
15
|
+
- name: standardrb
|
|
16
|
+
run: bundle exec standardrb
|