From d556e21287cfe2e23f33a4aea17de1c2cefbbfbb Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Wed, 27 Nov 2024 18:59:15 -0600 Subject: [PATCH] Use House for MD comments --- Gemfile | 3 + Gemfile.lock | 8 + app/assets/stylesheets/house.css | 310 ++++++++++++++++++ .../markdown/uploads_controller.rb | 16 + app/javascript/application.js | 1 + app/models/account.rb | 2 + app/models/comment.rb | 2 + .../markdown/uploads/create.json.jbuilder | 4 + app/views/comments/_comment.html.erb | 2 +- app/views/comments/_new.html.erb | 2 +- config/importmap.rb | 1 + config/initializers/markdown.rb | 4 + config/initializers/storage_paths.rb | 5 + config/routes.rb | 5 + config/storage.yml | 2 +- ...1126193431_create_action_text_markdowns.rb | 13 + ...0241126232658_remove_body_from_comments.rb | 5 + db/schema.rb | 15 +- lib/markdown_renderer.rb | 33 ++ lib/rails_ext/action_text_has_markdown.rb | 39 +++ lib/rails_ext/action_text_markdown.rb | 42 +++ lib/rails_ext/action_text_tag_helper.rb | 36 ++ lib/rails_ext/active_storage_sluggable.rb | 32 ++ test/fixtures/action_text/markdowns.yml | 14 + test/fixtures/comments.yml | 3 - vendor/.keep | 0 vendor/javascript/.keep | 0 vendor/javascript/house.min.js | 1 + 28 files changed, 592 insertions(+), 8 deletions(-) create mode 100644 app/assets/stylesheets/house.css create mode 100644 app/controllers/action_text/markdown/uploads_controller.rb create mode 100644 app/views/action_text/markdown/uploads/create.json.jbuilder create mode 100644 config/initializers/markdown.rb create mode 100644 config/initializers/storage_paths.rb create mode 100644 db/migrate/20241126193431_create_action_text_markdowns.rb create mode 100644 db/migrate/20241126232658_remove_body_from_comments.rb create mode 100644 lib/markdown_renderer.rb create mode 100644 lib/rails_ext/action_text_has_markdown.rb create mode 100644 lib/rails_ext/action_text_markdown.rb create mode 100644 lib/rails_ext/action_text_tag_helper.rb create mode 100644 lib/rails_ext/active_storage_sluggable.rb create mode 100644 test/fixtures/action_text/markdowns.yml delete mode 100644 vendor/.keep delete mode 100644 vendor/javascript/.keep create mode 100644 vendor/javascript/house.min.js diff --git a/Gemfile b/Gemfile index c70e1eb17..76e597abd 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,9 @@ gem "thruster", require: false # Features gem "bcrypt", "~> 3.1.7" gem "rqrcode" +gem "redcarpet", "~> 3.6" +gem "rouge", "~> 4.5" +gem "jbuilder" # Telemetry gem "sentry-ruby" diff --git a/Gemfile.lock b/Gemfile.lock index fccbac836..41094c329 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,6 +126,9 @@ GEM irb (1.14.1) rdoc (>= 4.0.0) reline (>= 0.4.2) + jbuilder (2.13.0) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) json (2.7.2) language_server-protocol (3.17.0.3) logger (1.6.1) @@ -223,10 +226,12 @@ GEM rake (13.2.1) rdoc (6.7.0) psych (>= 4.0.0) + redcarpet (3.6.0) regexp_parser (2.9.2) reline (0.5.10) io-console (~> 0.5) rexml (3.3.8) + rouge (4.5.1) rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) @@ -335,9 +340,12 @@ DEPENDENCIES debug hotwire_combobox! importmap-rails + jbuilder propshaft puma (>= 5.0) rails (>= 8.0.0.rc1) + redcarpet (~> 3.6) + rouge (~> 4.5) rqrcode rubocop-rails-omakase selenium-webdriver diff --git a/app/assets/stylesheets/house.css b/app/assets/stylesheets/house.css new file mode 100644 index 000000000..4689f8105 --- /dev/null +++ b/app/assets/stylesheets/house.css @@ -0,0 +1,310 @@ +/* Colors */ +:root { + /* Named color values */ + --house-md-black: 0% 0 0; + --house-md-white: 100% 0 0; + --house-md-gray-light: 96% 0.005 96; + --house-md-gray: 92% 0.005 96; + --house-md-gray-dark: 75% 0.005 96; + --house-md-blue: 54% 0.23 255; + --house-md-blue-light: 95% 0.03 255; + --house-md-blue-dark: 80% 0.08 255; + --house-md-orange: 70% 0.2 44; + --house-md-red: 51% 0.2 31; + --house-md-green: 65.59% 0.234 142.49; + --house-md-always-black: 0% 0 0; + + /* Abstractions */ + --house-color-negative: oklch(var(--house-md-red)); + --house-color-positive: oklch(var(--house-md-green)); + --house-color-bg: oklch(var(--house-md-white)); + --house-color-ink: oklch(var(--house-md-black)); + --house-color-ink-reversed: oklch(var(--house-md-white)); + --house-color-link: oklch(var(--house-md-blue)); + --house-color-subtle-light: oklch(var(--house-md-gray-light)); + --house-color-subtle: oklch(var(--house-md-gray)); + --house-color-subtle-dark: oklch(var(--house-md-gray-dark)); + --house-color-selected: oklch(var(--house-md-blue-light)); + --house-color-selected-dark: oklch(var(--house-md-blue-dark)); + --house-color-alert: oklch(var(--house-md-orange)); + --house-color-always-black: oklch(var(--house-md-always-black)); + + /* Redefine named color values for dark mode */ + @media (prefers-color-scheme: dark) { + --house-md-black: 100% 0 0; + --house-md-white: 0% 0 0; + --house-md-gray: 25.2% 0 0; + --house-md-gray-dark: 30.12% 0 0; + --house-md-gray-darker: 44.95% 0 0; + --house-md-blue: 72.25% 0.16 248; + --house-md-blue-light: 28.11% 0.053 248; + --house-md-blue-dark: 42.25% 0.07 248; + --house-md-red: 73.8% 0.184 29.18; + --house-md-green: 75% 0.21 141.89; + } +} + +/* Base styles */ +:is(house-md-editor, house-md-toolbar) { +} + +a:not([class]) { + color: var(--house-color-link); + text-decoration-skip-ink: auto; +} + +:is(a, button, input, textarea) { + --outline-size: max(2px, 0.08em); + --outline-style: solid; + --house-outline-color: currentColor; + + caret-color: var(--house-color-link); + + &:focus { + outline: var(--outline-size) var(--outline-style) var(--house-outline-color); + outline-offset: var(--outline-offset, var(--outline-size)); + } + + &:focus-visible { + outline: var(--outline-size) var(--outline-style) var(--house-outline-color); + outline-offset: var(--outline-offset, var(--outline-size)); + } + + &:focus:not(:focus-visible) { + outline: none; + } + + ::selection { + background-color: var(--house-color-selected); + } +} + +/* Editor */ +house-md { + display: flex; + flex-direction: column; + flex-grow: 1; + + &:invalid { + border: var(--house-color-negative) 2px solid; + } +} + +/* Toolbar */ +house-md-toolbar { + background-color: inherit; + border: 1px solid currentColor; + border-radius: 0.5em; + color: currentColor; + display: inline-flex; + font-size: inherit; + padding: 0.2em; + + button, label { + aspect-ratio: 4/3; + appearance: none; + background-color: transparent; + block-size: 2em; + border: none; + border-radius: 0.5em; + color: currentColor; + cursor: pointer; + display: grid; + font-size: inherit; + inline-size: auto; + place-items: center; + transition: background-color 300ms ease; + + svg { + -webkit-touch-callout: none; + block-size: 0.9em; + fill: currentColor; + grid-area: 1/1; + inline-size: auto; + user-select: none; + } + + &:is(:focus, :hover) { + background-color: var(--house-color-subtle); + } + + &:is(:active) { + background-color: var(--house-color-selected); + } + } +} + +/* Markdown Content */ +.house-md-content { + caret-color: var(--color-link); + flex-grow: 1; + font-family: var(--font-sans); + min-block-size: 50dvh; + text-align: left; + white-space: break-spaces; + padding: 1rem; + + &.house-md-content-empty::before { + content: attr(placeholder); + color: var(--house-color-ink); + opacity: 0.5; + pointer-events: none; + display: block; + } + + &:active, &:focus{ + border:none; + outline:none; + } + + :is(.h1, .h2, .h3, .h4, .h5, .h6) { + display: inline-inblock; + font-weight: 600; + margin-block: 0; + } + + .h1 { + font-size: 2em; + } + + .h2 { + font-size: 1.5em; + } + + .h3 { + font-size: 1.17em; + } + + .h4 { + font-size: 1em; + } + + .h5 { + font-size: 0.83em; + } + + .h6 { + font-size: 0.67em; + } + + .link, .img { + color: var(--house-color-link); + text-decoration: underline; + } + + .hr { + border-color: var(--color-house-subtle-dark); + border-style: solid none none; + margin: 2lh auto; + } + + .code { + font-family: ui-monospace, monospace; + font-weight: 600; + font-size: 0.85em; + } + + .quote { + font-style: italic; + margin: 0 3ch; + } + + .quote, .ol-li, .ul-li { + padding-left: 1em; + } + + .comment { + color: var(--color-house-positive); + } + + mark { + background-color: rgba(241, 241, 26, 0.678); + } +} + +/* Uploads */ +house-md-upload { + border-radius: 0.5em; + margin-block: 0.5ex; + position: relative; + + &[status="failed"] { + background-color: var(--house-color-negative); + color: var(--house-color-ink-reversed); + font-weight: bold; + padding: 0.5em; + } +} + +.md-close { + appearance: none; + display: none; + inline-size: 1em; + block-size: 1em; + border: none; + outline: none; + background-color: transparent; + position: absolute; + inset: 0.5em 0.5em auto auto; + cursor: pointer; + + house-md-upload[status="failed"] & { + color: var(--house-color-ink-reversed); + display: inline-block; + } + + &::before { + content: '×'; + color: var(--house-color-ink-reversed); + font-size: 24px; + font-weight: bold; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} + +.md-file { + font-weight: normal; + + house-md-upload[status="complete"] & { + color: var(--house-color-positive); + } +} + +.md-progress-bar { + -webkit-appearance: none; + appearance: none; + background-color: var(--house-color-subtle); + border-radius: 0.5em; + block-size: 1ex; + display: block; + inline-size: 100%; + line-height: inherit; + margin: 0; + + house-md-upload[status="failed"] & , + house-md-upload[status="complete"] &{ + display: none; + } + + &::-webkit-progress-bar { + border-radius: 0.5em; + background-color: var(--house-color-subtle); + } + + &::-webkit-progress-inner-element { + border-radius: 0.5em; + } + + &::-webkit-progress-value { + border-radius: 0.5em; + background-color: var(--house-color-positive); + } + + &::-moz-progress-bar { + border-radius: 0.5em; + background-color: var(--house-color-positive); + } +} diff --git a/app/controllers/action_text/markdown/uploads_controller.rb b/app/controllers/action_text/markdown/uploads_controller.rb new file mode 100644 index 000000000..7444a6f7c --- /dev/null +++ b/app/controllers/action_text/markdown/uploads_controller.rb @@ -0,0 +1,16 @@ +class ActionText::Markdown::UploadsController < ApplicationController + include ActiveStorage::SetCurrent + + def create + file = params[:file] + blob = ActiveStorage::Blob.create_and_upload! io: file, filename: file.original_filename, content_type: file.content_type + @upload = Current.account.uploads_attachments.create! blob: blob + render :create, status: :created, formats: :json + end + + def show + @attachment = ActiveStorage::Attachment.find_by! slug: "#{params[:slug]}.#{params[:format]}" + expires_in 1.year, public: true + redirect_to @attachment.url + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index e917b49c5..c2917aa3e 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -2,3 +2,4 @@ import "@hotwired/turbo-rails" import "initializers" import "controllers" +import "house" diff --git a/app/models/account.rb b/app/models/account.rb index c7062e340..fb5815f4e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -10,4 +10,6 @@ class Account < ApplicationRecord has_many :stages, through: :workflows, class_name: "Workflow::Stage" has_many :tags, dependent: :destroy + + has_many_attached :uploads end diff --git a/app/models/comment.rb b/app/models/comment.rb index 4a9afb28b..dd9c07dc7 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -4,4 +4,6 @@ class Comment < ApplicationRecord belongs_to :creator, class_name: "User", default: -> { Current.user } searchable_by :body, using: :comments_search_index + + has_markdown :body end diff --git a/app/views/action_text/markdown/uploads/create.json.jbuilder b/app/views/action_text/markdown/uploads/create.json.jbuilder new file mode 100644 index 000000000..984afba49 --- /dev/null +++ b/app/views/action_text/markdown/uploads/create.json.jbuilder @@ -0,0 +1,4 @@ +json.message "File uploaded successfully" +json.fileName @upload.filename.to_s +json.mimetype @upload.content_type +json.fileUrl @upload.slug_url diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index d405d8bc8..70e69ddb9 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -13,7 +13,7 @@ <% end %>
- <%= simple_format comment.body %> + <%= comment.body.to_html %>
<% end %> diff --git a/app/views/comments/_new.html.erb b/app/views/comments/_new.html.erb index 3a8073403..b3e65fd87 100644 --- a/app/views/comments/_new.html.erb +++ b/app/views/comments/_new.html.erb @@ -7,7 +7,7 @@
<%= form_with model: Comment.new, url: bucket_bubble_comments_path(bubble.bucket, bubble), class: "flex flex-column gap full-width", data: { controller: "form", action: "keydown.meta+enter->form#submit" } do |form| %> - <%= form.text_area :body, class: "input border border-radius", required: true, rows: 4, placeholder: new_comment_placeholder(bubble) %> + <%= form.markdown_area :body, class: "input border border-radius", required: true, placeholder: new_comment_placeholder(bubble) %> <%= form.button class: "btn btn--reversed center" do %> <%= image_tag "check.svg", aria: { hidden: "true" }, size: 24 %> Save diff --git a/config/importmap.rb b/config/importmap.rb index 3398c83de..bf42e394c 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -4,6 +4,7 @@ pin "application" pin "@hotwired/turbo-rails", to: "turbo.min.js" pin "@hotwired/stimulus", to: "stimulus.min.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin "house", to: "house.min.js" pin_all_from "app/javascript/controllers", under: "controllers" pin_all_from "app/javascript/helpers", under: "helpers" diff --git a/config/initializers/markdown.rb b/config/initializers/markdown.rb new file mode 100644 index 000000000..92b21b273 --- /dev/null +++ b/config/initializers/markdown.rb @@ -0,0 +1,4 @@ +ActiveSupport.on_load :action_text_markdown do + require "markdown_renderer" + ActionText::Markdown.renderer = -> { MarkdownRenderer.build } +end diff --git a/config/initializers/storage_paths.rb b/config/initializers/storage_paths.rb new file mode 100644 index 000000000..497a6d466 --- /dev/null +++ b/config/initializers/storage_paths.rb @@ -0,0 +1,5 @@ +Rails.application.config.after_initialize do + %w[ db files ].each do |dir| + Rails.root.join("storage", dir).mkpath + end +end diff --git a/config/routes.rb b/config/routes.rb index c11aff6ee..ab90b2124 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -50,6 +50,11 @@ Rails.application.routes.draw do resources :stages, module: :workflows end + namespace :action_text, path: nil do + get "/u/*slug" => "markdown/uploads#show", as: :markdown_upload + post "/uploads" => "markdown/uploads#create", as: :markdown_uploads + end + get "join/:join_code", to: "users#new", as: :join post "join/:join_code", to: "users#create" get "up", to: "rails/health#show", as: :rails_health_check diff --git a/config/storage.yml b/config/storage.yml index 4942ab669..badb11b67 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -4,7 +4,7 @@ test: local: service: Disk - root: <%= Rails.root.join("storage") %> + root: <%= Rails.root.join("storage", "files") %> # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: diff --git a/db/migrate/20241126193431_create_action_text_markdowns.rb b/db/migrate/20241126193431_create_action_text_markdowns.rb new file mode 100644 index 000000000..056b037fa --- /dev/null +++ b/db/migrate/20241126193431_create_action_text_markdowns.rb @@ -0,0 +1,13 @@ +class CreateActionTextMarkdowns < ActiveRecord::Migration[8.0] + def change + create_table :action_text_markdowns do |t| + t.references :record, polymorphic: true, null: false + t.string :name, null: false + t.text :content, null: false, default: "" + t.timestamps + end + + add_column :active_storage_attachments, :slug, :string + add_index :active_storage_attachments, :slug, unique: true + end +end diff --git a/db/migrate/20241126232658_remove_body_from_comments.rb b/db/migrate/20241126232658_remove_body_from_comments.rb new file mode 100644 index 000000000..bf2282a42 --- /dev/null +++ b/db/migrate/20241126232658_remove_body_from_comments.rb @@ -0,0 +1,5 @@ +class RemoveBodyFromComments < ActiveRecord::Migration[8.0] + def change + remove_column :comments, :body, :text, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 9895b7729..86d3a07d8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_11_20_222444) do +ActiveRecord::Schema[8.0].define(version: 2024_11_26_232658) do create_table "accesses", force: :cascade do |t| t.integer "bucket_id", null: false t.integer "user_id", null: false @@ -30,14 +30,26 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_20_222444) do t.index ["name"], name: "index_accounts_on_name", unique: true end + create_table "action_text_markdowns", force: :cascade do |t| + t.string "record_type", null: false + t.integer "record_id", null: false + t.string "name", null: false + t.text "content", default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record" + end + create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false t.bigint "record_id", null: false t.bigint "blob_id", null: false t.datetime "created_at", null: false + t.string "slug" t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + t.index ["slug"], name: "index_active_storage_attachments_on_slug", unique: true end create_table "active_storage_blobs", force: :cascade do |t| @@ -116,7 +128,6 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_20_222444) do end create_table "comments", force: :cascade do |t| - t.text "body", null: false t.integer "creator_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false diff --git a/lib/markdown_renderer.rb b/lib/markdown_renderer.rb new file mode 100644 index 000000000..97c496856 --- /dev/null +++ b/lib/markdown_renderer.rb @@ -0,0 +1,33 @@ +require "rouge/plugins/redcarpet" + +class MarkdownRenderer < Redcarpet::Render::HTML + include Rouge::Plugins::Redcarpet + + def self.build + renderer = MarkdownRenderer.new(ActionText::Markdown::DEFAULT_RENDERER_OPTIONS) + Redcarpet::Markdown.new(renderer, ActionText::Markdown::DEFAULT_MARKDOWN_EXTENSIONS) + end + + def initialize(*args) + super + @id_counts = Hash.new(0) + end + + def header(text, header_level) + unique_id(text).then do |id| + "#{text} " + end + end + + def image(url, title, alt_text) + %(#{alt_text}) + end + + private + def unique_id(text) + text.parameterize.then do |base_id| + @id_counts[base_id] += 1 + @id_counts[base_id] > 1 ? "#{base_id}-#{@id_counts[base_id]}" : base_id + end + end +end diff --git a/lib/rails_ext/action_text_has_markdown.rb b/lib/rails_ext/action_text_has_markdown.rb new file mode 100644 index 000000000..0e8f255e4 --- /dev/null +++ b/lib/rails_ext/action_text_has_markdown.rb @@ -0,0 +1,39 @@ +module ActionText + module HasMarkdown + extend ActiveSupport::Concern + + class_methods do + def has_markdown(name, strict_loading: strict_loading_by_default) + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name} + markdown_#{name} || build_markdown_#{name} + end + + def #{name}? + markdown_#{name}.present? + end + + def #{name}=(content) + self.#{name}.content = content + end + CODE + + has_one :"markdown_#{name}", -> { where(name: name) }, + class_name: "ActionText::Markdown", as: :record, inverse_of: :record, autosave: true, dependent: :destroy, + strict_loading: strict_loading + + scope :"with_markdown_#{name}", -> { includes("markdown_#{name}") } + end + end + + def safe_markdown_attribute(name) + if self.class.reflect_on_association("markdown_#{name}")&.klass == ActionText::Markdown + public_send(name) + end + end + end +end + +ActiveSupport.on_load :active_record do + include ActionText::HasMarkdown +end diff --git a/lib/rails_ext/action_text_markdown.rb b/lib/rails_ext/action_text_markdown.rb new file mode 100644 index 000000000..fa0a62298 --- /dev/null +++ b/lib/rails_ext/action_text_markdown.rb @@ -0,0 +1,42 @@ +module ActionText + class Markdown < Record + DEFAULT_RENDERER_OPTIONS = { + filter_html: false + } + + DEFAULT_MARKDOWN_EXTENSIONS = { + autolink: true, + highlight: true, + no_intra_emphasis: true, + fenced_code_blocks: true, + lax_spacing: true, + strikethrough: true, + tables: true + } + + mattr_accessor :renderer, default: Redcarpet::Markdown.new( + Redcarpet::Render::HTML.new(DEFAULT_RENDERER_OPTIONS), DEFAULT_MARKDOWN_EXTENSIONS) + + belongs_to :record, polymorphic: true, touch: true + + def to_html + (renderer.try(:call) || renderer).render(content).html_safe + end + end +end + +module ActionText::Markdown::Uploads + extend ActiveSupport::Concern + + included do + has_many_attached :uploads, dependent: :destroy + end +end + +ActiveSupport.on_load :active_storage_attachment do + class ActionText::Markdown + include ActionText::Markdown::Uploads + end +end + +ActiveSupport.run_load_hooks :action_text_markdown, ActionText::Markdown diff --git a/lib/rails_ext/action_text_tag_helper.rb b/lib/rails_ext/action_text_tag_helper.rb new file mode 100644 index 000000000..41dfb3ce7 --- /dev/null +++ b/lib/rails_ext/action_text_tag_helper.rb @@ -0,0 +1,36 @@ +module ActionText + module TagHelper + def markdown_area(record, name, value: nil, data: {}, **options) + field_name = "#{record.class.model_name.param_key}[#{name}]" + value = record.safe_markdown_attribute(name).content.to_s if value.nil? + data = data.reverse_merge! uploads_url: action_text_markdown_uploads_url(format: "json") + + tag.house_md value, name: field_name, data: data, **options + end + + def house_toolbar(**options, &block) + tag.house_md_toolbar(**options, &block) + end + + def house_toolbar_button(action, **options, &block) + tag.button title: action.to_s.humanize, data: { "house-md-action": action }, **options, &block + end + + def house_toolbar_file_upload_button(name: "upload", title: "Upload File", **options, &block) + tag.label title: title, **options do + safe_join [ + file_field_tag(name, data: { "house-md-toolbar-file-picker": true }, style: "display: none;"), + capture(&block) + ] + end + end + end +end + +module ActionView::Helpers + class FormBuilder + def markdown_area(method, **options) + @template.markdown_area(@object, method, **options) + end + end +end diff --git a/lib/rails_ext/active_storage_sluggable.rb b/lib/rails_ext/active_storage_sluggable.rb new file mode 100644 index 000000000..d1a3b1e4f --- /dev/null +++ b/lib/rails_ext/active_storage_sluggable.rb @@ -0,0 +1,32 @@ +module ActiveStorage::Sluggable + extend ActiveSupport::Concern + + included do + before_create :set_slug + end + + def slug_url(host: ActiveStorage::Current.url_options[:host], port: ActiveStorage::Current.url_options[:port]) + Rails.application.routes.url_helpers.action_text_markdown_upload_url(slug, host: host, port: port) + end + + private + def set_slug + self.slug = "#{slug_basename}-#{SecureRandom.alphanumeric(6)}.#{slug_extension}" + end + + def slug_basename + File.basename(slug_filename, ".*").parameterize + end + + def slug_extension + File.extname(slug_filename).delete(".").parameterize + end + + def slug_filename + slug.presence || filename.to_s + end +end + +ActiveSupport.on_load :active_storage_attachment do + ActiveStorage::Attachment.include ActiveStorage::Sluggable +end diff --git a/test/fixtures/action_text/markdowns.yml b/test/fixtures/action_text/markdowns.yml new file mode 100644 index 000000000..519cd408b --- /dev/null +++ b/test/fixtures/action_text/markdowns.yml @@ -0,0 +1,14 @@ +logo_agreement_jz: + record: logo_agreement_jz (Comment) + name: body + content: I agree. + +logo_agreement_kevin: + record: logo_agreement_kevin (Comment) + name: body + content: Same, let’s do it. + +layout_overflowing_david: + record: layout_overflowing_david (Comment) + name: body + content: The text is overflowing the container. diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml index 3f988f427..0f17c9760 100644 --- a/test/fixtures/comments.yml +++ b/test/fixtures/comments.yml @@ -1,13 +1,10 @@ logo_agreement_jz: - body: I agree. creator: jz created_at: <%= 2.days.ago %> logo_agreement_kevin: - body: Same, let’s do it. creator: kevin created_at: <%= 2.hours.ago %> layout_overflowing_david: - body: The text is overflowing the container. creator: david diff --git a/vendor/.keep b/vendor/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/vendor/javascript/house.min.js b/vendor/javascript/house.min.js new file mode 100644 index 000000000..6d717f5ee --- /dev/null +++ b/vendor/javascript/house.min.js @@ -0,0 +1 @@ +function t(t){return"string"==typeof t}function e(t,e,n=null,s=!1){return t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:n,cancelable:s}))}class n extends HTMLElement{constructor(){super()}connectedCallback(){this.#t(),this.addEventListener("click",this.#e),this.#n?.addEventListener("change",this.#s)}disconnectedCallback(){this.#n?.removeEventListener("change",this.#s),this.removeEventListener("click",this.#e)}get#n(){return this.querySelector("[data-house-md-toolbar-file-picker]")}#t(){this.#r()&&(this.innerHTML=this.#i())}#e=t=>{const n=t.target.closest("[data-house-md-action]")?.dataset?.houseMdAction;n&&(t.preventDefault(),e(this,"house-md:toolbar-action",{houseMdAction:n}))};#s=t=>{for(const n of t.target.files)e(this,"house-md:toolbar-action",{houseMdAction:"uploadFile",file:n})};#r(){return 0===this.children.length}#i(){return'\n \n \n \n \n \n \n \n '}}customElements.define("house-md-toolbar",n);class s{start=0;end=0;static fromDOMRange(t,e){const{startOffset:n,startContainer:i,endOffset:o,endContainer:l}=t,a=r(e,i,n);if(t.collapsed)return new s(e,a,a);const c=r(e,l,o);return new s(e,a,c)}constructor(t,e,n){this.container=t,this.start=e,this.end=n}toDOMRange(){const t=document.createRange();t.setStart(this.container,1),t.setEnd(this.container,this.container.childNodes.length);const e=document.createTreeWalker(this.container,NodeFilter.SHOW_TEXT);let n=0,s=!1,r=!1;for(;e.nextNode()&&(!s||!r);){const i=e.currentNode.textContent.length;n+i>=this.start&&!s&&(s=!0,t.setStart(e.currentNode,this.start-n)),n+i>=this.end&&!r&&(r=!0,t.setEnd(e.currentNode,this.end-n)),n+=i}return t}isEqual(t){return this.container===t.container&&this.start===t.start&&this.end===t.end}}function r(t,e,n){return e.nodeType===Node.ELEMENT_NODE?function(t,e,n){const s=e.childNodes[n];let r=0;const i=document.createTreeWalker(t);for(;i.nextNode();){if(i.currentNode===s)return r;i.currentNode.nodeType===Node.TEXT_NODE&&(r+=i.currentNode.textContent.length)}return r}(t,e,n):function(t,e,n){const s=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;s.nextNode();){if(s.currentNode===e)return n;n+=s.currentNode.textContent.length}return n}(t,e,n)}class i{#o;#l;#a;constructor(t){this.#o=t,this.#l=new s(this.contentElement,0,0),this.#a=""}current(){return{start:this.start,end:this.end,selection:this.#a}}currentLine(){return this.lineAt(this.end)}previousLine(){const{content:t}=this.#o,e=Math.max(t.lastIndexOf("\n",this.start-1),0);return this.lineAt(e)}lineAt(t){const{content:e}=this.#o,n=Math.max(e.lastIndexOf("\n",t-1)+1,0),s=e.indexOf("\n",t),r=-1===s?e.length:s+1;return{start:n,end:r,content:e.slice(n,r)}}select({start:t,end:e}){this.documentRange=new s(this.contentElement,t,e);const n=this.documentRange.toDOMRange();if(n){const t=window.getSelection();t.removeAllRanges(),t.addRange(n),this.update()}else console.error("Failed to create range",{start:t,end:e})}update=()=>{const t=document.getSelection()?.getRangeAt(0),n=this.#l;t.intersectsNode(this.contentElement)&&(this.#l=s.fromDOMRange(t,this.contentElement),this.#a=this.#o.content.slice(this.start,this.end)),this.#l.isEqual(n)||e(this.element,"house-md:selectionchange",{start:this.start,end:this.end})};get start(){return this.#l.start}get end(){return this.#l.end}get element(){return this.#o.element}get contentElement(){return this.#o.contentElement}get isEmpty(){return this.start===this.end}get isMultiline(){return this.current().selection.includes("\n")}get isBold(){return this.#c("strong")}get isItalic(){return this.#c("em")}get isStrikethrough(){return this.#c("s")}get isLink(){return this.#c(".link")}get isCode(){return this.#c(".code")}get isQuote(){return this.#c(".quote")}get isBulletList(){return this.#c(".ul-li")}get isNumberList(){return this.#c(".ol-li")}#c(t){let e=window.getSelection().getRangeAt(0).endContainer;if(e.nodeType===Node.TEXT_NODE&&(e=e.parentElement),this.contentElement.contains(e))return!!e.closest(t)}}class o{static saveInterval=1e3;constructor(t=500){this.maxSize=t,this.undoStack=[],this.redoStack=[]}undo(t){const e=this.undoStack.pop();return e?(this.redoStack.push(t),e):{}}redo(t){const e=this.redoStack.pop();return e?(this.undoStack.push(t),e):{}}add=function(t){let e;return(...n)=>{e||(t(...n),e=setTimeout((()=>{e=null}),o.saveInterval))}}((t=>{this.undoStack.push(t),this.redoStack=[],this.undoStack.length>this.maxSize&&this.undoStack.shift()}))}function l(t){let e=t;return e=function(t){return t=t.replaceAll(/&/g,"&"),t=t.replaceAll(//g,">"),t=t.replaceAll(/"/g,"""),t=t.replaceAll(/'/g,"'"),t}(e),e=function(t){return t=function(t){return t=t.replaceAll(/(?***$1***"),t=t.replaceAll(/(?___$1___"),t}(t),t=function(t){return t=t.replaceAll(/(?**$1**"),t=t.replaceAll(/(?__$1__"),t}(t),t=function(t){return t=t.replaceAll(/(?*$1*"),t=t.replaceAll(/(?_$1_"),t}(t),t=function(t){return t.replaceAll(/~~(.*)?~~/gm,"~~$1~~")}(t),t=function(t){return t.replaceAll(/==(.*?)==/gm,"==$1==")}(t),t}(e),e=function(t){return t=function(t){return t=t.replaceAll(/^# (.*)$/gm,'# $1'),t=t.replaceAll(/^## (.*)$/gm,'## $1'),t=t.replaceAll(/^### (.*)$/gm,'### $1'),t=t.replaceAll(/^#### (.*)$/gm,'#### $1'),t=t.replaceAll(/^##### (.*)$/gm,'##### $1'),t=t.replaceAll(/^###### (.*)$/gm,'###### $1'),t}(t),t=function(t){return t.replaceAll(/^(\d+\.\s.*)$/gm,'$1')}(t),t=function(t){return t.replaceAll(/^(-\s.*)$/gm,'$1')}(t),t=function(t){return t.replaceAll(/^>(.*)$/gm,'>$1')}(t),t}(e),e=function(t){return function(t){return t.replaceAll(/\n---\n/gm,'\n---\n')}(t)}(e),e=function(t){return t.replaceAll(/(?[$1]($2)')}(e),e=function(t){return t.replaceAll(/!\[(.*?)\]\((.*?)\)/gm,'$&')}(e),e=function(t){return t=t.replaceAll(/^```(.*?)```$/gms,'```$1```'),t=t.replaceAll(/([^`])`([^`]+)`([^`])/gm,'$1`$2`$3'),t}(e),e=function(t){return t.replaceAll(/<!--(.*?)-->/gm,'<!--$1-->')}(e),e}class a{constructor(t,e){this.content=t,this.element=e,this.selection=new i(this),this.history=new o}select(t,e){this.selection.select({start:t,end:e})}insertText(t){this.replaceText(t,this.currentSelection.start,this.currentSelection.end)}replaceText(t,e,n){const s=this.content.slice(0,e),r=`${s}${t}${this.content.slice(n)}`,i={start:s.length+t.length,end:s.length+t.length};this.#h(),this.#d(r,i)}insertParagraph(t,e){this.select(t,e),this.selection.isBulletList?this.#u():this.selection.isNumberList?this.#m():this.replaceText("\n",t,e)}insertLink(t,e,n=""){const{start:s,end:r}=this.currentSelection,i=` ${n}[${t}](${e}) `;this.replaceText(i,s,r),this.selection.select({start:s+i.length,end:s+i.length})}insertFile(t,e,n){n.startsWith("image/")?this.insertImage(t,e):this.insertLink(t,e)}insertImage(t,e){this.insertLink(t,e,"!")}deleteText(t,e){this.replaceText("",Math.max(t,0),e)}deleteCurrentLine(){const{start:t,end:e}=this.currentLine;this.deleteText(t,e)}toggleBold(){this.#p(this.selection.isBold,"**")}toggleItalic(){this.#p(this.selection.isItalic,"_")}toggleStrikethrough(){this.#p(this.selection.isStrikethrough,"~~")}toggleCode(){const[t,e]=this.selection.isMultiline?["```\n","\n```"]:["`","`"];this.#p(this.selection.isCode,t,e)}toggleLink(){this.selection.isLink?this.#g():this.#v()}toggleQuote(){this.selection.isQuote?this.#f("> "):this.#L("> ")}toggleBulletList(){this.selection.isBulletList?this.#f("- "):this.#L("- ")}toggleNumberList(){this.selection.isNumberList?this.#f(/\d+\. /):this.#L(`${this.#b+1}. `)}undo(){const{content:e,start:n,end:s}=this.history.undo(this.#x);t(e)&&this.#d(e,{start:n,end:s})}redo(){const{content:e,start:n,end:s}=this.history.redo(this.#x);t(e)&&this.#d(e,{start:n,end:s})}render(){this.contentElement.innerHTML=l(this.content)+""}get isEmpty(){return!this.content.trim()}get contentElement(){return this.element.contentWrapper}get currentSelection(){return this.selection.current()}get currentLine(){return this.selection.currentLine()}#p(t,e,n=e){t?this.#E(e,n):this.#w(e,n)}#w(t,e=t){const{start:n,end:s,selection:r}=this.currentSelection,i=`${t}${r}${e}`;this.replaceText(i,n,s),this.selection.select({start:n,end:n+r.length+t.length+e.length})}#E(t,e=t){const{start:n,end:s,selection:r}=this.currentSelection;r.startsWith(t)&&r.endsWith(e)&&(this.replaceText("",s-e.length,s),this.replaceText("",n,n+t.length),this.selection.select({start:n,end:s-t.length-e.length}))}#v(){const{start:t,end:e,selection:n}=this.currentSelection,s=this.selection.isEmpty,r=s?"title":n;this.replaceText(`[${r}](url)`,t,e),s?this.selection.select({start:t+1,end:t+r.length+1}):this.selection.select({start:t+n.length+3,end:t+n.length+6})}#g(){const{start:t,end:e,selection:n}=this.currentSelection,s=n.replace(/\[(.*)\]\(.*\)/,"$1");this.replaceText(s,t,e)}#u(){this.#T.match(/^-\s+$/)?this.deleteCurrentLine():(this.replaceText("\n",this.currentSelection.end,this.currentSelection.end),this.replaceText("- ",this.currentSelection.end+1,this.currentSelection.end+1))}#m(){this.#T.match(/^\d+\.\s+$/)?this.deleteCurrentLine():(this.replaceText("\n",this.currentSelection.end,this.currentSelection.end),this.replaceText(`${this.#b+1}. `,this.currentSelection.end+1,this.currentSelection.end+1))}#L(t){const{start:e,end:n}=this.currentSelection,s=this.currentLine;this.replaceText(t,s.start,s.start),this.selection.select({start:e+t.length,end:n+t.length})}#f(t){const{start:e,end:n}=this.currentSelection,s=this.currentLine;if(t instanceof RegExp){const e=s.content.match(t);if(!e)return;t=e[0]}s.content.startsWith(t)&&(this.deleteText(s.start,s.start+t.length),this.selection.select({start:e-t.length,end:n-t.length}))}#h(){this.history.add(this.#x)}#d(t,n){const s=this.content;this.content=t,this.element.value=t,this.selection.select(n),e(this.element,"house-md:change",{previousContent:s,newContent:t})}get#x(){const{start:t,end:e}=this.currentSelection;return{content:this.content,start:t,end:e}}get#T(){return this.currentLine.content}get#b(){const t=this.selection.previousLine().content.match(/^(\d+). /);return t?parseInt(t[1]):0}}class c{#S;constructor(t){this.#S=t}asPlainText(){const t=(new window.DOMParser).parseFromString(this.#S,"text/html");return this.#A(t.body).trim()}#A(t){return t.nodeType===Node.TEXT_NODE?t.nodeValue:Array.from(t.childNodes).map(((e,n)=>{const s=this.#k(e),r=n>0&&this.#k(t.childNodes[n-1]);return(s&&r?"\n\n":"")+this.#A(e)})).join("")}#k(t){return["BR","DIV","H1","H2","H3","H4","H5","H6","LI","P","TR"].includes(t.tagName)}}class h{constructor(t){this.document=t}handleInput(t,e){t.preventDefault();const n=this[`${t.inputType}Handler`];if(n){const r=t.getTargetRanges()[0],{start:i,end:o}=s.fromDOMRange(r,e);n.call(this,t,i,o)}else console.error("Not handling:",t.inputType)}insertTextHandler(t,e,n){this.#C(t,e,n)}insertCompositionTextHandler(t,e,n){this.#C(t,e,n)}insertParagraphHandler(t,e,n){this.document.insertParagraph(e,n)}insertLinkHandler(t,e,n){this.#C(t,e,n)}insertFromYankHandler(t,e,n){this.#C(t,e,n)}insertFromDropHandler(t,e,n){this.#C(t,e,n)}insertFromPasteAsQuotationHandler(t,e,n){const s=t.dataTransfer.getData("text/plain");this.document.replaceText(`> ${s}`,e,n)}insertTransposeHandler(t,e,n){this.#C(t,e,n)}insertReplacementTextHandler(t,e,n){this.#C(t,e,n)}insertFromPasteHandler(t,e,n){this.#C(t,e,n)}deleteContentBackwardHandler(t,e,n){this.document.deleteText(e,n)}deleteByCutHandler(t,e,n){this.document.deleteText(e,n)}deleteWordBackwardHandler(t,e,n){this.document.deleteText(e,n)}deleteWordForwardHandler(t,e,n){this.document.deleteText(e,n)}deleteSoftLineBackwardHandler(t,e,n){this.document.deleteText(e,n)}deleteSoftLineForwardHandler(t,e,n){this.document.deleteText(e,n)}deleteEntireSoftLineHandler(t,e,n){this.document.deleteText(e,n)}deleteHardLineBackwardHandler(t,e,n){this.document.deleteText(e,n)}deleteHardLineForwardHandler(t,e,n){this.document.deleteText(e,n)}deleteByDragHandler(t,e,n){this.document.deleteText(e,n)}deleteContentHandler(t,e,n){this.document.deleteText(e,n)}deleteContentForwardHandler(t,e,n){this.document.deleteText(e,n)}#C(t,e,n){let s;if(t.data)s=t.data;else{const e=t.dataTransfer.getData("text/html");s=e?new c(e).asPlainText():t.dataTransfer.getData("text/plain")}this.document.replaceText(s,e,n)}}class d{constructor(t,e){this.element=t.closest("house-md"),this.file=e}upload(){if(e(this.element,"house-md:before-upload",{file:this.file})){const t=document.createElement("house-md-upload");t.file=this.file,t.uploadsURL=this.#$,this.element.appendChild(t)}}get#$(){return this.element.dataset.uploadsUrl||document.head.querySelector("meta[name=house-uploads-url]")?.content||"/uploads"}}class u{constructor(t){this.document=t}handleAction(t,e){const n=this[t];n?n.call(this,e):console.error("Not handling:",t)}bold(){this.document.toggleBold()}italic(){this.document.toggleItalic()}strikethrough(){this.document.toggleStrikethrough()}quote(){this.document.toggleQuote()}code(){this.document.toggleCode()}link(){this.document.toggleLink()}bulletList(){this.document.toggleBulletList()}numberList(){this.document.toggleNumberList()}undo(){this.document.undo()}redo(){this.document.redo()}uploadFile({file:t}){new d(this.document.element,t).upload()}}class m{constructor(t){this.element=t.closest("house-md")}handleDragOver(t){t.preventDefault()}async handleDrop(t){t.preventDefault();for(const e of t.dataTransfer.items)"file"===e.kind&&new d(this.element,e.getAsFile()).upload()}}const p={"Ctrl+B":"bold","Ctrl+I":"italic","Ctrl+S":"strikethrough","Ctrl+Z":"undo","Ctrl+Shift+Z":"redo"};class g extends HTMLElement{static formAssociated=!0;constructor(){super(),this.internals=this.attachInternals(),this.internals.ariaRole="textbox"}connectedCallback(){const t=this.querySelector(".house-md-content")?.textContent||this.textContent;this.document=new a(t,this),this.#y(),this.#H(),this.inputHandler=new h(this.document),this.actionHandler=new u(this.document),this.dropAndDropHandler=new m(this),this.autofocus&&this.focus(),this.setAttribute("initialized",""),this.value=t}disconnectedCallback(){this.#N()}validate=t=>{this.required&&this.document.isEmpty?(t.preventDefault(),this.internals.setValidity({valueMissing:!0},"This field is required.",this.element),this.focus()):this.internals.setValidity({})};render(){this.document.render()}focus(){this.contentWrapper.focus()}get required(){return this.hasAttribute("required")}set required(t){t?this.setAttribute("required",""):this.removeAttribute("required")}get autofocus(){return this.hasAttribute("autofocus")}set autofocus(t){t?this.setAttribute("autofocus",""):this.removeAttribute("autofocus")}get value(){return this.document.content}set value(t){this.document.content=t,this.internals.setFormValue(t),this.internals.setValidity({}),this.render()}get form(){return this.internals.form}#y(){this.textContent="",this.toolbar=this.#_(),this.contentWrapper=document.createElement("div"),this.contentWrapper.classList.add("house-md-content"),this.contentWrapper.setAttribute("contenteditable",!0),this.contentWrapper.setAttribute("placeholder",this.getAttribute("placeholder")),this.append(this.contentWrapper),this.#D(),this.getAttribute("tabindex")||this.contentWrapper.setAttribute("tabindex",0)}#D(){this.contentWrapper.classList.toggle("house-md-content-empty",this.document.isEmpty)}#_(){if(this.getAttribute("toolbar"))return document.getElementById(this.getAttribute("toolbar"));{const t=document.createElement("house-md-toolbar");return this.prepend(t),t}}#H(){this.addEventListener("beforeinput",this.#I),this.addEventListener("keydown",this.#B),this.addEventListener("dragover",this.#F),this.addEventListener("drop",this.#R),this.addEventListener("house-md:change",this.#M),this.toolbar.addEventListener("house-md:toolbar-action",this.#z),document.addEventListener("selectionchange",this.#O),this.internals.form.addEventListener("submit",this.validate)}#N(){this.removeEventListener("beforeinput",this.#I),this.removeEventListener("keydown",this.#B),this.removeEventListener("dragover",this.#F),this.removeEventListener("drop",this.#R),this.removeEventListener("house-md:change",this.#M),this.toolbar.removeEventListener("house-md:toolbar-action",this.#z),document.removeEventListener("selectionchange",this.#O),this.internals.form.removeEventListener("submit",this.validate)}#I=t=>{this.inputHandler.handleInput(t,this.contentWrapper)};#B(t){const e=function(t){return p[function(t){const e=[];return(t.metaKey||t.ctrlKey)&&e.push("Ctrl"),t.altKey&&e.push("Alt"),t.shiftKey&&e.push("Shift"),e.push(t.key.toUpperCase()),e.join("+")}(t)]}(t);e&&(t.preventDefault(),this.actionHandler.handleAction(e))}#F=t=>{this.dropAndDropHandler.handleDragOver(t)};#R=t=>{this.dropAndDropHandler.handleDrop(t)};#M=()=>{this.#D()};#z=({detail:t})=>{this.actionHandler.handleAction(t.houseMdAction,t)};#O=()=>{this.document.selection.update()}}customElements.define("house-md",g);class v extends HTMLElement{#W;#$;#q;constructor(){super(),this.percentComplete=0,this.status="pending"}connectedCallback(){this.#t(),this.#P(),this.addEventListener("click",this.#e)}disconnectedCallback(){this.removeEventListener("click",this.#e)}async#P(){const t=new FormData;t.append("file",this.file),this.csrfParamName&&t.append(this.csrfParamName,this.csrfToken),this.xhr=new XMLHttpRequest,this.xhr.open("POST",this.uploadsURL,!0),this.xhr.upload.onprogress=this.#U,this.xhr.onload=this.#K,this.xhr.onerror=this.#Q,this.xhr.onabort=this.#V,this.xhr.send(t),this.status="uploading",this.#t()}#t(){this.setAttribute("status",this.status),this.innerHTML=`\n ${this.#X()}\n ${this.#j()}\n ${this.#Z()}\n ${this.#J()}\n `}#X(){return"failed"===this.status?"":""}#j(){return`
${this.file.name}
`}#Z(){return`${this.percentComplete}%`}#J(){return this.#q?`
${this.#q}
`:""}#U=t=>{t.lengthComputable&&(this.percentComplete=t.loaded/t.total*100),this.#t()};#K=async()=>{if(this.xhr.status>=400)this.status="failed",this.#q=this.xhr.responseText||"Upload failed";else{const t=JSON.parse(this.xhr.responseText);await this.document.insertFile(t.fileName,t.fileUrl,t.mimetype),this.status="complete",this.#Y()}this.#t()};#Q=()=>{this.status="failed",this.#t()};#V=()=>{this.status="aborted",this.#t()};#e=t=>{t.target.matches("[data-md-action=close]")&&this.remove()};async#Y(t=500){await function(t){return new Promise((e=>setTimeout(e,t)))}(t),this.remove()}get document(){return this.closest("house-md").document}get file(){return this.#W}set file(t){this.#W=t}get uploadsURL(){return this.#$}set uploadsURL(t){this.#$=t}get csrfParamName(){return document.head.querySelector("meta[name=csrf-param]")?.content}get csrfToken(){return document.head.querySelector("meta[name=csrf-token]")?.content}}customElements.define("house-md-upload",v);export{a as Document,g as Editor,o as History,i as Selection,n as Toolbar,v as Upload};