A modern, TipTap-based rich text editor for Ruby on Rails applications. Inkpen provides a clean, extensible editor with a PORO-based architecture that seamlessly integrates with Rails forms and Hotwire/Stimulus.
- TipTap/ProseMirror Foundation: Built on the powerful TipTap editor framework
- Rails Integration: Works seamlessly with Rails forms, Turbo, and Stimulus
- PORO Architecture: Clean Ruby objects for configuration and extensions
- Importmap Compatible: No Node.js build step required
- Extensible: Modular extension system for adding features
- Toolbar Options: Floating, fixed, or hidden toolbar configurations
Add to your Gemfile:
gem "inkpen", github: "nauman/inkpen"Then run:
bundle installConfigure Inkpen globally in an initializer:
# config/initializers/inkpen.rb
Inkpen.configure do |config|
config.toolbar = :floating # :floating, :fixed, :none
config.placeholder = "Start writing..."
config.autosave = true
config.autosave_interval = 5000 # milliseconds
config.min_height = "200px"
config.max_height = "600px"
# Enable/disable extensions
config.extensions = [:bold, :italic, :link, :heading, :bullet_list]
end# In your controller or view
editor = Inkpen::Editor.new(
name: "post[body]",
value: @post.body,
toolbar: :floating,
extensions: [:bold, :italic, :link, :heading, :mentions],
placeholder: "Write your post..."
)<%= tag.div editor.data_attributes do %>
<%= hidden_field_tag editor.input_name, editor.value %>
<div class="inkpen-editor" style="<%= editor.style_attributes %>"></div>
<% end %>toolbar = Inkpen::Toolbar.new(
style: :floating,
buttons: [:bold, :italic, :link, :heading],
position: :top
)
# Predefined button presets
Inkpen::Toolbar::PRESET_MINIMAL # [:bold, :italic, :link]
Inkpen::Toolbar::PRESET_STANDARD # Formatting + common blocks
Inkpen::Toolbar::PRESET_FULL # All available buttonsThe sticky toolbar provides a fixed-position toolbar for inserting blocks, media, and widgets. It supports horizontal (bottom) and vertical (left/right) positions.
# Enable sticky toolbar with default settings
editor = Inkpen::Editor.new(
name: "post[body]",
value: @post.body,
sticky_toolbar: Inkpen::StickyToolbar.new(
position: :bottom, # :bottom, :left, :right
buttons: [:table, :code_block, :image, :youtube, :widget],
widget_types: %w[form gallery poll]
)
)Available buttons:
| Button | Description |
|---|---|
table |
Insert a table |
code_block |
Insert code block |
blockquote |
Insert quote block |
horizontal_rule |
Insert divider line |
task_list |
Insert task list |
image |
Insert image (triggers inkpen:request-image event) |
youtube |
Insert YouTube video |
embed |
Insert embed (triggers inkpen:request-embed event) |
widget |
Open widget picker modal |
divider |
Visual separator |
Presets:
Inkpen::StickyToolbar::PRESET_BLOCKS # table, code_block, blockquote, etc.
Inkpen::StickyToolbar::PRESET_MEDIA # image, youtube, embed
Inkpen::StickyToolbar::PRESET_FULL # All buttonsHandling widget events:
// In your application.js or page-specific controller
document.addEventListener("inkpen:insert-widget", (event) => {
const { type, controller } = event.detail
// type is "form", "gallery", or "poll"
// Show your widget picker UI
})
document.addEventListener("inkpen:request-image", (event) => {
const { controller } = event.detail
// Show image upload modal
// Then call: controller.insertImage(url, altText)
})Inkpen uses a modular extension system. Each extension is a PORO that configures TipTap extensions.
Available by default:
bold,italic,strike,underlinelink,headingbullet_list,ordered_listblockquote,code_blockhorizontal_rule,hard_break
Enforces a document structure with a required title heading:
extension = Inkpen::Extensions::ForcedDocument.new(
heading_level: 1,
placeholder: "Enter your title...",
allow_deletion: false
)
extension.to_config
# => { headingLevel: 1, titlePlaceholder: "Enter your title...", ... }Enable @mentions functionality:
extension = Inkpen::Extensions::Mention.new(
search_url: "/api/users/search",
trigger: "@",
min_chars: 1,
suggestion_class: "mention-popup",
allow_custom: false
)
# Or with static items:
extension = Inkpen::Extensions::Mention.new(
items: [
{ id: 1, label: "John Doe" },
{ id: 2, label: "Jane Smith" }
]
)Add syntax highlighting to code blocks:
extension = Inkpen::Extensions::CodeBlockSyntax.new(
languages: [:ruby, :javascript, :python, :sql],
default_language: :ruby,
line_numbers: true,
language_selector: true,
copy_button: true,
theme: "github" # or "monokai", "dracula"
)Available languages: javascript, typescript, ruby, python, css, xml, html, json, bash, sql, markdown, go, rust, java, kotlin, swift, php, c, cpp, csharp, elixir, and more.
Add table support with resizing and toolbar:
extension = Inkpen::Extensions::Table.new(
resizable: true,
header_row: true,
header_column: false,
cell_min_width: 25,
toolbar: true,
allow_merge: true,
default_rows: 3,
default_cols: 3
)Add interactive checkboxes/task lists:
extension = Inkpen::Extensions::TaskList.new(
nested: true,
list_class: "task-list",
item_class: "task-item",
checked_class: "task-checked",
keyboard_shortcut: "Mod-Shift-9"
)Extend Inkpen::Extensions::Base:
module Inkpen
module Extensions
class MyCustomExtension < Base
def name
:my_custom
end
def to_config
{
optionOne: options.fetch(:option_one, "default"),
optionTwo: options.fetch(:option_two, true)
}
end
private
def default_options
super.merge(
option_one: "default",
option_two: true
)
end
end
end
endInkpen uses Stimulus controllers and importmaps. The gem automatically registers pins for TipTap and ProseMirror dependencies.
The gem includes pins for:
- TipTap core and PM adapters
- ProseMirror packages
- TipTap extensions (document, paragraph, text, formatting, etc.)
- Lowlight for syntax highlighting
- Highlight.js language definitions
The editor is controlled by inkpen--editor Stimulus controller. Connect it to your editor container:
<div data-controller="inkpen--editor"
data-inkpen--editor-extensions-value='["bold","italic","link"]'
data-inkpen--editor-toolbar-value="floating"
data-inkpen--editor-placeholder-value="Start writing...">
<!-- Editor content here -->
</div>inkpen/
├── lib/
│ ├── inkpen.rb # Main entry point
│ ├── inkpen/
│ │ ├── configuration.rb # Global config PORO
│ │ ├── editor.rb # Editor instance PORO
│ │ ├── toolbar.rb # Floating toolbar config PORO
│ │ ├── sticky_toolbar.rb # Sticky toolbar config PORO
│ │ ├── engine.rb # Rails engine
│ │ ├── version.rb
│ │ └── extensions/
│ │ ├── base.rb # Extension base class
│ │ ├── forced_document.rb # Title heading structure
│ │ ├── mention.rb # @mentions
│ │ ├── code_block_syntax.rb # Syntax highlighting
│ │ ├── table.rb # Table support
│ │ └── task_list.rb # Task/checkbox lists
├── app/
│ └── assets/
│ ├── javascripts/
│ │ └── inkpen/
│ │ ├── controllers/
│ │ │ ├── editor_controller.js # Main TipTap editor
│ │ │ ├── toolbar_controller.js # Floating toolbar
│ │ │ └── sticky_toolbar_controller.js # Sticky toolbar
│ │ └── index.js # Entry point
│ └── stylesheets/
│ └── inkpen/
│ ├── editor.css # Editor styles
│ └── sticky_toolbar.css # Sticky toolbar styles
├── config/
│ └── importmap.rb # TipTap/PM dependencies
└── README.md
| Method | Description |
|---|---|
name |
Form field name |
value |
Current editor content |
toolbar |
Toolbar style (:floating, :fixed, :none) |
extensions |
Array of enabled extension symbols |
data_attributes |
Hash of Stimulus data attributes |
style_attributes |
CSS inline styles string |
extension_enabled?(name) |
Check if extension is enabled |
input_id |
Safe HTML ID from name |
| Method | Description |
|---|---|
style |
Toolbar style |
buttons |
Array of button symbols |
position |
Toolbar position (:top, :bottom) |
floating? |
Is floating toolbar? |
fixed? |
Is fixed toolbar? |
hidden? |
Is toolbar hidden? |
| Method | Description |
|---|---|
position |
Position (:bottom, :left, :right) |
buttons |
Array of button symbols |
widget_types |
Array of widget type strings |
enabled? |
Is sticky toolbar enabled? |
vertical? |
Is vertical layout (left/right)? |
horizontal? |
Is horizontal layout (bottom)? |
data_attributes |
Hash of Stimulus data attributes |
| Method | Description |
|---|---|
name |
Extension identifier (Symbol) |
enabled? |
Is extension enabled? |
options |
Configuration options hash |
to_config |
JS configuration hash |
to_h |
Full extension hash |
to_json |
JSON representation |
After checking out the repo:
bin/setup # Install dependencies
bundle exec rake test # Run tests
bin/console # Interactive consoleBug reports and pull requests are welcome on GitHub.
MIT License