Skip to content

feature: markdown field (easy_mde replacement) #3584

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,5 @@ gem "avo-record_link_field"
gem "pagy", "> 8"

gem "csv"

gem "redcarpet"
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ GEM
rbs (2.8.4)
rdoc (6.10.0)
psych (>= 4.0.0)
redcarpet (3.6.0)
redis (5.3.0)
redis-client (>= 0.22.0)
redis-client (0.23.0)
Expand Down Expand Up @@ -741,6 +742,7 @@ DEPENDENCIES
rails (>= 8.0.0)
rails-controller-testing
ransack (>= 4.2.0)
redcarpet
redis (~> 5.0)
ripper-tags
rspec-rails (~> 6.0, >= 6.0.3)
Expand Down
1 change: 1 addition & 0 deletions app/assets/svgs/avo/heading.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/assets/svgs/avo/list-todo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/assets/svgs/avo/quote.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions app/components/avo/fields/easy_mde_field/edit_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<%= field_wrapper **field_wrapper_args, full_width: true do %>
<div data-controller="easy-mde">
<%= @form.text_area @field.id,
value: @field.value,
class: classes("w-full js-has-easy-mde-editor"),
data: {
view: view,
'easy-mde-target': 'element',
'component-options': @field.options.to_json,
},
disabled: disabled?,
placeholder: @field.placeholder,
autofocus: @autofocus,
style: @field.get_html(:style, view: view, element: :input)
%>
</div>
<% end %>
4 changes: 4 additions & 0 deletions app/components/avo/fields/easy_mde_field/edit_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

class Avo::Fields::EasyMdeField::EditComponent < Avo::Fields::EditComponent
end
11 changes: 11 additions & 0 deletions app/components/avo/fields/easy_mde_field/show_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<%= field_wrapper **field_wrapper_args, full_width: true do %>
<div data-controller="easy-mde">
<%= text_area_tag @field.id, @field.value,
class: helpers.input_classes('w-full js-has-easy-mde-editor'),
placeholder: @field.placeholder,
disabled: disabled?,
'data-easy-mde-target': 'element',
'data-component-options': @field.options.to_json,
'data-view': :show %>
</div>
<% end %>
4 changes: 4 additions & 0 deletions app/components/avo/fields/easy_mde_field/show_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

class Avo::Fields::EasyMdeField::ShowComponent < Avo::Fields::ShowComponent
end
72 changes: 58 additions & 14 deletions app/components/avo/fields/markdown_field/edit_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,17 +1,61 @@
<%= field_wrapper **field_wrapper_args, full_width: true do %>
<div data-controller="easy-mde">
<%= @form.text_area @field.id,
value: @field.value,
class: classes("w-full js-has-easy-mde-editor"),
data: {
view: view,
'easy-mde-target': 'element',
'component-options': @field.options.to_json,
},
disabled: disabled?,
placeholder: @field.placeholder,
autofocus: @autofocus,
style: @field.get_html(:style, view: view, element: :input)
%>
<%= content_tag :div,
class: "flex flex-col w-full border rounded",
data: {
controller: "markdown-field",
markdown_field_target: "toolbar",
markdown_field_preview_url_value: helpers.avo.markdown_previews_path,
markdown_field_active_tab_class: "bg-white",
markdown_field_attach_url_value: rails_direct_uploads_url,
markdown_field_resource_class_value: @resource.class.name,
markdown_field_field_id_value: @field.id,
} do %>
<div class="w-full flex-1 grow flex justify-bewteen bg-slate-50 rounded px-2 py-1">
<div class="flex-1 flex items-center">
<button class="<%= BUTTON_CLASSES %>" data-action="click->markdown-field#switchToPreview" data-markdown-field-target="previewTabButton">
Preview
</button>
<button class="<%= BUTTON_CLASSES %> hidden bg-slate-200" data-action="click->markdown-field#switchToWrite" data-markdown-field-target="writeTabButton">
Write
</button>
</div>

<markdown-toolbar for="<%= @field.id %>" class="flex space-x-1">
<md-bold class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/bold", class: "inline size-4" %></md-bold>
<md-header class="<%= BUTTON_CLASSES %>"><%= helpers.svg "avo/heading", class: "inline size-4" %></md-header>
<md-italic class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/italic", class: "inline size-4" %></md-italic>
<md-quote class="<%= BUTTON_CLASSES %>"><%= helpers.svg "avo/quote", class: "inline size-4" %></md-quote>
<md-code class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/code", class: "inline size-4" %></md-code>
<md-link class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/link", class: "inline size-4" %></md-link>
<md-image class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/photo", class: "inline size-4" %></md-image>
<md-unordered-list class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/list-bullet", class: "inline size-4" %></md-unordered-list>
<md-ordered-list class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/numbered-list", class: "inline size-4" %></md-ordered-list>
<md-task-list class="<%= BUTTON_CLASSES %>"><%= helpers.svg "avo/list-todo", class: "inline size-4" %></md-task-list>
</markdown-toolbar>
</div>

<div class="border-t">
<%= @form.text_area @field.id,
id: @field.id,
value: @field.value,
class: ("flex flex-1 rounded border-none w-full py-2 px-3"),
rows: 20,
data: {
markdown_field_target: "fieldElement",
action: "drop->markdown-field#dropUpload paste->markdown-field#pasteUpload"
},
disabled: disabled?,
placeholder: @field.placeholder,
autofocus: @autofocus,
style: @field.get_html(:style, view: view, element: :input)
%>
<%= content_tag :div, class: "hidden markdown-preview", id: "markdown-preview-#{@field.id}", data: { markdown_field_target: "previewElement" } do %>
<div class="button-spinner">
<div class="double-bounce1"></div>
<div class="double-bounce2"></div>
</div>
<% end %>
</div>
</div>
<% end %>
<% end %>
1 change: 1 addition & 0 deletions app/components/avo/fields/markdown_field/edit_component.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true

class Avo::Fields::MarkdownField::EditComponent < Avo::Fields::EditComponent
BUTTON_CLASSES = "cursor-pointer py-1 px-1.5 hover:bg-slate-200 rounded"
end
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
<%= field_wrapper **field_wrapper_args, full_width: true do %>
<div data-controller="easy-mde">
<%= text_area_tag @field.id, @field.value,
class: helpers.input_classes('w-full js-has-easy-mde-editor'),
placeholder: @field.placeholder,
disabled: disabled?,
'data-easy-mde-target': 'element',
'data-component-options': @field.options.to_json,
'data-view': :show %>
</div>
<%= content_tag :div, sanitize(parsed_body), class: "trix-content" %>
<% end %>
5 changes: 5 additions & 0 deletions app/components/avo/fields/markdown_field/show_component.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# frozen_string_literal: true

class Avo::Fields::MarkdownField::ShowComponent < Avo::Fields::ShowComponent
def parsed_body
renderer = Redcarpet::Render::HTML.new(hard_wrap: true)
parser = Redcarpet::Markdown.new(renderer, lax_spacing: true, fenced_code_blocks: true, hard_wrap: true)
parser.render(@field.value)
end
end
10 changes: 10 additions & 0 deletions app/controllers/avo/markdown_previews_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Avo
class MarkdownPreviewsController < ApplicationController
def create
renderer = Redcarpet::Render::HTML.new(hard_wrap: true)
parser = Redcarpet::Markdown.new(renderer, lax_spacing: true, fenced_code_blocks: true, hard_wrap: true)

@result = parser.render(params[:body])
end
end
end
2 changes: 2 additions & 0 deletions app/javascript/js/controllers.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ItemSelectAllController from './controllers/item_select_all_controller'
import ItemSelectorController from './controllers/item_selector_controller'
import KeyValueController from './controllers/fields/key_value_controller'
import LoadingButtonController from './controllers/loading_button_controller'
import MarkdownController from './controllers/fields/markdown_controller'
import MenuController from './controllers/menu_controller'
import ModalController from './controllers/modal_controller'
import MultipleSelectFilterController from './controllers/multiple_select_filter_controller'
Expand Down Expand Up @@ -96,6 +97,7 @@ application.register('code-field', CodeFieldController)
application.register('date-field', DateFieldController)
application.register('easy-mde', EasyMdeController)
application.register('key-value', KeyValueController)
application.register('markdown-field', MarkdownController)
application.register('progress-bar-field', ProgressBarFieldController)
application.register('reload-belongs-to-field', ReloadBelongsToFieldController)
application.register('tags-field', TagsFieldController)
Expand Down
105 changes: 105 additions & 0 deletions app/javascript/js/controllers/fields/markdown_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* eslint-disable camelcase */
import '@github/markdown-toolbar-element'
import { Controller } from '@hotwired/stimulus'
import { DirectUpload } from '@rails/activestorage'
import { post } from '@rails/request.js'
import { subscribe } from '@github/paste-markdown'

// upload code from Jeremy Smith's blog post
// https://hybrd.co/posts/github-issue-style-file-uploader-using-stimulus-and-active-storage

// Connects to data-controller="form"
export default class extends Controller {
static values = {
attachUrl: String,
previewUrl: String,
resourceClass: String,
fieldId: String,
}

static targets = ['fieldElement', 'previewElement', 'writeTabButton', 'previewTabButton']

connect() {
subscribe(this.fieldElementTarget, { defaultPlainTextPaste: { urlLinks: true } })
}

switchToWrite(event) {
event.preventDefault()

// toggle buttons
this.writeTabButtonTarget.classList.add('hidden')
this.previewTabButtonTarget.classList.remove('hidden')

// toggle write/preview buttons
this.fieldElementTarget.classList.remove('hidden')
this.previewElementTarget.classList.add('hidden')
}

switchToPreview(event) {
event.preventDefault()

post(this.previewUrlValue, {
body: {
body: this.fieldElementTarget.value,
resource_class: this.resourceClassValue,
field_id: this.fieldIdValue,
element_id: this.previewElementTarget.id,
},
responseKind: 'turbo-stream',
})

// set the min height to the field element height
this.previewElementTarget.style.minHeight = `${this.fieldElementTarget.offsetHeight}px`

// toggle buttons
this.writeTabButtonTarget.classList.remove('hidden')
this.previewTabButtonTarget.classList.add('hidden')

// toggle elements
this.fieldElementTarget.classList.add('hidden')
this.previewElementTarget.classList.remove('hidden')
}

dropUpload(event) {
event.preventDefault()
this.uploadFiles(event.dataTransfer.files)
}

pasteUpload(event) {
if (!event.clipboardData.files.length) return

event.preventDefault()
this.uploadFiles(event.clipboardData.files)
}

uploadFiles(files) {
Array.from(files).forEach((file) => this.uploadFile(file))
}

uploadFile(file) {
const upload = new DirectUpload(file, this.attachUrlValue)

upload.create((error, blob) => {
if (error) {
console.log('Error', error)
} else {
const text = this.markdownLink(blob)
const start = this.fieldElementTarget.selectionStart
const end = this.fieldElementTarget.selectionEnd
this.fieldElementTarget.setRangeText(text, start, end)
}
})
}

markdownLink(blob) {
const { filename } = blob
const url = `/rails/active_storage/blobs/${blob.signed_id}/${filename}`
const prefix = (this.isImage(blob.content_type) ? '!' : '')

return `${prefix}[${filename}](${url})\n`
}

isImage(contentType) {
return ['image/jpeg', 'image/gif', 'image/png'].includes(contentType)
}
}
5 changes: 5 additions & 0 deletions app/views/avo/markdown_previews/create.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<%= turbo_stream.update params[:element_id] do %>
<div class="trix-content py-2 px-3">
<%= sanitize(@result) %>
</div>
<% end %>
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
instance_exec(&Avo.mount_engines)
end

resources :markdown_previews, only: [:create]

post "/rails/active_storage/direct_uploads", to: "/active_storage/direct_uploads#create"

scope "avo_api", as: "avo_api" do
Expand Down
22 changes: 22 additions & 0 deletions lib/avo/fields/easy_mde_field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module Avo
module Fields
class EasyMdeField < BaseField
attr_reader :options

def initialize(id, **args, &block)
super(id, **args, &block)

hide_on :index

@always_show = args[:always_show].present? ? args[:always_show] : false
@height = args[:height].present? ? args[:height].to_s : "auto"
@spell_checker = args[:spell_checker].present? ? args[:spell_checker] : false
@options = {
spell_checker: @spell_checker,
always_show: @always_show,
height: @height
}
end
end
end
end
12 changes: 2 additions & 10 deletions lib/avo/fields/markdown_field.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
module Avo
module Fields
class MarkdownField < BaseField
attr_reader :options
attr_reader :rows

def initialize(id, **args, &block)
super(id, **args, &block)

hide_on :index

@always_show = args[:always_show].present? ? args[:always_show] : false
@height = args[:height].present? ? args[:height].to_s : "auto"
@spell_checker = args[:spell_checker].present? ? args[:spell_checker] : false
@options = {
spell_checker: @spell_checker,
always_show: @always_show,
height: @height
}
add_string_prop args, :rows, default: 20
end
end
end
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
"@algolia/autocomplete-js": "^1.0.0-alpha.46",
"@algolia/autocomplete-theme-classic": "^1.0.0-alpha.46",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@github/markdown-toolbar-element": "^2.2.3",
"@github/paste-markdown": "^1.5.3",
"@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo-rails": "^8.0.12",
"@rails/activestorage": "^6.1.710",
"@rails/request.js": "^0.0.11",
"@stimulus-components/clipboard": "^5.0.0",
"@stimulus-components/password-visibility": "^3.0.0",
"@tailwindcss/forms": "^0.5.10",
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/app/avo/resources/city.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def tool_fields
field :is_capital, as: :boolean, filterable: true
field :features, as: :key_value
field :image_url, as: :external_image
field :tiny_description, as: :markdown
field :tiny_description, as: :easy_mde
field :status, as: :badge, enum: ::City.statuses
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<% if params[:show_native_fields].present? %>
<%= avo_edit_field(:description, as: :trix, form: form, component_options: {resource_name: 'cities', resource_id: @resource.record&.id}) %>
<% end %>
<%= avo_edit_field(:tiny_description, as: :markdown, form: form) %>
<%= avo_edit_field(:tiny_description, as: :easy_mde, form: form) %>
<%= avo_edit_field(:status, as: :select, enum: ::City.statuses, form: form) %>
<%= avo_show_field(:status, as: :badge, enum: ::City.statuses, form: form) %>
<% end %>
Expand Down
Loading
Loading