Skip to content

nauman/inkpen

Repository files navigation

Inkpen

Built on Rails

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.

Features

  • 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

Installation

Add to your Gemfile:

gem "inkpen", github: "nauman/inkpen"

Then run:

bundle install

Configuration

Configure 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

Basic Usage

Creating an Editor Instance

# 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..."
)

In Views (ERB)

<%= 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 Configuration

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 buttons

Sticky Toolbar

The 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 buttons

Handling 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)
})

Extensions

Inkpen uses a modular extension system. Each extension is a PORO that configures TipTap extensions.

Core Extensions

Available by default:

  • bold, italic, strike, underline
  • link, heading
  • bullet_list, ordered_list
  • blockquote, code_block
  • horizontal_rule, hard_break

Advanced Extensions

Forced Document Structure

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...", ... }

Mentions

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" }
  ]
)

Code Block with Syntax Highlighting

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.

Tables

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
)

Task Lists

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"
)

Creating Custom Extensions

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
end

JavaScript Integration

Inkpen uses Stimulus controllers and importmaps. The gem automatically registers pins for TipTap and ProseMirror dependencies.

Required Importmap Pins

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

Stimulus Controller

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>

Architecture

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

API Reference

Inkpen::Editor

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

Inkpen::Toolbar

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?

Inkpen::StickyToolbar

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

Inkpen::Extensions::Base

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

Development

After checking out the repo:

bin/setup              # Install dependencies
bundle exec rake test  # Run tests
bin/console            # Interactive console

Used by

Powering the series editor at inventlist.com/stream?type=series — where indie builders write build-in-public stories.

Contributing

Bug reports and pull requests are welcome on GitHub.

License

MIT License

About

InkPend editor for Rails

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors