lexxy-realtime 0.4.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/LICENSE +21 -0
- data/README.md +129 -0
- data/app/assets/javascript/lexxy_realtime/lexical.js +11418 -0
- data/app/assets/javascript/lexxy_realtime/lexical.js.map +1 -0
- data/app/assets/javascript/lexxy_realtime/lexical.min.js +17 -0
- data/app/assets/javascript/lexxy_realtime/lexxy-realtime.js +15357 -0
- data/app/assets/javascript/lexxy_realtime/lexxy-realtime.js.map +1 -0
- data/app/assets/javascript/lexxy_realtime/lexxy-realtime.min.js +6 -0
- data/app/assets/javascript/lexxy_realtime/lexxy.js +30472 -0
- data/app/assets/javascript/lexxy_realtime/lexxy.js.map +1 -0
- data/app/assets/javascript/lexxy_realtime/lexxy.min.js +306 -0
- data/lib/generators/lexxy_realtime/install/USAGE +15 -0
- data/lib/generators/lexxy_realtime/install/install_generator.rb +84 -0
- data/lib/generators/lexxy_realtime/install/templates/application_cable_channel.rb +6 -0
- data/lib/generators/lexxy_realtime/install/templates/application_cable_connection.rb +13 -0
- data/lib/generators/lexxy_realtime/install/templates/document_channel.rb +57 -0
- data/lib/lexxy-realtime.rb +4 -0
- data/lib/lexxy_realtime/collaborative.rb +84 -0
- data/lib/lexxy_realtime/engine.rb +26 -0
- data/lib/lexxy_realtime/form_builder.rb +41 -0
- data/lib/lexxy_realtime/version.rb +5 -0
- data/lib/lexxy_realtime.rb +38 -0
- metadata +123 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1ca9f7e8da5b292e73222ff67b6f49f6abe35825faf45af947157d4442fa37ce
|
|
4
|
+
data.tar.gz: a5aa12d0c2835df82d5eefa99e884b1920af507e824eb7ee3ee032d8b1c06a41
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2eb4bbdc5584615480b1b8cde54df071b1a38cce8c84cd6eaca8e14dbce7a57617f3f5c758d9b3c9dbc226a2c69c15d2f1a84ef69b5aa986824c7182498fb420
|
|
7
|
+
data.tar.gz: a9ec33bca8104051993e0d4a2fc6233afb459bf9e8a6cd63d72b1801f316244d5ef161d38e33d1ca7c325446f87d16622eb8bd7ce3cb20952eaf13d146d0f0ed
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 JP Camara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# lexxy-realtime (Rails gem)
|
|
2
|
+
|
|
3
|
+
Collaborative [Lexxy](https://github.com/basecamp/lexxy) editing for
|
|
4
|
+
Rails, backed by [yrby](https://github.com/jpcamara/yrby)'s Ruby
|
|
5
|
+
implementation of Yjs.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Requires Ruby 3.4+, Rails 8.0.2+, and a working
|
|
10
|
+
[Lexxy](https://github.com/basecamp/lexxy) installation (its gem and
|
|
11
|
+
editor JavaScript). Import maps and bundlers (esbuild, Vite, webpack)
|
|
12
|
+
both work.
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Gemfile
|
|
16
|
+
gem "lexxy-realtime"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bin/rails generate lexxy_realtime:install
|
|
21
|
+
bin/rails db:migrate
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The generator creates `app/channels/document_channel.rb`, installs
|
|
25
|
+
yrby's table migration, and adds the standard Action Cable files when
|
|
26
|
+
they are missing. The `Y::Document` and `Y::DocumentUpdate` models come
|
|
27
|
+
from `yrby-rails`.
|
|
28
|
+
|
|
29
|
+
With a bundler, install the JavaScript package and import it next to
|
|
30
|
+
your Lexxy import:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install lexxy-realtime # yarn, bun, and pnpm also work
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
With import maps, there is no npm install; the generator pins assets
|
|
37
|
+
this gem ships:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# config/importmap.rb, added by the generator
|
|
41
|
+
pin "lexical", to: "lexxy_realtime/lexical.js"
|
|
42
|
+
pin "@37signals/lexxy", to: "lexxy_realtime/lexxy.js"
|
|
43
|
+
pin "lexxy-realtime", to: "lexxy_realtime/lexxy-realtime.js"
|
|
44
|
+
pin "@rails/activestorage", to: "activestorage.esm.js"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`lexical` is the one module the Lexxy and lexxy-realtime bundles share,
|
|
48
|
+
so both ship with it external and it resolves through its own pin. The
|
|
49
|
+
`@37signals/lexxy` pin must point at this gem's build: Lexxy's own
|
|
50
|
+
asset bundles a second copy of `lexical`, and two copies break the
|
|
51
|
+
collaboration binding, so remove any pin of Lexxy's asset. Keep
|
|
52
|
+
`stylesheet_link_tag "lexxy"` for the editor's CSS. These assets are a
|
|
53
|
+
stopgap until Lexxy ships import-map-ready builds itself.
|
|
54
|
+
|
|
55
|
+
Either way, your entrypoint imports both:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import "@37signals/lexxy"
|
|
59
|
+
import "lexxy-realtime"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Use
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
class Post < ApplicationRecord
|
|
66
|
+
has_collaborative_rich_text :body # a regular Action Text attribute
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```erb
|
|
71
|
+
<%= form_with model: @post do |form| %>
|
|
72
|
+
<%= form.collaborative_rich_textarea :body %>
|
|
73
|
+
<% end %>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Add your app's access check to `authorized?` in the generated channel,
|
|
77
|
+
then open the page in two browsers and edit together. The record must be persisted
|
|
78
|
+
(the document key derives from it). A record with an existing body works: the
|
|
79
|
+
first collaborative open seeds the document from it.
|
|
80
|
+
|
|
81
|
+
Encryption works the way Action Text's does:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
has_collaborative_rich_text :body, encrypted: true
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The rendered body goes through `ActionText::EncryptedRichText`, and the
|
|
88
|
+
collaborative document (CRDT state and update payloads) is stored through
|
|
89
|
+
yrby's `Y::EncryptedDocument`. Both use Active Record encryption, so the
|
|
90
|
+
app must configure encryption keys. Without Action Text, declare
|
|
91
|
+
`encrypts` on the plain attribute yourself.
|
|
92
|
+
|
|
93
|
+
Use it for new attributes. Existing plaintext rows need migration
|
|
94
|
+
before you add `encrypted: true`: enable `support_unencrypted_data`,
|
|
95
|
+
rewrite each document, update, and rich-text row through its encrypted
|
|
96
|
+
class, then turn it back off. There is no built-in task for that yet.
|
|
97
|
+
And if your channel came from an earlier pre-release checkout, update it
|
|
98
|
+
to the current record-based storage first; a channel calling
|
|
99
|
+
`Y::Document` directly stores encrypted attributes as plaintext.
|
|
100
|
+
|
|
101
|
+
## How the body stays current
|
|
102
|
+
|
|
103
|
+
The channel records each CRDT update, renders the full document with
|
|
104
|
+
`Y::Lexxy`, and saves the HTML through the Action Text writer. This
|
|
105
|
+
happens synchronously in `refresh_collaborative_rich_text`, so
|
|
106
|
+
reads use the stored `post.body` value.
|
|
107
|
+
|
|
108
|
+
If rendering fails, the update remains stored and the error is logged.
|
|
109
|
+
The next successful update renders the full document again. Until then,
|
|
110
|
+
`post.body` keeps its previous value.
|
|
111
|
+
|
|
112
|
+
## Access control
|
|
113
|
+
|
|
114
|
+
The form helper gives clients a signed GlobalID scoped to one record and
|
|
115
|
+
field. `DocumentChannel` uses it to locate the record. Put the user
|
|
116
|
+
access check in `authorized?` (for example,
|
|
117
|
+
`record.editable_by?(current_user)`).
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
LexxyRealtime.identity = ->(view) { { name: view.current_user.handle, color: nil } }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
By default, identity uses the first available `current_user` value from
|
|
126
|
+
`name`, `username`, or `handle`, then falls back to `"Anonymous"`.
|
|
127
|
+
|
|
128
|
+
Full documentation, the demo app, and the JavaScript package:
|
|
129
|
+
[repository README](https://github.com/jpcamara/lexxy-realtime#readme).
|