panda-editor 0.6.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/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 +29 -9
- data/lib/panda/editor/footnote_registry.rb +10 -5
- data/lib/panda/editor/html_to_editor_js_converter.rb +1 -1
- data/lib/panda/editor/renderer.rb +31 -31
- data/lib/panda/editor/version.rb +1 -1
- data/lib/panda/editor.rb +18 -16
- data/lib/tasks/assets.rake +27 -27
- data/mise.toml +2 -0
- data/panda-editor.gemspec +25 -24
- metadata +32 -3
|
@@ -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
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
5
|
|
|
6
6
|
module Panda
|
|
7
7
|
module Editor
|
|
8
8
|
class AssetLoader
|
|
9
|
-
GITHUB_RELEASES_URL =
|
|
10
|
-
ASSET_CACHE_DIR = Rails.root.join(
|
|
9
|
+
GITHUB_RELEASES_URL = "https://api.github.com/repos/tastybamboo/panda-editor/releases/latest"
|
|
10
|
+
ASSET_CACHE_DIR = Rails.root.join("tmp", "panda_editor_assets")
|
|
11
11
|
|
|
12
12
|
class << self
|
|
13
13
|
def load_assets
|
|
@@ -22,7 +22,7 @@ module Panda
|
|
|
22
22
|
if use_compiled_assets?
|
|
23
23
|
compiled_javascript_url
|
|
24
24
|
else
|
|
25
|
-
|
|
25
|
+
"/assets/panda/editor/application.js"
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ module Panda
|
|
|
30
30
|
if use_compiled_assets?
|
|
31
31
|
compiled_stylesheet_url
|
|
32
32
|
else
|
|
33
|
-
|
|
33
|
+
"/assets/panda/editor/application.css"
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -39,7 +39,7 @@ module Panda
|
|
|
39
39
|
def use_compiled_assets?
|
|
40
40
|
Rails.env.production? ||
|
|
41
41
|
Rails.env.test? ||
|
|
42
|
-
ENV[
|
|
42
|
+
ENV["PANDA_EDITOR_USE_COMPILED_ASSETS"] == "true"
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def load_compiled_assets
|
|
@@ -52,23 +52,23 @@ module Panda
|
|
|
52
52
|
|
|
53
53
|
def load_development_assets
|
|
54
54
|
{
|
|
55
|
-
javascript:
|
|
56
|
-
stylesheet:
|
|
55
|
+
javascript: "/assets/panda/editor/application.js",
|
|
56
|
+
stylesheet: "/assets/panda/editor/application.css"
|
|
57
57
|
}
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def compiled_javascript_url
|
|
61
|
-
asset_path = find_latest_asset(
|
|
61
|
+
asset_path = find_latest_asset("js")
|
|
62
62
|
asset_path ? "/panda-editor-assets/#{File.basename(asset_path)}" : nil
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def compiled_stylesheet_url
|
|
66
|
-
asset_path = find_latest_asset(
|
|
66
|
+
asset_path = find_latest_asset("css")
|
|
67
67
|
asset_path ? "/panda-editor-assets/#{File.basename(asset_path)}" : nil
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def find_latest_asset(extension)
|
|
71
|
-
pattern = Rails.root.join(
|
|
71
|
+
pattern = Rails.root.join("public", "panda-editor-assets", "panda-editor-*.#{extension}")
|
|
72
72
|
Dir.glob(pattern).max_by { |f| File.mtime(f) }
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -79,18 +79,18 @@ module Panda
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def assets_exist?
|
|
82
|
-
js_exists = Dir.glob(Rails.root.join(
|
|
83
|
-
css_exists = Dir.glob(Rails.root.join(
|
|
82
|
+
js_exists = Dir.glob(Rails.root.join("public", "panda-editor-assets", "panda-editor-*.js")).any?
|
|
83
|
+
css_exists = Dir.glob(Rails.root.join("public", "panda-editor-assets", "panda-editor-*.css")).any?
|
|
84
84
|
js_exists && css_exists
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def download_assets_from_github
|
|
88
|
-
Rails.logger.info
|
|
88
|
+
Rails.logger.info "[Panda Editor] Downloading assets from GitHub releases..."
|
|
89
89
|
|
|
90
90
|
begin
|
|
91
91
|
release_data = fetch_latest_release
|
|
92
|
-
download_release_assets(release_data[
|
|
93
|
-
rescue
|
|
92
|
+
download_release_assets(release_data["assets"])
|
|
93
|
+
rescue => e
|
|
94
94
|
Rails.logger.error "[Panda Editor] Failed to download assets: #{e.message}"
|
|
95
95
|
use_fallback_assets
|
|
96
96
|
end
|
|
@@ -100,30 +100,30 @@ module Panda
|
|
|
100
100
|
uri = URI(GITHUB_RELEASES_URL)
|
|
101
101
|
response = Net::HTTP.get_response(uri)
|
|
102
102
|
|
|
103
|
-
raise "GitHub API returned #{response.code}" unless response.code ==
|
|
103
|
+
raise "GitHub API returned #{response.code}" unless response.code == "200"
|
|
104
104
|
|
|
105
105
|
JSON.parse(response.body)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def download_release_assets(assets)
|
|
109
109
|
assets.each do |asset|
|
|
110
|
-
next unless asset[
|
|
110
|
+
next unless asset["name"].match?(/panda-editor.*\.(js|css)$/)
|
|
111
111
|
|
|
112
112
|
download_asset(asset)
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
def download_asset(asset)
|
|
117
|
-
uri = URI(asset[
|
|
117
|
+
uri = URI(asset["browser_download_url"])
|
|
118
118
|
response = Net::HTTP.get_response(uri)
|
|
119
119
|
|
|
120
|
-
return unless response.code ==
|
|
120
|
+
return unless response.code == "200"
|
|
121
121
|
|
|
122
|
-
save_asset(asset[
|
|
122
|
+
save_asset(asset["name"], response.body)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
def save_asset(filename, content)
|
|
126
|
-
dir = Rails.root.join(
|
|
126
|
+
dir = Rails.root.join("public", "panda-editor-assets")
|
|
127
127
|
FileUtils.mkdir_p(dir)
|
|
128
128
|
|
|
129
129
|
File.write(dir.join(filename), content)
|
|
@@ -131,17 +131,17 @@ module Panda
|
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def use_fallback_assets
|
|
134
|
-
Rails.logger.warn
|
|
134
|
+
Rails.logger.warn "[Panda Editor] Using fallback embedded assets"
|
|
135
135
|
# Copy embedded assets from gem to public directory
|
|
136
136
|
copy_embedded_assets
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
def copy_embedded_assets
|
|
140
|
-
source_dir = Panda::Editor::Engine.root.join(
|
|
141
|
-
dest_dir = Rails.root.join(
|
|
140
|
+
source_dir = Panda::Editor::Engine.root.join("public", "panda-editor-assets")
|
|
141
|
+
dest_dir = Rails.root.join("public", "panda-editor-assets")
|
|
142
142
|
|
|
143
143
|
FileUtils.mkdir_p(dest_dir)
|
|
144
|
-
FileUtils.cp_r(Dir.glob(source_dir.join(
|
|
144
|
+
FileUtils.cp_r(Dir.glob(source_dir.join("*")), dest_dir)
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
end
|
|
@@ -5,13 +5,13 @@ module Panda
|
|
|
5
5
|
module Blocks
|
|
6
6
|
class Alert < Base
|
|
7
7
|
def render
|
|
8
|
-
message = sanitize(data[
|
|
9
|
-
type = data[
|
|
8
|
+
message = sanitize(data["message"])
|
|
9
|
+
type = data["type"] || "primary"
|
|
10
10
|
|
|
11
11
|
html_safe(
|
|
12
12
|
"<div class=\"#{alert_classes(type)} p-4 mb-4 rounded-lg\">" \
|
|
13
13
|
"#{message}" \
|
|
14
|
-
|
|
14
|
+
"</div>"
|
|
15
15
|
)
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -19,13 +19,13 @@ module Panda
|
|
|
19
19
|
|
|
20
20
|
def alert_classes(type)
|
|
21
21
|
case type
|
|
22
|
-
when
|
|
23
|
-
when
|
|
24
|
-
when
|
|
25
|
-
when
|
|
26
|
-
when
|
|
27
|
-
when
|
|
28
|
-
else
|
|
22
|
+
when "primary" then "bg-blue-100 text-blue-800"
|
|
23
|
+
when "secondary" then "bg-gray-100 text-gray-800"
|
|
24
|
+
when "success" then "bg-green-100 text-green-800"
|
|
25
|
+
when "danger" then "bg-red-100 text-red-800"
|
|
26
|
+
when "warning" then "bg-yellow-100 text-yellow-800"
|
|
27
|
+
when "info" then "bg-indigo-100 text-indigo-800"
|
|
28
|
+
else "bg-blue-100 text-blue-800"
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -5,19 +5,19 @@ module Panda
|
|
|
5
5
|
module Blocks
|
|
6
6
|
class Image < Base
|
|
7
7
|
def render
|
|
8
|
-
url = data[
|
|
9
|
-
caption = sanitize(data[
|
|
10
|
-
with_border = data[
|
|
11
|
-
with_background = data[
|
|
12
|
-
stretched = data[
|
|
8
|
+
url = data["url"]
|
|
9
|
+
caption = sanitize(data["caption"])
|
|
10
|
+
with_border = data["withBorder"]
|
|
11
|
+
with_background = data["withBackground"]
|
|
12
|
+
stretched = data["stretched"]
|
|
13
13
|
|
|
14
|
-
css_classes = [
|
|
15
|
-
css_classes <<
|
|
16
|
-
css_classes <<
|
|
17
|
-
css_classes <<
|
|
14
|
+
css_classes = ["prose"]
|
|
15
|
+
css_classes << "border" if with_border
|
|
16
|
+
css_classes << "bg-gray-100" if with_background
|
|
17
|
+
css_classes << "w-full" if stretched
|
|
18
18
|
|
|
19
19
|
html_safe(<<~HTML)
|
|
20
|
-
<figure class="#{css_classes.join(
|
|
20
|
+
<figure class="#{css_classes.join(" ")}">
|
|
21
21
|
<img src="#{url}" alt="#{caption}" />
|
|
22
22
|
#{caption_element(caption)}
|
|
23
23
|
</figure>
|
|
@@ -27,7 +27,7 @@ module Panda
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
29
|
def caption_element(caption)
|
|
30
|
-
return
|
|
30
|
+
return "" if caption.blank?
|
|
31
31
|
|
|
32
32
|
"<figcaption>#{caption}</figcaption>"
|
|
33
33
|
end
|
|
@@ -5,10 +5,11 @@ module Panda
|
|
|
5
5
|
module Blocks
|
|
6
6
|
class List < Base
|
|
7
7
|
def render
|
|
8
|
-
|
|
8
|
+
style = data["style"] || data[:style]
|
|
9
|
+
list_type = (style == "ordered") ? "ol" : "ul"
|
|
9
10
|
html_safe(
|
|
10
11
|
"<#{list_type}>" \
|
|
11
|
-
"#{render_items(data[
|
|
12
|
+
"#{render_items(data["items"] || data[:items] || [])}" \
|
|
12
13
|
"</#{list_type}>"
|
|
13
14
|
)
|
|
14
15
|
end
|
|
@@ -16,15 +17,33 @@ module Panda
|
|
|
16
17
|
private
|
|
17
18
|
|
|
18
19
|
def render_items(items)
|
|
20
|
+
return "" unless items.is_a?(Array)
|
|
21
|
+
|
|
19
22
|
items.map do |item|
|
|
20
|
-
content = item
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
content = extract_content(item)
|
|
24
|
+
nested_items = extract_nested_items(item)
|
|
25
|
+
nested = nested_items.present? ? render_nested(nested_items) : ""
|
|
26
|
+
"<li>#{sanitize(content.to_s)}#{nested}</li>"
|
|
23
27
|
end.join
|
|
24
28
|
end
|
|
25
29
|
|
|
30
|
+
def extract_content(item)
|
|
31
|
+
return item unless item.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
# Handle both string and symbol keys
|
|
34
|
+
item["content"] || item[:content] || ""
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def extract_nested_items(item)
|
|
38
|
+
return [] unless item.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
# Handle both string and symbol keys
|
|
41
|
+
item["items"] || item[:items] || []
|
|
42
|
+
end
|
|
43
|
+
|
|
26
44
|
def render_nested(items)
|
|
27
|
-
|
|
45
|
+
style = data["style"] || data[:style]
|
|
46
|
+
self.class.new({"items" => items, "style" => style}).render
|
|
28
47
|
end
|
|
29
48
|
end
|
|
30
49
|
end
|