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
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Installs the channel and database tables required for collaborative
|
|
3
|
+
Lexxy editing. The channel locates records by signed GlobalID and
|
|
4
|
+
saves rendered updates to Action Text.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
bin/rails generate lexxy_realtime:install
|
|
8
|
+
|
|
9
|
+
This will create:
|
|
10
|
+
app/channels/document_channel.rb
|
|
11
|
+
app/channels/application_cable/{connection,channel}.rb (if missing)
|
|
12
|
+
db/migrate/XXXXXXXX_create_y_tables.rb
|
|
13
|
+
The models ship in the gems (Y::Document, Y::DocumentUpdate).
|
|
14
|
+
The generator will remind you to add `import "lexxy-realtime"` to
|
|
15
|
+
your JavaScript entrypoint.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "generators/yrby/tables/tables_generator"
|
|
5
|
+
|
|
6
|
+
module LexxyRealtime
|
|
7
|
+
module Generators
|
|
8
|
+
# Installs the document channel, the storage migration (via yrby's
|
|
9
|
+
# generator), and the Action Cable boilerplate when missing.
|
|
10
|
+
class InstallGenerator < Rails::Generators::Base
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
# rails new --skip-action-cable leaves nothing for the channel to
|
|
14
|
+
# inherit from.
|
|
15
|
+
def check_action_cable
|
|
16
|
+
return if defined?(ActionCable)
|
|
17
|
+
|
|
18
|
+
say "Action Cable is not loaded (rails new --skip-action-cable?). " \
|
|
19
|
+
'Add `require "action_cable/engine"` to config/application.rb, ' \
|
|
20
|
+
"create config/cable.yml, and re-run this generator.", :red
|
|
21
|
+
raise Thor::Error, "lexxy_realtime:install requires Action Cable"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create_application_cable
|
|
25
|
+
%w[connection channel].each do |file|
|
|
26
|
+
destination = "app/channels/application_cable/#{file}.rb"
|
|
27
|
+
next if File.exist?(File.join(destination_root, destination))
|
|
28
|
+
|
|
29
|
+
template "application_cable_#{file}.rb", destination
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_channel
|
|
34
|
+
template "document_channel.rb", "app/channels/document_channel.rb"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# yrby owns the models and their migration.
|
|
38
|
+
def create_tables
|
|
39
|
+
invoke "yrby:tables"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Import-map apps get pins to the assets this gem ships. The Lexxy
|
|
43
|
+
# pin must point at this gem's build (Lexxy's own asset bundles a
|
|
44
|
+
# second copy of lexical, which breaks collaboration), so an
|
|
45
|
+
# existing @37signals/lexxy pin is left for the app to resolve.
|
|
46
|
+
def add_importmap_pins
|
|
47
|
+
return unless File.exist?(File.join(destination_root, "config/importmap.rb"))
|
|
48
|
+
return if File.read(File.join(destination_root, "config/importmap.rb")).include?("lexxy_realtime/")
|
|
49
|
+
|
|
50
|
+
append_to_file "config/importmap.rb", <<~RUBY
|
|
51
|
+
|
|
52
|
+
# lexxy-realtime. lexical is shared between the Lexxy and
|
|
53
|
+
# lexxy-realtime bundles; @37signals/lexxy must point at the
|
|
54
|
+
# lexxy_realtime build, not Lexxy's own asset.
|
|
55
|
+
pin "lexical", to: "lexxy_realtime/lexical.js"
|
|
56
|
+
pin "@37signals/lexxy", to: "lexxy_realtime/lexxy.js"
|
|
57
|
+
pin "lexxy-realtime", to: "lexxy_realtime/lexxy-realtime.js"
|
|
58
|
+
pin "@rails/activestorage", to: "activestorage.esm.js"
|
|
59
|
+
RUBY
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def show_next_steps
|
|
63
|
+
say <<~NEXT
|
|
64
|
+
|
|
65
|
+
lexxy-realtime is installed. Lexxy itself (the gem and its editor
|
|
66
|
+
JS) must already be installed and working. Next steps:
|
|
67
|
+
|
|
68
|
+
1. bin/rails db:migrate
|
|
69
|
+
2. Wire up the JavaScript. With import maps, the generator
|
|
70
|
+
added pins; import "@37signals/lexxy" and "lexxy-realtime"
|
|
71
|
+
from your entrypoint, and remove any pin for Lexxy's own
|
|
72
|
+
asset. With a bundler, install the lexxy-realtime npm
|
|
73
|
+
package and import it.
|
|
74
|
+
3. Declare `has_collaborative_rich_text :body` on a model and
|
|
75
|
+
render it with `<%= form.collaborative_rich_textarea :body %>`.
|
|
76
|
+
4. Update `authorized?` in app/channels/document_channel.rb
|
|
77
|
+
to check the current user.
|
|
78
|
+
|
|
79
|
+
Optional: set cursor names with `LexxyRealtime.identity`.
|
|
80
|
+
NEXT
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ApplicationCable
|
|
4
|
+
class Connection < ActionCable::Connection::Base
|
|
5
|
+
# Expose the current user to DocumentChannel#authorized?:
|
|
6
|
+
#
|
|
7
|
+
# identified_by :current_user
|
|
8
|
+
#
|
|
9
|
+
# def connect
|
|
10
|
+
# self.current_user = User.find_by(id: cookies.signed[:user_id]) || reject_unauthorized_connection
|
|
11
|
+
# end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Syncs clients with the record's collaborative document. After storing
|
|
4
|
+
# each update, the Action Text attribute is refreshed from the full
|
|
5
|
+
# document.
|
|
6
|
+
class DocumentChannel < ApplicationCable::Channel
|
|
7
|
+
include Y::ActionCable
|
|
8
|
+
|
|
9
|
+
# Storage routes through the record's association, so an encrypted
|
|
10
|
+
# attribute reads and writes through Y::EncryptedDocument.
|
|
11
|
+
on_load { |_key| record.find_or_create_collaborative_document(field).load_state }
|
|
12
|
+
on_change do |key, update|
|
|
13
|
+
record.find_or_create_collaborative_document(field).append(update)
|
|
14
|
+
# Log render failures. The stored document renders again after the
|
|
15
|
+
# next update. Raising would make the client resend an update the
|
|
16
|
+
# server already has.
|
|
17
|
+
begin
|
|
18
|
+
record.refresh_collaborative_rich_text(field)
|
|
19
|
+
rescue StandardError => e
|
|
20
|
+
Rails.logger.error("lexxy-realtime render failed for #{key}: #{e.class}: #{e.message}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def subscribed
|
|
25
|
+
reject and return unless record&.collaborative_rich_text?(field)
|
|
26
|
+
reject and return unless authorized?
|
|
27
|
+
|
|
28
|
+
sync_subscribed(record.find_or_create_collaborative_document(field).key)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def receive(data)
|
|
32
|
+
return unless record
|
|
33
|
+
|
|
34
|
+
sync_receive(data, record.find_or_create_collaborative_document(field).key)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Check whether the current user may edit this record, e.g.
|
|
40
|
+
# record.editable_by?(current_user) with identified_by :current_user
|
|
41
|
+
# on the connection. Nothing connects until this returns true.
|
|
42
|
+
def authorized?
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Invalid, stale, or field-mismatched tokens return nil and are
|
|
47
|
+
# rejected by subscribed.
|
|
48
|
+
def record
|
|
49
|
+
@record ||= GlobalID::Locator.locate_signed(params[:sgid], for: LexxyRealtime.sgid_purpose(field))
|
|
50
|
+
rescue ActiveRecord::RecordNotFound
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def field
|
|
55
|
+
params[:field].to_s
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module LexxyRealtime
|
|
6
|
+
# Adds Y::Document-backed collaboration to an attribute. When Action
|
|
7
|
+
# Text is available, it declares the corresponding +has_rich_text+
|
|
8
|
+
# association.
|
|
9
|
+
module Collaborative
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
class_methods do
|
|
13
|
+
def has_collaborative_rich_text(name, **options) # rubocop:disable Naming/PredicatePrefix
|
|
14
|
+
include Model unless include?(Model)
|
|
15
|
+
has_rich_text(name, **options) if respond_to?(:has_rich_text)
|
|
16
|
+
self.collaborative_rich_text_names = (collaborative_rich_text_names + [name.to_sym]).freeze
|
|
17
|
+
|
|
18
|
+
# encrypted: true encrypts both halves: has_rich_text gets the
|
|
19
|
+
# option (an encrypted body), and the document stores CRDT state
|
|
20
|
+
# through Y::EncryptedDocument. Without Action Text, declare
|
|
21
|
+
# +encrypts+ on the plain attribute yourself.
|
|
22
|
+
document_class = options[:encrypted] ? "Y::EncryptedDocument" : "Y::Document"
|
|
23
|
+
has_one :"collaborative_document_#{name}", -> { where(name: name) },
|
|
24
|
+
class_name: document_class, as: :record, inverse_of: :record, dependent: :destroy
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The instance API, present only on models that declared an attribute.
|
|
29
|
+
module Model
|
|
30
|
+
extend ActiveSupport::Concern
|
|
31
|
+
|
|
32
|
+
included do
|
|
33
|
+
class_attribute :collaborative_rich_text_names, instance_writer: false, default: [].freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def collaborative_rich_text?(name) = collaborative_rich_text_names.include?(name.to_sym)
|
|
37
|
+
|
|
38
|
+
# The document, if collaboration has started (nil until the first join).
|
|
39
|
+
def collaborative_document(name) = public_send("collaborative_document_#{name}")
|
|
40
|
+
|
|
41
|
+
# Creates the document on first use. The association supplies the
|
|
42
|
+
# class, so an encrypted attribute gets a Y::EncryptedDocument.
|
|
43
|
+
def find_or_create_collaborative_document(name)
|
|
44
|
+
collaborative_document(name) || begin
|
|
45
|
+
association(:"collaborative_document_#{name}").klass.for(self, name)
|
|
46
|
+
public_send("reload_collaborative_document_#{name}")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Reloads and renders the document while holding the record lock,
|
|
51
|
+
# then saves the HTML through the attribute writer. Returns false
|
|
52
|
+
# when the document has no state.
|
|
53
|
+
def refresh_collaborative_rich_text(name)
|
|
54
|
+
ensure_collaborative!(name)
|
|
55
|
+
|
|
56
|
+
document = collaborative_document(name)
|
|
57
|
+
return false unless document
|
|
58
|
+
|
|
59
|
+
with_lock do
|
|
60
|
+
strict_loading!(false) if strict_loading? # the writer lazily loads the rich-text row
|
|
61
|
+
state = document.reload.load_state
|
|
62
|
+
break false if state.nil?
|
|
63
|
+
|
|
64
|
+
doc = Y::Doc.new
|
|
65
|
+
doc.apply_update(state)
|
|
66
|
+
html = Y::Lexxy.new(doc).to_html
|
|
67
|
+
break false if html.nil?
|
|
68
|
+
|
|
69
|
+
public_send("#{name}=", html)
|
|
70
|
+
save!(validate: false) # collaboration updates should not run unrelated model validations
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def ensure_collaborative!(name)
|
|
78
|
+
return if collaborative_rich_text?(name)
|
|
79
|
+
|
|
80
|
+
raise ArgumentError, "#{name.inspect} is not collaborative on #{self.class.name}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
require "action_dispatch" # Engine::Configuration references it at subclass definition
|
|
5
|
+
|
|
6
|
+
# Require the engine dependencies explicitly so their initializers run
|
|
7
|
+
# during boot.
|
|
8
|
+
require "lexxy"
|
|
9
|
+
require "y"
|
|
10
|
+
require "yrby-rails" # the sync concern, Y::Document storage, and yrby's engine
|
|
11
|
+
|
|
12
|
+
module LexxyRealtime
|
|
13
|
+
class Engine < ::Rails::Engine
|
|
14
|
+
initializer "lexxy_realtime.active_record" do
|
|
15
|
+
ActiveSupport.on_load(:active_record) { include LexxyRealtime::Collaborative }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
initializer "lexxy_realtime.form_builder" do |app|
|
|
19
|
+
app.config.to_prepare { ActionView::Helpers::FormBuilder.prepend(LexxyRealtime::FormBuilder) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The import-map assets under app/assets/javascript need no
|
|
23
|
+
# initializer: Rails adds every app/assets subdirectory of an engine
|
|
24
|
+
# to the asset paths itself.
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexxyRealtime
|
|
4
|
+
# Renders a Lexxy editor with collaboration configured for the record
|
|
5
|
+
# and field. LexxyRealtime.identity supplies the cursor name and color.
|
|
6
|
+
module FormBuilder
|
|
7
|
+
def collaborative_rich_textarea(method, name: nil, color: nil, **options)
|
|
8
|
+
record = object
|
|
9
|
+
unless record.respond_to?(:collaborative_rich_text?) && record.collaborative_rich_text?(method)
|
|
10
|
+
raise ArgumentError,
|
|
11
|
+
"#{record.class.name}##{method} is not collaborative (declare has_collaborative_rich_text :#{method})"
|
|
12
|
+
end
|
|
13
|
+
raise ArgumentError, "#{record.class.name} must be persisted to collaborate on it" unless record.persisted?
|
|
14
|
+
|
|
15
|
+
identity = LexxyRealtime.identity.call(@template)
|
|
16
|
+
collaborator = name || identity[:name]
|
|
17
|
+
public_send(lexxy_editor_method, method, options) do
|
|
18
|
+
# The client-side Yjs binding key, shared by peers of this
|
|
19
|
+
# attribute. The server never sees it.
|
|
20
|
+
@template.content_tag("lexxy-collaboration", "",
|
|
21
|
+
"doc-id" => "#{record.model_name.param_key}-#{record.id}-#{method}",
|
|
22
|
+
"name" => collaborator,
|
|
23
|
+
"color" => color || identity[:color] || LexxyRealtime.collaborator_color(collaborator),
|
|
24
|
+
"channel-name" => LexxyRealtime::CHANNEL_NAME,
|
|
25
|
+
"channel-params" => { sgid: record.to_sgid(for: LexxyRealtime.sgid_purpose(method)).to_s,
|
|
26
|
+
field: method }.to_json)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
alias collaborative_rich_text_area collaborative_rich_textarea
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Lexxy's explicit helper exists on Rails 8.0/8.1. On the
|
|
35
|
+
# ActionText::Editor adapter path in newer Rails, the standard
|
|
36
|
+
# rich_text_area renders Lexxy and accepts the block.
|
|
37
|
+
def lexxy_editor_method
|
|
38
|
+
respond_to?(:lexxy_rich_textarea) ? :lexxy_rich_textarea : :rich_text_area
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lexxy_realtime/version"
|
|
4
|
+
require "lexxy_realtime/collaborative"
|
|
5
|
+
require "lexxy_realtime/form_builder"
|
|
6
|
+
require "lexxy_realtime/engine"
|
|
7
|
+
|
|
8
|
+
# Rails integration for collaborative Lexxy editing with yrby.
|
|
9
|
+
module LexxyRealtime
|
|
10
|
+
# Signed ids from the form helper carry this purpose scoped per field
|
|
11
|
+
# (sgid_purpose), so a token minted elsewhere can't join a document.
|
|
12
|
+
SGID_PURPOSE = :lexxy_realtime
|
|
13
|
+
|
|
14
|
+
# The channel the installer generates and the form helper points elements at.
|
|
15
|
+
CHANNEL_NAME = "DocumentChannel"
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def sgid_purpose(field) = "#{SGID_PURPOSE}/#{field}"
|
|
19
|
+
|
|
20
|
+
# Cursor identity, called with the view context; returns { name:, color: }
|
|
21
|
+
# (a nil color gets a stable one derived from the name).
|
|
22
|
+
attr_writer :identity
|
|
23
|
+
|
|
24
|
+
def identity
|
|
25
|
+
@identity ||= lambda do |view|
|
|
26
|
+
user = view.respond_to?(:current_user) ? view.current_user : nil
|
|
27
|
+
# Use Anonymous when no display name is available.
|
|
28
|
+
name = user && %i[name username handle].lazy.filter_map { |a| user.try(a).presence }.first
|
|
29
|
+
{ name: name || "Anonymous", color: nil }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A stable, readable cursor color per collaborator name.
|
|
34
|
+
def collaborator_color(name)
|
|
35
|
+
"hsl(#{name.to_s.each_byte.reduce(0) { |acc, b| ((acc * 31) + b) % 360 }}, 70%, 45%)"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lexxy-realtime
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.4.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- JP Camara
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: lexxy
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.9'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 8.0.2
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 8.0.2
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: yrby
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 0.6.0
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 0.6.0
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: yrby-rails
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.5'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.5'
|
|
68
|
+
description: Adds collaborative Lexxy editing to Rails applications using yrby. Includes
|
|
69
|
+
the model, form, channel, generator, and Action Text integration.
|
|
70
|
+
email:
|
|
71
|
+
- jp@jpcamara.com
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- LICENSE
|
|
77
|
+
- README.md
|
|
78
|
+
- app/assets/javascript/lexxy_realtime/lexical.js
|
|
79
|
+
- app/assets/javascript/lexxy_realtime/lexical.js.map
|
|
80
|
+
- app/assets/javascript/lexxy_realtime/lexical.min.js
|
|
81
|
+
- app/assets/javascript/lexxy_realtime/lexxy-realtime.js
|
|
82
|
+
- app/assets/javascript/lexxy_realtime/lexxy-realtime.js.map
|
|
83
|
+
- app/assets/javascript/lexxy_realtime/lexxy-realtime.min.js
|
|
84
|
+
- app/assets/javascript/lexxy_realtime/lexxy.js
|
|
85
|
+
- app/assets/javascript/lexxy_realtime/lexxy.js.map
|
|
86
|
+
- app/assets/javascript/lexxy_realtime/lexxy.min.js
|
|
87
|
+
- lib/generators/lexxy_realtime/install/USAGE
|
|
88
|
+
- lib/generators/lexxy_realtime/install/install_generator.rb
|
|
89
|
+
- lib/generators/lexxy_realtime/install/templates/application_cable_channel.rb
|
|
90
|
+
- lib/generators/lexxy_realtime/install/templates/application_cable_connection.rb
|
|
91
|
+
- lib/generators/lexxy_realtime/install/templates/document_channel.rb
|
|
92
|
+
- lib/lexxy-realtime.rb
|
|
93
|
+
- lib/lexxy_realtime.rb
|
|
94
|
+
- lib/lexxy_realtime/collaborative.rb
|
|
95
|
+
- lib/lexxy_realtime/engine.rb
|
|
96
|
+
- lib/lexxy_realtime/form_builder.rb
|
|
97
|
+
- lib/lexxy_realtime/version.rb
|
|
98
|
+
homepage: https://github.com/jpcamara/lexxy-realtime
|
|
99
|
+
licenses:
|
|
100
|
+
- MIT
|
|
101
|
+
metadata:
|
|
102
|
+
homepage_uri: https://github.com/jpcamara/lexxy-realtime
|
|
103
|
+
source_code_uri: https://github.com/jpcamara/lexxy-realtime
|
|
104
|
+
changelog_uri: https://github.com/jpcamara/lexxy-realtime/releases
|
|
105
|
+
rubygems_mfa_required: 'true'
|
|
106
|
+
rdoc_options: []
|
|
107
|
+
require_paths:
|
|
108
|
+
- lib
|
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '3.4'
|
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '0'
|
|
119
|
+
requirements: []
|
|
120
|
+
rubygems_version: 3.6.9
|
|
121
|
+
specification_version: 4
|
|
122
|
+
summary: Collaborative editing for Lexxy in Rails
|
|
123
|
+
test_files: []
|