pressroom 0.1.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/MIT-LICENSE +20 -0
- data/README.md +172 -0
- data/Rakefile +8 -0
- data/app/assets/javascripts/pressroom/application.js +18 -0
- data/app/assets/javascripts/pressroom/controllers/editor_controller.js +146 -0
- data/app/assets/javascripts/pressroom/vendor/code.js +181 -0
- data/app/assets/javascripts/pressroom/vendor/delimiter.js +98 -0
- data/app/assets/javascripts/pressroom/vendor/editorjs.js +11217 -0
- data/app/assets/javascripts/pressroom/vendor/header.js +321 -0
- data/app/assets/javascripts/pressroom/vendor/image.js +1103 -0
- data/app/assets/javascripts/pressroom/vendor/inline-code.js +99 -0
- data/app/assets/javascripts/pressroom/vendor/list.js +2091 -0
- data/app/assets/javascripts/pressroom/vendor/marker.js +95 -0
- data/app/assets/javascripts/pressroom/vendor/quote.js +990 -0
- data/app/assets/javascripts/pressroom/vendor/stimulus.js +2563 -0
- data/app/assets/javascripts/pressroom/vendor/turbo.js +7256 -0
- data/app/assets/stylesheets/pressroom/admin.css +216 -0
- data/app/assets/stylesheets/pressroom/content.css +18 -0
- data/app/controllers/pressroom/application_controller.rb +41 -0
- data/app/controllers/pressroom/categories_controller.rb +50 -0
- data/app/controllers/pressroom/posts_controller.rb +98 -0
- data/app/controllers/pressroom/tags_controller.rb +39 -0
- data/app/controllers/pressroom/uploads_controller.rb +52 -0
- data/app/helpers/pressroom/application_helper.rb +4 -0
- data/app/helpers/pressroom/blocks_helper.rb +13 -0
- data/app/jobs/pressroom/application_job.rb +4 -0
- data/app/mailers/pressroom/application_mailer.rb +6 -0
- data/app/models/concerns/pressroom/authorable.rb +23 -0
- data/app/models/concerns/pressroom/sluggable.rb +66 -0
- data/app/models/pressroom/application_record.rb +7 -0
- data/app/models/pressroom/category.rb +54 -0
- data/app/models/pressroom/post.rb +90 -0
- data/app/models/pressroom/tag.rb +30 -0
- data/app/models/pressroom/tagging.rb +12 -0
- data/app/views/layouts/pressroom/admin.html.erb +34 -0
- data/app/views/layouts/pressroom/application.html.erb +17 -0
- data/app/views/pressroom/blocks/_code.html.erb +2 -0
- data/app/views/pressroom/blocks/_delimiter.html.erb +1 -0
- data/app/views/pressroom/blocks/_header.html.erb +5 -0
- data/app/views/pressroom/blocks/_image.html.erb +18 -0
- data/app/views/pressroom/blocks/_list.html.erb +6 -0
- data/app/views/pressroom/blocks/_list_items.html.erb +13 -0
- data/app/views/pressroom/blocks/_paragraph.html.erb +1 -0
- data/app/views/pressroom/blocks/_quote.html.erb +6 -0
- data/app/views/pressroom/categories/_form.html.erb +31 -0
- data/app/views/pressroom/categories/edit.html.erb +3 -0
- data/app/views/pressroom/categories/index.html.erb +34 -0
- data/app/views/pressroom/categories/new.html.erb +3 -0
- data/app/views/pressroom/posts/_form.html.erb +49 -0
- data/app/views/pressroom/posts/_post_row.html.erb +17 -0
- data/app/views/pressroom/posts/edit.html.erb +34 -0
- data/app/views/pressroom/posts/index.html.erb +38 -0
- data/app/views/pressroom/posts/new.html.erb +3 -0
- data/app/views/pressroom/tags/edit.html.erb +24 -0
- data/app/views/pressroom/tags/index.html.erb +27 -0
- data/config/editorjs_manifest.yml +53 -0
- data/config/importmap.rb +18 -0
- data/config/locales/pressroom.en.yml +16 -0
- data/config/locales/transliteration.ru.yml +70 -0
- data/config/locales/transliteration.uk.yml +70 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20260821000001_create_pressroom_posts.rb +42 -0
- data/db/migrate/20260821000002_create_pressroom_taxonomy.rb +33 -0
- data/lib/pressroom/blocks/definition.rb +103 -0
- data/lib/pressroom/blocks/registry.rb +56 -0
- data/lib/pressroom/blocks/renderer.rb +97 -0
- data/lib/pressroom/blocks/sanitizer.rb +36 -0
- data/lib/pressroom/blocks/schema.rb +247 -0
- data/lib/pressroom/blocks/text_extractor.rb +46 -0
- data/lib/pressroom/built_in_blocks.rb +97 -0
- data/lib/pressroom/configuration.rb +64 -0
- data/lib/pressroom/engine.rb +33 -0
- data/lib/pressroom/errors.rb +11 -0
- data/lib/pressroom/feed.rb +69 -0
- data/lib/pressroom/transliteration.rb +213 -0
- data/lib/pressroom/version.rb +3 -0
- data/lib/pressroom.rb +70 -0
- data/lib/tasks/pressroom_tasks.rake +4 -0
- data/lib/tasks/pressroom_vendor.rake +31 -0
- metadata +171 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f4713e4ba3f3eaae896ab1ad8b152749fa9c0be2a66cf11210a57fd43ba5c2d7
|
|
4
|
+
data.tar.gz: cde078d6ebe1273874c9da4610b92866489f163e8b5fbeb4cd6feb1e58177640
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f712eaa9e76b6ffa4bdab60c5fb14e09d2609872d4fdbe2dd5ab8e7e7f247684a4cee01a8de66b3771fbac532381fcf2750fbfde534954ecfc8bbff5d84e2986
|
|
7
|
+
data.tar.gz: 37524af128ab5f3d6f27516a41d51a1a5e52086396d3f3f169acf574f981364e20f191b8e0929803acdf0fcf43292d4b3e6c549f0c1ae129ecb069c2ed121d31
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- Block registry: one Ruby description drives the editor, server-side
|
|
8
|
+
rendering, sanitization and search-text extraction.
|
|
9
|
+
- Seven built-in blocks: paragraph, header, list (nested), quote, code,
|
|
10
|
+
image, delimiter.
|
|
11
|
+
- Article model with drafts, archiving and schedule-free scheduled
|
|
12
|
+
publication.
|
|
13
|
+
- Slugs that survive Cyrillic titles. Transliteration rules for :ru and
|
|
14
|
+
:uk ship with the gem and are applied automatically when the configured
|
|
15
|
+
locale has none, so a Russian title yields a real slug out of the box.
|
|
16
|
+
- Hierarchical categories and flat tags, with no external gems.
|
|
17
|
+
- Mountable admin that inherits the host controller and fails closed
|
|
18
|
+
without an authentication hook.
|
|
19
|
+
- Editor.js, Stimulus and Turbo vendored and pinned through importmap; no
|
|
20
|
+
Node build required.
|
|
21
|
+
- RSS 2.0 and Atom feeds.
|
|
22
|
+
- Optional ActiveStorage-backed image uploads.
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2026 Alex Andreiev
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Pressroom
|
|
2
|
+
|
|
3
|
+
Block-based article publishing for Rails, as a mountable engine.
|
|
4
|
+
|
|
5
|
+
Pressroom adds a block editor, an article model with drafts and scheduled
|
|
6
|
+
publication, categories, tags and feeds to an application you already have.
|
|
7
|
+
It brings no authentication, no user model and no public routes — those stay
|
|
8
|
+
yours.
|
|
9
|
+
|
|
10
|
+
Its distinguishing feature is the **block registry**: a block is described
|
|
11
|
+
once in Ruby, and that single description drives the editor, server-side
|
|
12
|
+
rendering, sanitization and search-text extraction.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
# Gemfile
|
|
18
|
+
gem "pressroom"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bundle install
|
|
23
|
+
bin/rails db:migrate
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Mount the admin:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# config/routes.rb
|
|
30
|
+
mount Pressroom::Engine => "/admin"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# config/initializers/pressroom.rb
|
|
37
|
+
Pressroom.configure do |config|
|
|
38
|
+
# Required. Without it the admin refuses to open.
|
|
39
|
+
config.authenticate_with do
|
|
40
|
+
redirect_to main_app.root_path unless current_user&.editor?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
config.author_class = "User"
|
|
44
|
+
config.transliteration_locale = :ru # slug rules for :ru and :uk ship with the gem
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Pressroom **fails closed**: an admin with no `authenticate_with` hook raises
|
|
49
|
+
rather than letting anyone in.
|
|
50
|
+
|
|
51
|
+
## Rendering an article
|
|
52
|
+
|
|
53
|
+
Pressroom does not own your public routes. Write your own controller and
|
|
54
|
+
view, and render the document:
|
|
55
|
+
|
|
56
|
+
```erb
|
|
57
|
+
<h1><%= @post.title %></h1>
|
|
58
|
+
<%= render_blocks @post %>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Optionally include the structural stylesheet. It contains no typography,
|
|
62
|
+
colour or spacing — only the handful of rules without which a block is
|
|
63
|
+
broken rather than merely unstyled:
|
|
64
|
+
|
|
65
|
+
```erb
|
|
66
|
+
<%= stylesheet_link_tag "pressroom/content" %>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Registering your own block
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
Pressroom.register_block :callout do |b|
|
|
73
|
+
b.tool "Callout" # the Editor.js tool class
|
|
74
|
+
b.schema do
|
|
75
|
+
string :title, required: true
|
|
76
|
+
html :body # html fields are sanitized; string fields are escaped
|
|
77
|
+
enum :tone, in: %w[info warning], default: "info"
|
|
78
|
+
end
|
|
79
|
+
b.partial "blocks/callout"
|
|
80
|
+
b.searchable { |data| data["title"] }
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Pin the JavaScript tool from your own importmap and it appears in the
|
|
85
|
+
toolbar. Field types: `string`, `html`, `integer`, `boolean`, `enum`,
|
|
86
|
+
`array`, `nested`, `tree`.
|
|
87
|
+
|
|
88
|
+
## Publication
|
|
89
|
+
|
|
90
|
+
Publication is a timestamp comparison, not a background job:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
post.update!(status: :published, published_at: Time.current) # live now
|
|
94
|
+
post.update!(status: :published, published_at: 3.days.from_now) # scheduled
|
|
95
|
+
|
|
96
|
+
Pressroom::Post.published # status published AND published_at <= now
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Nothing polls and nothing is enqueued. A scheduled post becomes visible the
|
|
100
|
+
moment its timestamp passes. The admin exposes both as buttons: *Publish
|
|
101
|
+
now*, *Unpublish*, and a *Schedule publication* field.
|
|
102
|
+
|
|
103
|
+
## Feeds
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
feed = Pressroom::Feed.new(
|
|
107
|
+
Pressroom::Post.published.recent.limit(20),
|
|
108
|
+
title: "Example blog",
|
|
109
|
+
url: "https://example.com",
|
|
110
|
+
description: "Latest posts"
|
|
111
|
+
) { |post| post_url(post) }
|
|
112
|
+
|
|
113
|
+
render xml: feed.to_rss # or feed.to_atom
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Search
|
|
117
|
+
|
|
118
|
+
Pressroom maintains a `plain_text` column on every save and does not search
|
|
119
|
+
it. Index it with whatever you already use:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
Pressroom::Post.published.where("plain_text ILIKE ?", "%rails%")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Images
|
|
126
|
+
|
|
127
|
+
The image block renders from a URL always. File uploads additionally require
|
|
128
|
+
ActiveStorage; when it is present the upload endpoint and the editor's image
|
|
129
|
+
tool are enabled automatically. Uploads keep CSRF protection — the editor
|
|
130
|
+
sends the token as a request header.
|
|
131
|
+
|
|
132
|
+
## Built-in blocks
|
|
133
|
+
|
|
134
|
+
`paragraph`, `header`, `list` (nested), `quote`, `code`, `image`,
|
|
135
|
+
`delimiter`. Tables, checklists, embeds and attachments are deliberately not
|
|
136
|
+
included — register them yourself.
|
|
137
|
+
|
|
138
|
+
## JavaScript
|
|
139
|
+
|
|
140
|
+
Editor.js, Stimulus and Turbo are vendored into the gem and pinned through
|
|
141
|
+
importmap. No Node build step is required in your application, and nothing
|
|
142
|
+
is fetched from a CDN, so a strict CSP and an offline network are both fine.
|
|
143
|
+
|
|
144
|
+
Stimulus and Turbo are pinned under private names (`pressroom/stimulus`,
|
|
145
|
+
`pressroom/turbo`) so they cannot collide with your own pins for those
|
|
146
|
+
packages.
|
|
147
|
+
|
|
148
|
+
## Security
|
|
149
|
+
|
|
150
|
+
- Fields declared `html` are sanitized through an allow-list; every other
|
|
151
|
+
field is escaped by ERB.
|
|
152
|
+
- The allow-list is configurable via `sanitizer_tags` and
|
|
153
|
+
`sanitizer_attributes`.
|
|
154
|
+
- An unknown block type, malformed stored JSON, a missing partial or a
|
|
155
|
+
deleted author degrade to a logged skip in production and raise in
|
|
156
|
+
development, so one damaged article cannot take a page down.
|
|
157
|
+
|
|
158
|
+
## Public API and versioning
|
|
159
|
+
|
|
160
|
+
Semver applies to: `Pressroom.register_block` and its block DSL,
|
|
161
|
+
`Pressroom.configure`, `Pressroom::Post`, `Pressroom::Category`,
|
|
162
|
+
`Pressroom::Tag`, `Pressroom::Feed`, the `render_blocks` helper, and the
|
|
163
|
+
stored JSON format. Everything else is internal.
|
|
164
|
+
|
|
165
|
+
## Requirements
|
|
166
|
+
|
|
167
|
+
Ruby >= 3.4, Rails >= 7.2. Tested against Rails 7.2, 8.0 and 8.1 on
|
|
168
|
+
PostgreSQL and SQLite.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "pressroom/turbo"
|
|
2
|
+
import { Application } from "pressroom/stimulus"
|
|
3
|
+
import PressroomEditorController from "pressroom/controllers/editor_controller"
|
|
4
|
+
|
|
5
|
+
// Turbo is loaded here on purpose. A host application almost certainly runs
|
|
6
|
+
// Turbo Drive, so navigating into the admin caches these pages whether or not
|
|
7
|
+
// this layout asks for it -- which means the editor's teardown really does
|
|
8
|
+
// have to work. Loading it here makes the admin behave in tests the way it
|
|
9
|
+
// behaves in production instead of hiding the problem.
|
|
10
|
+
//
|
|
11
|
+
// The admin runs its own Stimulus application rather than reusing the host's.
|
|
12
|
+
// The admin layout is ours and loads only this module, so there is nothing to
|
|
13
|
+
// share, and a self-contained instance means the gem never depends on how the
|
|
14
|
+
// host names or boots its controllers.
|
|
15
|
+
const application = Application.start()
|
|
16
|
+
application.register("pressroom-editor", PressroomEditorController)
|
|
17
|
+
|
|
18
|
+
export { application }
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Controller } from "pressroom/stimulus"
|
|
2
|
+
import EditorJS from "@editorjs/editorjs"
|
|
3
|
+
import Header from "@editorjs/header"
|
|
4
|
+
import List from "@editorjs/list"
|
|
5
|
+
import Quote from "@editorjs/quote"
|
|
6
|
+
import CodeTool from "@editorjs/code"
|
|
7
|
+
import ImageTool from "@editorjs/image"
|
|
8
|
+
import Delimiter from "@editorjs/delimiter"
|
|
9
|
+
import Marker from "@editorjs/marker"
|
|
10
|
+
import InlineCode from "@editorjs/inline-code"
|
|
11
|
+
|
|
12
|
+
export default class extends Controller {
|
|
13
|
+
static targets = ["field", "holder"]
|
|
14
|
+
static values = { uploadUrl: String }
|
|
15
|
+
|
|
16
|
+
connect() {
|
|
17
|
+
this.beforeCache = this.beforeCache.bind(this)
|
|
18
|
+
this.onSubmitClick = this.onSubmitClick.bind(this)
|
|
19
|
+
this.flushing = false
|
|
20
|
+
|
|
21
|
+
this.editor = new EditorJS({
|
|
22
|
+
holder: this.holderTarget,
|
|
23
|
+
data: this.readDocument(),
|
|
24
|
+
tools: this.tools(),
|
|
25
|
+
// The hidden field is kept current as the author types rather than
|
|
26
|
+
// serialized when the form is submitted. Intercepting submit would
|
|
27
|
+
// mean preventDefault, an await, and then re-submitting -- and a
|
|
28
|
+
// nested requestSubmit() raised from inside a submit event is
|
|
29
|
+
// silently ignored by the browser. Keeping the field fresh leaves
|
|
30
|
+
// form submission entirely to Turbo, with nothing to re-enter.
|
|
31
|
+
onChange: () => this.persist()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
document.addEventListener("turbo:before-cache", this.beforeCache)
|
|
35
|
+
this.form?.addEventListener("click", this.onSubmitClick, true)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
disconnect() {
|
|
39
|
+
document.removeEventListener("turbo:before-cache", this.beforeCache)
|
|
40
|
+
this.form?.removeEventListener("click", this.onSubmitClick, true)
|
|
41
|
+
this.teardown()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get form() {
|
|
45
|
+
return this.element.closest("form")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
csrfToken() {
|
|
49
|
+
return document.querySelector('meta[name="csrf-token"]')?.content || ""
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Flush the editor before the form is submitted.
|
|
53
|
+
//
|
|
54
|
+
// This hooks the submit button's click rather than the form's submit
|
|
55
|
+
// event on purpose. Serializing is asynchronous, so the handler must
|
|
56
|
+
// stop the default action, await, and then let it happen again -- and a
|
|
57
|
+
// requestSubmit() raised from inside a submit event is silently dropped
|
|
58
|
+
// by the browser. Re-dispatching a click is not, so Turbo still handles
|
|
59
|
+
// the submission normally.
|
|
60
|
+
//
|
|
61
|
+
// Editor.js debounces onChange, so relying on that alone would lose the
|
|
62
|
+
// last thing an author typed before hitting save.
|
|
63
|
+
async onSubmitClick(event) {
|
|
64
|
+
if (this.flushing) return
|
|
65
|
+
|
|
66
|
+
const submitter = event.target.closest?.("input[type=submit], button[type=submit]")
|
|
67
|
+
if (!submitter) return
|
|
68
|
+
|
|
69
|
+
event.preventDefault()
|
|
70
|
+
event.stopPropagation()
|
|
71
|
+
|
|
72
|
+
this.flushing = true
|
|
73
|
+
await this.persist()
|
|
74
|
+
submitter.click()
|
|
75
|
+
this.flushing = false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
readDocument() {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(this.fieldTarget.value || "{}")
|
|
81
|
+
return { blocks: parsed.blocks || [] }
|
|
82
|
+
} catch {
|
|
83
|
+
return { blocks: [] }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
tools() {
|
|
88
|
+
const tools = {
|
|
89
|
+
header: Header,
|
|
90
|
+
list: List,
|
|
91
|
+
quote: Quote,
|
|
92
|
+
code: CodeTool,
|
|
93
|
+
delimiter: Delimiter,
|
|
94
|
+
marker: Marker,
|
|
95
|
+
inlineCode: InlineCode
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The image tool is only useful when the host exposes an upload
|
|
99
|
+
// endpoint, which it does only when ActiveStorage is available.
|
|
100
|
+
if (this.hasUploadUrlValue && this.uploadUrlValue) {
|
|
101
|
+
tools.image = {
|
|
102
|
+
class: ImageTool,
|
|
103
|
+
config: {
|
|
104
|
+
endpoints: { byFile: this.uploadUrlValue },
|
|
105
|
+
// Editor.js posts FormData without a Rails CSRF token, so it is
|
|
106
|
+
// supplied as a header instead of disabling CSRF protection.
|
|
107
|
+
additionalRequestHeaders: { "X-CSRF-Token": this.csrfToken() }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return tools
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async persist() {
|
|
116
|
+
if (!this.editor) return
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const output = await this.editor.save()
|
|
120
|
+
this.fieldTarget.value = JSON.stringify(output)
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error("pressroom: could not serialize the editor", error)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Turbo caches a snapshot of the page on navigation. A live editor left
|
|
127
|
+
// in that snapshot is restored as markup and then initialized a second
|
|
128
|
+
// time, producing two editors and duplicated content.
|
|
129
|
+
beforeCache() {
|
|
130
|
+
this.teardown()
|
|
131
|
+
this.holderTarget.innerHTML = ""
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
teardown() {
|
|
135
|
+
const editor = this.editor
|
|
136
|
+
if (!editor) return
|
|
137
|
+
|
|
138
|
+
this.editor = null
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
editor.destroy()
|
|
142
|
+
} catch {
|
|
143
|
+
// The editor may not have finished initializing; nothing to clean up.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ce-code__textarea{min-height:200px;font-family:Menlo,Monaco,Consolas,Courier New,monospace;color:#41314e;line-height:1.6em;font-size:12px;background:#f8f7fa;border:1px solid #f1f1f4;box-shadow:none;white-space:pre;word-wrap:normal;overflow-x:auto;resize:vertical}")),document.head.appendChild(e)}}catch(o){console.error("vite-plugin-css-injected-by-js",o)}})();
|
|
2
|
+
function c(l, t) {
|
|
3
|
+
let a = "";
|
|
4
|
+
for (; a !== `
|
|
5
|
+
` && t > 0; )
|
|
6
|
+
t = t - 1, a = l.substr(t, 1);
|
|
7
|
+
return a === `
|
|
8
|
+
` && (t += 1), t;
|
|
9
|
+
}
|
|
10
|
+
const h = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 8L5 12L9 16"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 8L19 12L15 16"/></svg>';
|
|
11
|
+
/**
|
|
12
|
+
* CodeTool for Editor.js
|
|
13
|
+
* @version 2.0.0
|
|
14
|
+
* @license MIT
|
|
15
|
+
*/
|
|
16
|
+
class d {
|
|
17
|
+
/**
|
|
18
|
+
* Notify core that read-only mode is supported
|
|
19
|
+
* @returns true if read-only mode is supported
|
|
20
|
+
*/
|
|
21
|
+
static get isReadOnlySupported() {
|
|
22
|
+
return !0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Allows pressing Enter key to create line breaks inside the CodeTool textarea
|
|
26
|
+
* This enables multi-line input within the code editor.
|
|
27
|
+
* @returns true if line breaks are allowed in the textarea
|
|
28
|
+
*/
|
|
29
|
+
static get enableLineBreaks() {
|
|
30
|
+
return !0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Render plugin`s main Element and fill it with saved data
|
|
34
|
+
* @param options - tool constricting options
|
|
35
|
+
* @param options.data — previously saved plugin code
|
|
36
|
+
* @param options.config - user config for Tool
|
|
37
|
+
* @param options.api - Editor.js API
|
|
38
|
+
* @param options.readOnly - read only mode flag
|
|
39
|
+
*/
|
|
40
|
+
constructor({ data: t, config: e, api: a, readOnly: s }) {
|
|
41
|
+
this.api = a, this.readOnly = s, this.placeholder = this.api.i18n.t(e.placeholder || d.DEFAULT_PLACEHOLDER), this.CSS = {
|
|
42
|
+
baseClass: this.api.styles.block,
|
|
43
|
+
input: this.api.styles.input,
|
|
44
|
+
wrapper: "ce-code",
|
|
45
|
+
textarea: "ce-code__textarea"
|
|
46
|
+
}, this.nodes = {
|
|
47
|
+
holder: null,
|
|
48
|
+
textarea: null
|
|
49
|
+
}, this.data = {
|
|
50
|
+
code: t.code ?? ""
|
|
51
|
+
}, this.nodes.holder = this.drawView();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Return Tool's view
|
|
55
|
+
* @returns this.nodes.holder - Code's wrapper
|
|
56
|
+
*/
|
|
57
|
+
render() {
|
|
58
|
+
return this.nodes.holder;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Extract Tool's data from the view
|
|
62
|
+
* @param codeWrapper - CodeTool's wrapper, containing textarea with code
|
|
63
|
+
* @returns - saved plugin code
|
|
64
|
+
*/
|
|
65
|
+
save(t) {
|
|
66
|
+
return {
|
|
67
|
+
code: t.querySelector("textarea").value
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* onPaste callback fired from Editor`s core
|
|
72
|
+
* @param event - event with pasted content
|
|
73
|
+
*/
|
|
74
|
+
onPaste(t) {
|
|
75
|
+
switch (t.type) {
|
|
76
|
+
case "tag": {
|
|
77
|
+
const e = t.detail.data;
|
|
78
|
+
this.handleHTMLPaste(e);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Returns Tool`s data from private property
|
|
85
|
+
* @returns
|
|
86
|
+
*/
|
|
87
|
+
get data() {
|
|
88
|
+
return this._data;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Set Tool`s data to private property and update view
|
|
92
|
+
* @param data - saved tool data
|
|
93
|
+
*/
|
|
94
|
+
set data(t) {
|
|
95
|
+
this._data = t, this.nodes.textarea && (this.nodes.textarea.value = t.code);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get Tool toolbox settings.
|
|
99
|
+
* Provides the icon and title to display in the toolbox for the CodeTool.
|
|
100
|
+
* @returns An object containing:
|
|
101
|
+
* - icon: SVG representation of the Tool's icon
|
|
102
|
+
* - title: Title to show in the toolbox
|
|
103
|
+
*/
|
|
104
|
+
static get toolbox() {
|
|
105
|
+
return {
|
|
106
|
+
icon: h,
|
|
107
|
+
title: "Code"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Default placeholder for CodeTool's textarea
|
|
112
|
+
* @returns
|
|
113
|
+
*/
|
|
114
|
+
static get DEFAULT_PLACEHOLDER() {
|
|
115
|
+
return "Enter a code";
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Used by Editor.js paste handling API.
|
|
119
|
+
* Provides configuration to handle CODE tag.
|
|
120
|
+
* @returns
|
|
121
|
+
*/
|
|
122
|
+
static get pasteConfig() {
|
|
123
|
+
return {
|
|
124
|
+
tags: ["pre"]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Automatic sanitize config
|
|
129
|
+
* @returns
|
|
130
|
+
*/
|
|
131
|
+
static get sanitize() {
|
|
132
|
+
return {
|
|
133
|
+
code: !0
|
|
134
|
+
// Allow HTML tags
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Handles Tab key pressing (adds/removes indentations)
|
|
139
|
+
* @param event - keydown
|
|
140
|
+
*/
|
|
141
|
+
tabHandler(t) {
|
|
142
|
+
t.stopPropagation(), t.preventDefault();
|
|
143
|
+
const e = t.target, a = t.shiftKey, s = e.selectionStart, r = e.value, n = " ";
|
|
144
|
+
let i;
|
|
145
|
+
if (!a)
|
|
146
|
+
i = s + n.length, e.value = r.substring(0, s) + n + r.substring(s);
|
|
147
|
+
else {
|
|
148
|
+
const o = c(r, s);
|
|
149
|
+
if (r.substr(o, n.length) !== n)
|
|
150
|
+
return;
|
|
151
|
+
e.value = r.substring(0, o) + r.substring(o + n.length), i = s - n.length;
|
|
152
|
+
}
|
|
153
|
+
e.setSelectionRange(i, i);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Create Tool's view
|
|
157
|
+
* @returns
|
|
158
|
+
*/
|
|
159
|
+
drawView() {
|
|
160
|
+
const t = document.createElement("div"), e = document.createElement("textarea");
|
|
161
|
+
return t.classList.add(this.CSS.baseClass, this.CSS.wrapper), e.classList.add(this.CSS.textarea, this.CSS.input), e.value = this.data.code, e.placeholder = this.placeholder, this.readOnly && (e.disabled = !0), t.appendChild(e), e.addEventListener("keydown", (a) => {
|
|
162
|
+
switch (a.code) {
|
|
163
|
+
case "Tab":
|
|
164
|
+
this.tabHandler(a);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}), this.nodes.textarea = e, t;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Extracts the code content from the pasted element's innerHTML and populates the tool's data.
|
|
171
|
+
* @param element - pasted HTML element
|
|
172
|
+
*/
|
|
173
|
+
handleHTMLPaste(t) {
|
|
174
|
+
this.data = {
|
|
175
|
+
code: t.innerHTML
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export {
|
|
180
|
+
d as default
|
|
181
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode('.ce-delimiter{line-height:1.6em;width:100%;text-align:center}.ce-delimiter:before{display:inline-block;content:"***";font-size:30px;line-height:65px;height:30px;letter-spacing:.2em}')),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})();
|
|
2
|
+
const r = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><line x1="6" x2="10" y1="12" y2="12" stroke="currentColor" stroke-linecap="round" stroke-width="2"/><line x1="14" x2="18" y1="12" y2="12" stroke="currentColor" stroke-linecap="round" stroke-width="2"/></svg>';
|
|
3
|
+
/**
|
|
4
|
+
* Delimiter Block for the Editor.js.
|
|
5
|
+
*
|
|
6
|
+
* @author CodeX (team@ifmo.su)
|
|
7
|
+
* @copyright CodeX 2018
|
|
8
|
+
* @license The MIT License (MIT)
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
class n {
|
|
12
|
+
/**
|
|
13
|
+
* Notify core that read-only mode is supported
|
|
14
|
+
* @return {boolean}
|
|
15
|
+
*/
|
|
16
|
+
static get isReadOnlySupported() {
|
|
17
|
+
return !0;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Allow Tool to have no content
|
|
21
|
+
* @return {boolean}
|
|
22
|
+
*/
|
|
23
|
+
static get contentless() {
|
|
24
|
+
return !0;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Render plugin`s main Element and fill it with saved data
|
|
28
|
+
*
|
|
29
|
+
* @param {{data: DelimiterData, config: object, api: object}}
|
|
30
|
+
* data — previously saved data
|
|
31
|
+
* config - user config for Tool
|
|
32
|
+
* api - Editor.js API
|
|
33
|
+
*/
|
|
34
|
+
constructor({ data: t, config: s, api: e }) {
|
|
35
|
+
this.api = e, this._CSS = {
|
|
36
|
+
block: this.api.styles.block,
|
|
37
|
+
wrapper: "ce-delimiter"
|
|
38
|
+
}, this._element = this.drawView(), this.data = t;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create Tool's view
|
|
42
|
+
* @return {HTMLDivElement}
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
drawView() {
|
|
46
|
+
let t = document.createElement("div");
|
|
47
|
+
return t.classList.add(this._CSS.wrapper, this._CSS.block), t;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Return Tool's view
|
|
51
|
+
* @returns {HTMLDivElement}
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
render() {
|
|
55
|
+
return this._element;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract Tool's data from the view
|
|
59
|
+
* @param {HTMLDivElement} toolsContent - Paragraph tools rendered view
|
|
60
|
+
* @returns {DelimiterData} - saved data
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
save(t) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get Tool toolbox settings
|
|
68
|
+
* icon - Tool icon's SVG
|
|
69
|
+
* title - title to show in toolbox
|
|
70
|
+
*
|
|
71
|
+
* @return {{icon: string, title: string}}
|
|
72
|
+
*/
|
|
73
|
+
static get toolbox() {
|
|
74
|
+
return {
|
|
75
|
+
icon: r,
|
|
76
|
+
title: "Delimiter"
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Delimiter onPaste configuration
|
|
81
|
+
*
|
|
82
|
+
* @public
|
|
83
|
+
*/
|
|
84
|
+
static get pasteConfig() {
|
|
85
|
+
return { tags: ["HR"] };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* On paste callback that is fired from Editor
|
|
89
|
+
*
|
|
90
|
+
* @param {PasteEvent} event - event with pasted data
|
|
91
|
+
*/
|
|
92
|
+
onPaste(t) {
|
|
93
|
+
this.data = {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
n as default
|
|
98
|
+
};
|