From 10b2aad6478708431000bcfb4ab495875688f150 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 23 May 2025 11:41:08 +0200 Subject: [PATCH 01/58] Replace House with lexical-powered actiontext --- Gemfile | 4 +- Gemfile.lock | 57 ++- app/assets/stylesheets/actiontext.css | 440 ++++++++++++++++++ app/helpers/notifications_helper.rb | 2 +- app/helpers/rich_textarea_helper.rb | 29 ++ app/javascript/application.js | 3 + .../controllers/local_save_controller.js | 4 +- app/models/card.rb | 2 +- app/models/comment.rb | 6 +- app/models/concerns/mentions.rb | 8 +- app/views/active_storage/blobs/_blob.html.erb | 14 + app/views/cards/comments/_comment.html.erb | 2 +- app/views/cards/comments/_new.html.erb | 3 +- app/views/cards/comments/edit.html.erb | 3 +- app/views/cards/container/_title.html.erb | 6 +- app/views/cards/edit.html.erb | 11 +- app/views/events/event/_comment.html.erb | 2 +- .../action_text/contents/_content.html.erb | 3 + config/importmap.rb | 3 + config/initializers/markdown.rb | 7 - ...4_create_action_text_tables.action_text.rb | 26 ++ db/schema.rb | 12 +- db/schema_cache.yml | 194 ++++---- lib/markdown_renderer.rb | 81 ---- lib/rails_ext/action_text_has_markdown.rb | 47 -- lib/rails_ext/action_text_markdown.rb | 44 -- lib/rails_ext/action_text_tag_helper.rb | 36 -- lib/rails_ext/active_storage_sluggable.rb | 32 -- script/migrate-to-multi-tenant | 20 +- script/reset_cards_ids.rb | 6 +- script/reset_collections_ids.rb | 6 +- test/controllers/cards_controller_test.rb | 2 +- test/fixtures/action_text/rich_texts.yml | 4 + 33 files changed, 737 insertions(+), 382 deletions(-) create mode 100644 app/assets/stylesheets/actiontext.css create mode 100644 app/helpers/rich_textarea_helper.rb create mode 100644 app/views/active_storage/blobs/_blob.html.erb create mode 100644 app/views/layouts/action_text/contents/_content.html.erb delete mode 100644 config/initializers/markdown.rb create mode 100644 db/migrate/20250523084734_create_action_text_tables.action_text.rb delete mode 100644 lib/markdown_renderer.rb delete mode 100644 lib/rails_ext/action_text_has_markdown.rb delete mode 100644 lib/rails_ext/action_text_markdown.rb delete mode 100644 lib/rails_ext/action_text_tag_helper.rb delete mode 100644 lib/rails_ext/active_storage_sluggable.rb create mode 100644 test/fixtures/action_text/rich_texts.yml diff --git a/Gemfile b/Gemfile index 64a32d01a..85052847e 100644 --- a/Gemfile +++ b/Gemfile @@ -24,9 +24,10 @@ gem "thruster", require: false gem "bcrypt", "~> 3.1.7" gem "geared_pagination", "~> 1.2" gem "rqrcode" -gem "redcarpet" gem "rouge" gem "jbuilder" +gem "actiontext-lexical", path: "/Users/jorge/Work/jorgemanrubia/actiontext-lexical" +gem "image_processing", "~> 1.14" # Telemetry and logging gem "sentry-ruby" @@ -51,3 +52,4 @@ group :test do gem "webmock" gem "vcr" end + diff --git a/Gemfile.lock b/Gemfile.lock index b8dd7eebb..a0a44f206 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/active_record-tenanted - revision: 612f5e8b7f77b7076a7eb21521920630161461aa + revision: 173048c2af29e9a723811e552bc59e3154e82e2a specs: active_record-tenanted (0.1.0) activerecord (>= 8.1.alpha) @@ -128,6 +128,16 @@ GIT thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) +PATH + remote: /Users/jorge/Work/jorgemanrubia/actiontext-lexical + specs: + actiontext-lexical (0.1.0) + capybara + cuprite + image_processing + rails (>= 8.0.2) + selenium-webdriver + GEM remote: https://rubygems.org/ specs: @@ -163,13 +173,17 @@ GEM bigdecimal rexml crass (1.0.6) + cuprite (0.17) + capybara (~> 3.0) + ferrum (~> 0.17.0) date (3.4.1) debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) dotenv (3.1.8) - drb (2.2.1) + drb (2.2.3) ed25519 (1.4.0) + erb (5.0.1) erubi (1.13.1) et-orbi (1.2.11) tzinfo @@ -184,6 +198,16 @@ GEM net-http (>= 0.5.0) faraday-retry (2.3.1) faraday (~> 2.0) + ferrum (0.17.1) + addressable (~> 2.5) + base64 (~> 0.2) + concurrent-ruby (~> 1.1) + webrick (~> 1.7) + websocket-driver (~> 0.7) + ffi (1.17.2) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -195,6 +219,9 @@ GEM hashdiff (1.1.2) i18n (1.14.7) concurrent-ruby (~> 1.0) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) importmap-rails (2.1.0) actionpack (>= 6.0.0) activesupport (>= 6.0.0) @@ -222,7 +249,7 @@ GEM language_server-protocol (3.17.0.4) lint_roller (1.1.0) logger (1.7.0) - loofah (2.24.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -232,8 +259,11 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + mini_magick (5.2.0) + benchmark + logger mini_mime (1.1.5) - mini_portile2 (2.8.8) + mini_portile2 (2.8.9) minitest (5.25.5) msgpack (1.8.0) multipart-post (2.4.1) @@ -277,7 +307,7 @@ GEM activesupport (>= 7.0.0) rack railties (>= 7.0.0) - psych (5.2.3) + psych (5.2.6) date stringio public_suffix (6.0.1) @@ -285,7 +315,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.1.14) + rack (3.1.15) rack-contrib (2.5.0) rack (< 4) rack-session (2.1.1) @@ -295,7 +325,7 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) - rails-dom-testing (2.2.0) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) @@ -305,9 +335,9 @@ GEM rainbow (3.1.1) rake (13.2.1) rb_sys (0.9.106) - rdoc (6.13.1) + rdoc (6.14.0) + erb psych (>= 4.0.0) - redcarpet (3.6.1) regexp_parser (2.10.0) reline (0.6.1) io-console (~> 0.5) @@ -346,6 +376,9 @@ GEM rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) + ruby-vips (2.2.3) + ffi (~> 1.12) + logger rubyzip (2.4.1) securerandom (0.4.1) selenium-webdriver (4.31.0) @@ -423,6 +456,7 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) + webrick (1.9.1) websocket (1.2.11) websocket-driver (0.7.7) base64 @@ -430,7 +464,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.2) + zeitwerk (2.7.3) PLATFORMS arm64-darwin @@ -440,6 +474,7 @@ PLATFORMS x86_64-mingw32 DEPENDENCIES + actiontext-lexical! active_record-tenanted! bcrypt (~> 3.1.7) bootsnap @@ -448,6 +483,7 @@ DEPENDENCIES capybara debug geared_pagination (~> 1.2) + image_processing (~> 1.14) importmap-rails jbuilder kamal @@ -455,7 +491,6 @@ DEPENDENCIES puma (>= 5.0) rails! rails_structured_logging! - redcarpet rouge rqrcode rubocop-rails-omakase diff --git a/app/assets/stylesheets/actiontext.css b/app/assets/stylesheets/actiontext.css new file mode 100644 index 000000000..9b6bcb064 --- /dev/null +++ b/app/assets/stylesheets/actiontext.css @@ -0,0 +1,440 @@ +/* + * Default Trix editor styles. See Action Text overwrites below. +*/ + +trix-editor { + border: 1px solid #bbb; + border-radius: 3px; + margin: 0; + padding: 0.4em 0.6em; + min-height: 5em; + outline: none; } + +trix-toolbar * { + box-sizing: border-box; } + +trix-toolbar .trix-button-row { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + overflow-x: auto; } + +trix-toolbar .trix-button-group { + display: flex; + margin-bottom: 10px; + border: 1px solid #bbb; + border-top-color: #ccc; + border-bottom-color: #888; + border-radius: 3px; } + trix-toolbar .trix-button-group:not(:first-child) { + margin-left: 1.5vw; } + @media (max-width: 768px) { + trix-toolbar .trix-button-group:not(:first-child) { + margin-left: 0; } } + +trix-toolbar .trix-button-group-spacer { + flex-grow: 1; } + @media (max-width: 768px) { + trix-toolbar .trix-button-group-spacer { + display: none; } } + +trix-toolbar .trix-button { + position: relative; + float: left; + color: rgba(0, 0, 0, 0.6); + font-size: 0.75em; + font-weight: 600; + white-space: nowrap; + padding: 0 0.5em; + margin: 0; + outline: none; + border: none; + border-bottom: 1px solid #ddd; + border-radius: 0; + background: transparent; } + trix-toolbar .trix-button:not(:first-child) { + border-left: 1px solid #ccc; } + trix-toolbar .trix-button.trix-active { + background: #cbeefa; + color: black; } + trix-toolbar .trix-button:not(:disabled) { + cursor: pointer; } + trix-toolbar .trix-button:disabled { + color: rgba(0, 0, 0, 0.125); } + @media (max-width: 768px) { + trix-toolbar .trix-button { + letter-spacing: -0.01em; + padding: 0 0.3em; } } + +trix-toolbar .trix-button--icon { + font-size: inherit; + width: 2.6em; + height: 1.6em; + max-width: calc(0.8em + 4vw); + text-indent: -9999px; } + @media (max-width: 768px) { + trix-toolbar .trix-button--icon { + height: 2em; + max-width: calc(0.8em + 3.5vw); } } + trix-toolbar .trix-button--icon::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.6; + content: ""; + background-position: center; + background-repeat: no-repeat; + background-size: contain; } + @media (max-width: 768px) { + trix-toolbar .trix-button--icon::before { + right: 6%; + left: 6%; } } + trix-toolbar .trix-button--icon.trix-active::before { + opacity: 1; } + trix-toolbar .trix-button--icon:disabled::before { + opacity: 0.125; } + +trix-toolbar .trix-button--icon-attach::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M10.5%2018V7.5c0-2.25%203-2.25%203%200V18c0%204.125-6%204.125-6%200V7.5c0-6.375%209-6.375%209%200V18%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-miterlimit%3D%2210%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E"); + top: 8%; + bottom: 4%; } + +trix-toolbar .trix-button--icon-bold::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M6.522%2019.242a.5.5%200%200%201-.5-.5V5.35a.5.5%200%200%201%20.5-.5h5.783c1.347%200%202.46.345%203.24.982.783.64%201.216%201.562%201.216%202.683%200%201.13-.587%202.129-1.476%202.71a.35.35%200%200%200%20.049.613c1.259.56%202.101%201.742%202.101%203.22%200%201.282-.483%202.334-1.363%203.063-.876.726-2.132%201.12-3.66%201.12h-5.89ZM9.27%207.347v3.362h1.97c.766%200%201.347-.17%201.733-.464.38-.291.587-.716.587-1.27%200-.53-.183-.928-.513-1.198-.334-.273-.838-.43-1.505-.43H9.27Zm0%205.606v3.791h2.389c.832%200%201.448-.177%201.853-.497.399-.315.614-.786.614-1.423%200-.62-.22-1.077-.63-1.385-.418-.313-1.053-.486-1.905-.486H9.27Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-italic::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M9%205h6.5v2h-2.23l-2.31%2010H13v2H6v-2h2.461l2.306-10H9V5Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-link::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M18.948%205.258a4.337%204.337%200%200%200-6.108%200L11.217%206.87a.993.993%200%200%200%200%201.41c.392.39%201.027.39%201.418%200l1.623-1.613a2.323%202.323%200%200%201%203.271%200%202.29%202.29%200%200%201%200%203.251l-2.393%202.38a3.021%203.021%200%200%201-4.255%200l-.05-.049a1.007%201.007%200%200%200-1.418%200%20.993.993%200%200%200%200%201.41l.05.049a5.036%205.036%200%200%200%207.091%200l2.394-2.38a4.275%204.275%200%200%200%200-6.072Zm-13.683%2013.6a4.337%204.337%200%200%200%206.108%200l1.262-1.255a.993.993%200%200%200%200-1.41%201.007%201.007%200%200%200-1.418%200L9.954%2017.45a2.323%202.323%200%200%201-3.27%200%202.29%202.29%200%200%201%200-3.251l2.344-2.331a2.579%202.579%200%200%201%203.631%200c.392.39%201.027.39%201.419%200a.993.993%200%200%200%200-1.41%204.593%204.593%200%200%200-6.468%200l-2.345%202.33a4.275%204.275%200%200%200%200%206.072Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-strike::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M6%2014.986c.088%202.647%202.246%204.258%205.635%204.258%203.496%200%205.713-1.728%205.713-4.463%200-.275-.02-.536-.062-.781h-3.461c.398.293.573.654.573%201.123%200%201.035-1.074%201.787-2.646%201.787-1.563%200-2.773-.762-2.91-1.924H6ZM6.432%2010h3.763c-.632-.314-.914-.715-.914-1.273%200-1.045.977-1.739%202.432-1.739%201.475%200%202.52.723%202.617%201.914h2.764c-.05-2.548-2.11-4.238-5.39-4.238-3.145%200-5.392%201.719-5.392%204.316%200%20.363.04.703.12%201.02ZM4%2011a1%201%200%201%200%200%202h15a1%201%200%201%200%200-2H4Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-quote::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M4.581%208.471c.44-.5%201.056-.834%201.758-.995C8.074%207.17%209.201%207.822%2010%208.752c1.354%201.578%201.33%203.555.394%205.277-.941%201.731-2.788%203.163-4.988%203.56a.622.622%200%200%201-.653-.317c-.113-.205-.121-.49.16-.764.294-.286.567-.566.791-.835.222-.266.413-.54.524-.815.113-.28.156-.597.026-.908-.128-.303-.39-.524-.72-.69a3.02%203.02%200%200%201-1.674-2.7c0-.905.283-1.59.72-2.088Zm9.419%200c.44-.5%201.055-.834%201.758-.995%201.734-.306%202.862.346%203.66%201.276%201.355%201.578%201.33%203.555.395%205.277-.941%201.731-2.789%203.163-4.988%203.56a.622.622%200%200%201-.653-.317c-.113-.205-.122-.49.16-.764.294-.286.567-.566.791-.835.222-.266.412-.54.523-.815.114-.28.157-.597.026-.908-.127-.303-.39-.524-.72-.69a3.02%203.02%200%200%201-1.672-2.701c0-.905.283-1.59.72-2.088Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-heading-1::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21.5%207.5v-3h-12v3H14v13h3v-13h4.5ZM9%2013.5h3.5v-3h-10v3H6v7h3v-7Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-code::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3.293%2011.293a1%201%200%200%200%200%201.414l4%204a1%201%200%201%200%201.414-1.414L5.414%2012l3.293-3.293a1%201%200%200%200-1.414-1.414l-4%204Zm13.414%205.414%204-4a1%201%200%200%200%200-1.414l-4-4a1%201%200%201%200-1.414%201.414L18.586%2012l-3.293%203.293a1%201%200%200%200%201.414%201.414Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-bullet-list::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%207.5a1.5%201.5%200%201%200%200-3%201.5%201.5%200%200%200%200%203ZM8%206a1%201%200%200%201%201-1h11a1%201%200%201%201%200%202H9a1%201%200%200%201-1-1Zm1%205a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm0%206a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm-2.5-5a1.5%201.5%200%201%201-3%200%201.5%201.5%200%200%201%203%200ZM5%2019.5a1.5%201.5%200%201%200%200-3%201.5%201.5%200%200%200%200%203Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-number-list::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3%204h2v4H4V5H3V4Zm5%202a1%201%200%200%201%201-1h11a1%201%200%201%201%200%202H9a1%201%200%200%201-1-1Zm1%205a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm0%206a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm-3.5-7H6v1l-1.5%202H6v1H3v-1l1.667-2H3v-1h2.5ZM3%2017v-1h3v4H3v-1h2v-.5H4v-1h1V17H3Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-undo::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3%2014a1%201%200%200%200%201%201h6a1%201%200%201%200%200-2H6.257c2.247-2.764%205.151-3.668%207.579-3.264%202.589.432%204.739%202.356%205.174%205.405a1%201%200%200%200%201.98-.283c-.564-3.95-3.415-6.526-6.825-7.095C11.084%207.25%207.63%208.377%205%2011.39V8a1%201%200%200%200-2%200v6Zm2-1Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-redo::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21%2014a1%201%200%200%201-1%201h-6a1%201%200%201%201%200-2h3.743c-2.247-2.764-5.151-3.668-7.579-3.264-2.589.432-4.739%202.356-5.174%205.405a1%201%200%200%201-1.98-.283c.564-3.95%203.415-6.526%206.826-7.095%203.08-.513%206.534.614%209.164%203.626V8a1%201%200%201%201%202%200v6Zm-2-1Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-decrease-nesting-level::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%206a1%201%200%200%201%201-1h12a1%201%200%201%201%200%202H6a1%201%200%200%201-1-1Zm4%205a1%201%200%201%200%200%202h9a1%201%200%201%200%200-2H9Zm-3%206a1%201%200%201%200%200%202h12a1%201%200%201%200%200-2H6Zm-3.707-5.707a1%201%200%200%200%200%201.414l2%202a1%201%200%201%200%201.414-1.414L4.414%2012l1.293-1.293a1%201%200%200%200-1.414-1.414l-2%202Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-increase-nesting-level::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%206a1%201%200%200%201%201-1h12a1%201%200%201%201%200%202H6a1%201%200%200%201-1-1Zm4%205a1%201%200%201%200%200%202h9a1%201%200%201%200%200-2H9Zm-3%206a1%201%200%201%200%200%202h12a1%201%200%201%200%200-2H6Zm-2.293-2.293%202-2a1%201%200%200%200%200-1.414l-2-2a1%201%200%201%200-1.414%201.414L3.586%2012l-1.293%201.293a1%201%200%201%200%201.414%201.414Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-dialogs { + position: relative; } + +trix-toolbar .trix-dialog { + position: absolute; + top: 0; + left: 0; + right: 0; + font-size: 0.75em; + padding: 15px 10px; + background: #fff; + box-shadow: 0 0.3em 1em #ccc; + border-top: 2px solid #888; + border-radius: 5px; + z-index: 5; } + +trix-toolbar .trix-input--dialog { + font-size: inherit; + font-weight: normal; + padding: 0.5em 0.8em; + margin: 0 10px 0 0; + border-radius: 3px; + border: 1px solid #bbb; + background-color: #fff; + box-shadow: none; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; } + trix-toolbar .trix-input--dialog.validate:invalid { + box-shadow: #F00 0px 0px 1.5px 1px; } + +trix-toolbar .trix-button--dialog { + font-size: inherit; + padding: 0.5em; + border-bottom: none; } + +trix-toolbar .trix-dialog--link { + max-width: 600px; } + +trix-toolbar .trix-dialog__link-fields { + display: flex; + align-items: baseline; } + trix-toolbar .trix-dialog__link-fields .trix-input { + flex: 1; } + trix-toolbar .trix-dialog__link-fields .trix-button-group { + flex: 0 0 content; + margin: 0; } + +trix-editor [data-trix-mutable]:not(.attachment__caption-editor) { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + +trix-editor [data-trix-mutable]::-moz-selection, +trix-editor [data-trix-cursor-target]::-moz-selection, trix-editor [data-trix-mutable] ::-moz-selection { + background: none; } + +trix-editor [data-trix-mutable]::selection, +trix-editor [data-trix-cursor-target]::selection, trix-editor [data-trix-mutable] ::selection { + background: none; } + +trix-editor .attachment__caption-editor:focus[data-trix-mutable]::-moz-selection { + background: highlight; } + +trix-editor .attachment__caption-editor:focus[data-trix-mutable]::selection { + background: highlight; } + +trix-editor [data-trix-mutable].attachment.attachment--file { + box-shadow: 0 0 0 2px highlight; + border-color: transparent; } + +trix-editor [data-trix-mutable].attachment img { + box-shadow: 0 0 0 2px highlight; } + +trix-editor .attachment { + position: relative; } + trix-editor .attachment:hover { + cursor: default; } + +trix-editor .attachment--preview .attachment__caption:hover { + cursor: text; } + +trix-editor .attachment__progress { + position: absolute; + z-index: 1; + height: 20px; + top: calc(50% - 10px); + left: 5%; + width: 90%; + opacity: 0.9; + transition: opacity 200ms ease-in; } + trix-editor .attachment__progress[value="100"] { + opacity: 0; } + +trix-editor .attachment__caption-editor { + display: inline-block; + width: 100%; + margin: 0; + padding: 0; + font-size: inherit; + font-family: inherit; + line-height: inherit; + color: inherit; + text-align: center; + vertical-align: top; + border: none; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; } + +trix-editor .attachment__toolbar { + position: absolute; + z-index: 1; + top: -0.9em; + left: 0; + width: 100%; + text-align: center; } + +trix-editor .trix-button-group { + display: inline-flex; } + +trix-editor .trix-button { + position: relative; + float: left; + color: #666; + white-space: nowrap; + font-size: 80%; + padding: 0 0.8em; + margin: 0; + outline: none; + border: none; + border-radius: 0; + background: transparent; } + trix-editor .trix-button:not(:first-child) { + border-left: 1px solid #ccc; } + trix-editor .trix-button.trix-active { + background: #cbeefa; } + trix-editor .trix-button:not(:disabled) { + cursor: pointer; } + +trix-editor .trix-button--remove { + text-indent: -9999px; + display: inline-block; + padding: 0; + outline: none; + width: 1.8em; + height: 1.8em; + line-height: 1.8em; + border-radius: 50%; + background-color: #fff; + border: 2px solid highlight; + box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.25); } + trix-editor .trix-button--remove::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.7; + content: ""; + background-image: url("data:image/svg+xml,%3Csvg%20height%3D%2224%22%20width%3D%2224%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M19%206.41%2017.59%205%2012%2010.59%206.41%205%205%206.41%2010.59%2012%205%2017.59%206.41%2019%2012%2013.41%2017.59%2019%2019%2017.59%2013.41%2012z%22%2F%3E%3Cpath%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E"); + background-position: center; + background-repeat: no-repeat; + background-size: 90%; } + trix-editor .trix-button--remove:hover { + border-color: #333; } + trix-editor .trix-button--remove:hover::before { + opacity: 1; } + +trix-editor .attachment__metadata-container { + position: relative; } + +trix-editor .attachment__metadata { + position: absolute; + left: 50%; + top: 2em; + transform: translate(-50%, 0); + max-width: 90%; + padding: 0.1em 0.6em; + font-size: 0.8em; + color: #fff; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 3px; } + trix-editor .attachment__metadata .attachment__name { + display: inline-block; + max-width: 100%; + vertical-align: bottom; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + trix-editor .attachment__metadata .attachment__size { + margin-left: 0.2em; + white-space: nowrap; } + +.trix-content { + line-height: 1.5; + overflow-wrap: break-word; + word-break: break-word; } + .trix-content * { + box-sizing: border-box; + margin: 0; + padding: 0; } + .trix-content h1 { + font-size: 1.2em; + line-height: 1.2; } + .trix-content blockquote { + border: 0 solid #ccc; + border-left-width: 0.3em; + margin-left: 0.3em; + padding-left: 0.6em; } + .trix-content [dir=rtl] blockquote, + .trix-content blockquote[dir=rtl] { + border-width: 0; + border-right-width: 0.3em; + margin-right: 0.3em; + padding-right: 0.6em; } + .trix-content li { + margin-left: 1em; } + .trix-content [dir=rtl] li { + margin-right: 1em; } + .trix-content pre { + display: inline-block; + width: 100%; + vertical-align: top; + font-family: monospace; + font-size: 0.9em; + padding: 0.5em; + white-space: pre; + background-color: #eee; + overflow-x: auto; } + .trix-content img { + max-width: 100%; + height: auto; } + .trix-content .attachment { + display: inline-block; + position: relative; + max-width: 100%; } + .trix-content .attachment a { + color: inherit; + text-decoration: none; } + .trix-content .attachment a:hover, .trix-content .attachment a:visited:hover { + color: inherit; } + .trix-content .attachment__caption { + text-align: center; } + .trix-content .attachment__caption .attachment__name + .attachment__size::before { + content: ' \2022 '; } + .trix-content .attachment--preview { + width: 100%; + text-align: center; } + .trix-content .attachment--preview .attachment__caption { + color: #666; + font-size: 0.9em; + line-height: 1.2; } + .trix-content .attachment--file { + color: #333; + line-height: 1; + margin: 0 2px 2px 2px; + padding: 0.4em 1em; + border: 1px solid #bbb; + border-radius: 5px; } + .trix-content .attachment-gallery { + display: flex; + flex-wrap: wrap; + position: relative; } + .trix-content .attachment-gallery .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; } + .trix-content .attachment-gallery.attachment-gallery--2 .attachment, .trix-content .attachment-gallery.attachment-gallery--4 .attachment { + flex-basis: 50%; + max-width: 50%; } + +/* + * We need to override trix.css’s image gallery styles to accommodate the + * element we wrap around attachments. Otherwise, + * images in galleries will be squished by the max-width: 33%; rule. +*/ +.trix-content .attachment-gallery > action-text-attachment, +.trix-content .attachment-gallery > .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; +} + +.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--4 > .attachment { + flex-basis: 50%; + max-width: 50%; +} + +.trix-content action-text-attachment .attachment { + padding: 0 !important; + max-width: 100% !important; +} diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 57fd63eff..8dab7c110 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -58,7 +58,7 @@ module NotificationsHelper def comment_notification_body(event) comment = event.eventable - "#{strip_tags(comment.body_html).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body_html).truncate(200)}" + "#{strip_tags(comment.body.to_s).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body.to_s).truncate(200)}" end def card_notification_title(card) diff --git a/app/helpers/rich_textarea_helper.rb b/app/helpers/rich_textarea_helper.rb new file mode 100644 index 000000000..c6651ac2e --- /dev/null +++ b/app/helpers/rich_textarea_helper.rb @@ -0,0 +1,29 @@ +module RichTextareaHelper + def rich_textarea_toolbar(id = "editor-toolbar") + content_tag("lexical-toolbar", id: id) do + safe_join([ + # Inline formatting + rich_textarea_toolbar_button("Bold", "bold"), + rich_textarea_toolbar_button("Italic", "italic"), + rich_textarea_toolbar_button("Link", "link"), + + # Block-level formatting + rich_textarea_toolbar_button("Heading", "formatElement", data: { payload: "h2" }), + rich_textarea_toolbar_button("Subheading", "formatElement", data: { payload: "h3" }), + rich_textarea_toolbar_button("Code Block", "insertCodeBlock"), + + # Lists + rich_textarea_toolbar_button("• Bullet List", "insertUnorderedList"), + rich_textarea_toolbar_button("1. Numbered List", "insertOrderedList"), + + # Attachments + rich_textarea_toolbar_button("Upload Attachment", "uploadAttachments") + ]) + end + end + + def rich_textarea_toolbar_button(label, command, data: {}, **properties) + data[:command] = command + content_tag(:button, label, type: "button", data: data, **properties) + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index c2917aa3e..df3dda654 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -3,3 +3,6 @@ import "@hotwired/turbo-rails" import "initializers" import "controllers" import "house" + +import "actiontext-lexical" +import "@rails/actiontext" diff --git a/app/javascript/controllers/local_save_controller.js b/app/javascript/controllers/local_save_controller.js index 5d9b89a72..26da25f43 100644 --- a/app/javascript/controllers/local_save_controller.js +++ b/app/javascript/controllers/local_save_controller.js @@ -30,9 +30,11 @@ export default class extends Controller { async restoreContent() { await nextFrame() - const savedContent = localStorage.getItem(this.keyValue) + let savedContent = localStorage.getItem(this.keyValue) if (savedContent) { + savedContent = `
${savedContent}
` // temporary for old markdown saves + console.debug("Es", savedContent); this.inputTarget.value = savedContent this.#triggerChangeEvent(savedContent) } diff --git a/app/models/card.rb b/app/models/card.rb index 8707b2bf2..69588ea88 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -9,7 +9,7 @@ class Card < ApplicationRecord has_many :comments, dependent: :destroy has_one_attached :image, dependent: :purge_later - has_markdown :description + has_rich_text :description before_save :set_default_title, if: :published? diff --git a/app/models/comment.rb b/app/models/comment.rb index ebb1229a4..f62f457a0 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -5,7 +5,7 @@ class Comment < ApplicationRecord belongs_to :creator, class_name: "User", default: -> { Current.user } has_many :reactions, dependent: :delete_all - has_markdown :body + has_rich_text :body searchable_by :body_plain_text, using: :comments_search_index, as: :body scope :chronologically, -> { order created_at: :asc, id: :desc } @@ -19,6 +19,10 @@ class Comment < ApplicationRecord end private + def body_plain_text + body.to_plain_text + end + def watch_card_by_creator card.watch_by creator end diff --git a/app/models/concerns/mentions.rb b/app/models/concerns/mentions.rb index 08e0362cb..e8efb550c 100644 --- a/app/models/concerns/mentions.rb +++ b/app/models/concerns/mentions.rb @@ -14,7 +14,7 @@ module Mentions end def mentionable_content - markdown_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ") + rich_text_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ") end private @@ -32,12 +32,12 @@ module Mentions collection.users end - def markdown_associations - self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::Markdown } + def rich_text_associations + self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::RichText } end def mentionable_content_changed? - markdown_associations.any? { send(it.name).content_previously_changed? } + rich_text_associations.any? { send(it.name).body_previously_changed? } end def create_mentions_later diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb new file mode 100644 index 000000000..49ba357dd --- /dev/null +++ b/app/views/active_storage/blobs/_blob.html.erb @@ -0,0 +1,14 @@ +
attachment--<%= blob.filename.extension %>"> + <% if blob.representable? %> + <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %> + <% end %> + +
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <%= number_to_human_size blob.byte_size %> + <% end %> +
+
diff --git a/app/views/cards/comments/_comment.html.erb b/app/views/cards/comments/_comment.html.erb index 9c41c81d6..02787e49b 100644 --- a/app/views/cards/comments/_comment.html.erb +++ b/app/views/cards/comments/_comment.html.erb @@ -29,7 +29,7 @@
- <%= sanitize comment.body_html %> + <%= comment.body %>
<%= render "cards/comments/reactions/reactions", comment: comment %> diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb index c4645fe86..0a41b43cf 100644 --- a/app/views/cards/comments/_new.html.erb +++ b/app/views/cards/comments/_new.html.erb @@ -9,7 +9,8 @@ data: { controller: "form paste local-save", local_save_key_value: "comment-#{card.id}", action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> - <%= form.markdown_area :body, class: "input comment__input", required: true, placeholder: new_comment_placeholder(card), + <%= rich_textarea_toolbar "editor-toolbar" %> + <%= form.rich_textarea :body, class: "input comment__input", toolbar: "editor-toolbar", required: true, placeholder: new_comment_placeholder(card), data: { local_save_target: "input", action: "house-md:change->local-save#save turbo:morph-element->local-save#restoreContent" } %> <%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %> Post this comment diff --git a/app/views/cards/comments/edit.html.erb b/app/views/cards/comments/edit.html.erb index 342afc590..7fa50ec44 100644 --- a/app/views/cards/comments/edit.html.erb +++ b/app/views/cards/comments/edit.html.erb @@ -8,7 +8,8 @@
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width", data: { controller: "form paste", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> - <%= form.markdown_area :body, class: "input comment__input", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %> + <%= rich_textarea_toolbar "editor-toolbar" %> + <%= form.rich_textarea :body, class: "input comment__input", toolbar: "editor-toolbar", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %>
<%= form.button class: "btn btn--reversed", type: :submit do %> Save changes diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index 4e4c2c9f5..ed3e8f489 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -5,7 +5,7 @@ <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>
- <%= sanitize card.description_html %> + <%= card.description %>
<% end %> @@ -18,7 +18,9 @@ data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
- <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + <%= rich_textarea_toolbar "editor-toolbar" %> + <%= form.rich_textarea :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + toolbar: "the-toolbar", placeholder: "Add some notes, context, pictures, or video about this…", data: { action: "house-md:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index 499563bb4..cd955a621 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -8,10 +8,13 @@ data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> <% end %> -
- <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", - placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> + + <%= rich_textarea_toolbar "editor-toolbar" %> + + <%= form.rich_textarea :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + toolbar: "editor-toolbar", + placeholder: "Add some notes, context, pictures, or video about this…", + data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
<%= form.button "Save changes", type: :submit, class: "btn" %> <%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %> diff --git a/app/views/events/event/_comment.html.erb b/app/views/events/event/_comment.html.erb index 22f4596e8..b437349bb 100644 --- a/app/views/events/event/_comment.html.erb +++ b/app/views/events/event/_comment.html.erb @@ -1,5 +1,5 @@ <%= render "events/event/layout", card: event.eventable.card, event: event do %> - <%= strip_tags(event&.eventable.body_html).truncate(200) -%> + <%= strip_tags(event&.eventable.body.to_s).truncate(200) -%> <% end %> diff --git a/app/views/layouts/action_text/contents/_content.html.erb b/app/views/layouts/action_text/contents/_content.html.erb new file mode 100644 index 000000000..0dec72775 --- /dev/null +++ b/app/views/layouts/action_text/contents/_content.html.erb @@ -0,0 +1,3 @@ +
+ <%= yield -%> +
diff --git a/config/importmap.rb b/config/importmap.rb index 65dc7629c..ead746d7e 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -11,3 +11,6 @@ pin_all_from "app/javascript/controllers", under: "controllers" pin_all_from "app/javascript/helpers", under: "helpers" pin_all_from "app/javascript/initializers", under: "initializers" pin "marked" # @15.0.11 +pin "actiontext-lexical" +pin "@rails/activestorage", to: "activestorage.esm.js" +pin "@rails/actiontext", to: "actiontext.esm.js" diff --git a/config/initializers/markdown.rb b/config/initializers/markdown.rb deleted file mode 100644 index 04d5c8e78..000000000 --- a/config/initializers/markdown.rb +++ /dev/null @@ -1,7 +0,0 @@ -ActiveSupport.on_load :action_text_markdown do - require "markdown_renderer" - require "redcarpet/render_strip" - - ActionText::Markdown.html_renderer = ->(content) { MarkdownRenderer.build.render(content) } - ActionText::Markdown.plain_text_renderer = Redcarpet::Markdown.new(Redcarpet::Render::StripDown) -end diff --git a/db/migrate/20250523084734_create_action_text_tables.action_text.rb b/db/migrate/20250523084734_create_action_text_tables.action_text.rb new file mode 100644 index 000000000..c1c0a9d8c --- /dev/null +++ b/db/migrate/20250523084734_create_action_text_tables.action_text.rb @@ -0,0 +1,26 @@ +# This migration comes from action_text (originally 20180528164100) +class CreateActionTextTables < ActiveRecord::Migration[6.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :action_text_rich_texts, id: primary_key_type do |t| + t.string :name, null: false + t.text :body, size: :long + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + + t.timestamps + + t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [ primary_key_type, foreign_key_type ] + end +end diff --git a/db/schema.rb b/db/schema.rb index 834b2bcc9..098674f3a 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.1].define(version: 2025_05_15_125505) do +ActiveRecord::Schema[8.1].define(version: 2025_05_23_084734) do create_table "accesses", force: :cascade do |t| t.integer "collection_id", null: false t.datetime "created_at", null: false @@ -39,6 +39,16 @@ ActiveRecord::Schema[8.1].define(version: 2025_05_15_125505) do t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record" end + create_table "action_text_rich_texts", force: :cascade do |t| + t.text "body" + t.datetime "created_at", null: false + t.string "name", null: false + t.bigint "record_id", null: false + t.string "record_type", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + create_table "active_storage_attachments", force: :cascade do |t| t.bigint "blob_id", null: false t.datetime "created_at", null: false diff --git a/db/schema_cache.yml b/db/schema_cache.yml index fbaacdc9c..6cd19a67c 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -1,7 +1,7 @@ --- !ruby/object:ActiveRecord::ConnectionAdapters::SchemaCache columns: accesses: - - &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: collection_id cast_type: &1 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer @@ -80,7 +80,7 @@ columns: default_function: collation: comment: - - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &25 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: user_id cast_type: *1 @@ -118,13 +118,13 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: content - cast_type: &14 !ruby/object:ActiveRecord::Type::Text + cast_type: &11 !ruby/object:ActiveRecord::Type::Text true: t false: f precision: scale: limit: - sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: TEXT type: :text limit: @@ -159,17 +159,30 @@ columns: collation: comment: - *9 - active_storage_attachments: + action_text_rich_texts: + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: body + cast_type: *11 + sql_type_metadata: *12 + 'null': true + default: + default_function: + collation: + comment: + - *5 + - *6 + - *10 - &16 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: - name: blob_id - cast_type: &11 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer + name: record_id + cast_type: &14 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer precision: scale: limit: max: 9223372036854775808 min: -9223372036854775808 - sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: bigint type: :integer limit: @@ -180,19 +193,23 @@ columns: default_function: collation: comment: - - *5 - - *6 - - *10 - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - *13 + - *9 + active_storage_attachments: + - &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: - name: record_id - cast_type: *11 - sql_type_metadata: *12 + name: blob_id + cast_type: *14 + sql_type_metadata: *15 'null': false default: default_function: collation: comment: + - *5 + - *6 + - *10 + - *16 - *13 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -208,8 +225,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: byte_size - cast_type: *11 - sql_type_metadata: *12 + cast_type: *14 + sql_type_metadata: *15 'null': false default: default_function: @@ -247,7 +264,7 @@ columns: collation: comment: - *6 - - &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: key cast_type: *7 @@ -260,8 +277,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: metadata - cast_type: *14 - sql_type_metadata: *15 + cast_type: *11 + sql_type_metadata: *12 'null': true default: default_function: @@ -278,7 +295,7 @@ columns: collation: comment: active_storage_variant_records: - - *16 + - *17 - *6 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -292,7 +309,7 @@ columns: comment: ar_internal_metadata: - *5 - - *17 + - *18 - *9 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -305,7 +322,7 @@ columns: collation: comment: assignees_filters: - - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: assignee_id cast_type: *1 @@ -315,7 +332,7 @@ columns: default_function: collation: comment: - - &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: filter_id cast_type: *1 @@ -326,7 +343,7 @@ columns: collation: comment: assigners_filters: - - &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: assigner_id cast_type: *1 @@ -336,11 +353,11 @@ columns: default_function: collation: comment: - - *18 - assignments: - *19 + assignments: - *20 - - &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - *21 + - &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: card_id cast_type: *1 @@ -368,14 +385,14 @@ columns: - *6 - *9 card_goldnesses: - - *21 + - *22 - *5 - *6 - *9 cards: - - *22 + - *23 - *5 - - &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: creator_id cast_type: *1 @@ -428,14 +445,14 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: status - cast_type: *14 - sql_type_metadata: *15 + cast_type: *11 + sql_type_metadata: *12 'null': false default: creating default_function: collation: comment: - - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: title cast_type: *7 @@ -461,7 +478,7 @@ columns: comment: - *9 closures: - - *21 + - *22 - *5 - *6 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column @@ -489,11 +506,11 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: all_access - cast_type: &31 !ruby/object:ActiveModel::Type::Boolean + cast_type: &32 !ruby/object:ActiveModel::Type::Boolean precision: scale: limit: - sql_type_metadata: &32 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &33 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: boolean type: :boolean limit: @@ -507,15 +524,15 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: auto_close_period - cast_type: *11 - sql_type_metadata: *12 + cast_type: *14 + sql_type_metadata: *15 'null': true default: default_function: collation: comment: - *5 - - *23 + - *24 - *6 - *10 - *9 @@ -530,18 +547,18 @@ columns: collation: comment: collections_filters: - - *22 - - *18 + - *23 + - *19 commands: - *5 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: data - cast_type: &25 !ruby/object:ActiveRecord::Type::Json + cast_type: &26 !ruby/object:ActiveRecord::Type::Json precision: scale: limit: - sql_type_metadata: &26 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: json type: :json limit: @@ -556,8 +573,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: line - cast_type: *14 - sql_type_metadata: *15 + cast_type: *11 + sql_type_metadata: *12 'null': true default: default_function: @@ -584,16 +601,16 @@ columns: collation: comment: - *9 - - *24 + - *25 comments: - - *21 + - *22 - *5 - - *23 + - *24 - *6 - *9 creators_filters: - - *23 - - *18 + - *24 + - *19 events: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -605,9 +622,9 @@ columns: default_function: collation: comment: - - *22 - - *5 - *23 + - *5 + - *24 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: eventable_id @@ -632,8 +649,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: particulars - cast_type: *25 - sql_type_metadata: *26 + cast_type: *26 + sql_type_metadata: *27 'null': true default: "{}" default_function: @@ -642,12 +659,12 @@ columns: - *9 filters: - *5 - - *23 + - *24 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: fields - cast_type: *25 - sql_type_metadata: *26 + cast_type: *26 + sql_type_metadata: *27 'null': false default: "{}" default_function: @@ -666,7 +683,7 @@ columns: comment: - *9 filters_stages: - - *18 + - *19 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: stage_id @@ -678,8 +695,8 @@ columns: collation: comment: filters_tags: - - *18 - - &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - *19 + - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: tag_id cast_type: *1 @@ -712,7 +729,7 @@ columns: default_function: collation: comment: - - &27 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: source_id cast_type: *1 @@ -722,7 +739,7 @@ columns: default_function: collation: comment: - - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: source_type cast_type: *7 @@ -756,16 +773,16 @@ columns: default_function: collation: comment: - - *27 - *28 + - *29 - *9 - - *24 + - *25 pins: - - *21 + - *22 - *5 - *6 - *9 - - *24 + - *25 reactions: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -882,24 +899,24 @@ columns: default_function: collation: comment: - - *24 + - *25 taggings: - - *21 + - *22 - *5 - *6 - - *29 + - *30 - *9 tags: - *5 - *6 - - *30 + - *31 - *9 users: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: active - cast_type: *31 - sql_type_metadata: *32 + cast_type: *32 + sql_type_metadata: *33 'null': false default: true default_function: @@ -940,16 +957,16 @@ columns: comment: - *9 watches: - - *21 + - *22 - *5 - *6 - *9 - - *24 + - *25 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: watching - cast_type: *31 - sql_type_metadata: *32 + cast_type: *32 + sql_type_metadata: *33 'null': false default: true default_function: @@ -989,6 +1006,7 @@ primary_keys: accesses: id accounts: id action_text_markdowns: id + action_text_rich_texts: id active_storage_attachments: id active_storage_blobs: id active_storage_variant_records: id @@ -1027,6 +1045,7 @@ data_sources: accesses: true accounts: true action_text_markdowns: true + action_text_rich_texts: true active_storage_attachments: true active_storage_blobs: true active_storage_variant_records: true @@ -1131,6 +1150,25 @@ indexes: nulls_not_distinct: comment: valid: true + action_text_rich_texts: + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: action_text_rich_texts + name: index_action_text_rich_texts_uniqueness + unique: true + columns: + - record_type + - record_id + - name + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true active_storage_attachments: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: active_storage_attachments @@ -2161,4 +2199,4 @@ indexes: comment: valid: true workflows: [] -version: 20250515125505 +version: 20250523084734 diff --git a/lib/markdown_renderer.rb b/lib/markdown_renderer.rb deleted file mode 100644 index e90e86fb4..000000000 --- a/lib/markdown_renderer.rb +++ /dev/null @@ -1,81 +0,0 @@ -require "rouge/plugins/redcarpet" - -class MarkdownRenderer < Redcarpet::Render::HTML - include Rouge::Plugins::Redcarpet - - def self.build - Redcarpet::Markdown.new MarkdownRenderer.new(filter_html: false), - autolink: true, highlight: true, no_intra_emphasis: true, - fenced_code_blocks: true, lax_spacing: true, strikethrough: true, tables: true - end - - def initialize(...) - super - @id_counts = Hash.new 0 - end - - def header(text, header_level) - unique_id(text).then do |id| - <<~HTML.chomp - - #{text} - - HTML - end - end - - def image(url, title, alt_text) - <<~HTML.chomp - - #{alt_text} - - HTML - end - - def code_block(code, language) - block_code(code, language) # call Rouge Redcarpet plugin - end - - def link(url, title, content) - return video_tag(url, title) if video_url?(url) - - attributes = { href: url } - attributes[:title] = title if title - attributes["data-turbo-frame"] = "_top" - - "#{content}" - end - - def autolink(url, link_type) - attributes = { href: url } - attributes["data-turbo-frame"] = "_top" - - "#{url}" - end - - private - attr_reader :id_counts - - def html_attributes(attributes) - attributes.map { |key, value| %Q(#{key}="#{value}") }.join(" ") - end - - 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 - - def video_url?(url) - File.extname(url).downcase.match?(/\.(mp4|webm|ogg|mov|avi|mkv)/) - end - - def video_tag(url, title) - <<~HTML.chomp - - HTML - end -end diff --git a/lib/rails_ext/action_text_has_markdown.rb b/lib/rails_ext/action_text_has_markdown.rb deleted file mode 100644 index b792657d5..000000000 --- a/lib/rails_ext/action_text_has_markdown.rb +++ /dev/null @@ -1,47 +0,0 @@ -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}_html - #{name}.to_html - end - - def #{name}_plain_text - #{name}.to_plain_text - 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 deleted file mode 100644 index cd19fa001..000000000 --- a/lib/rails_ext/action_text_markdown.rb +++ /dev/null @@ -1,44 +0,0 @@ -module ActionText - class Markdown < Record - mattr_accessor :html_renderer - mattr_accessor :plain_text_renderer - - belongs_to :record, polymorphic: true, touch: true - - def to_html - to_unsafe_html.html_safe - end - - def to_unsafe_html - if html_renderer.respond_to? :call - html_renderer.call content - else - html_renderer.render content - end - end - - def to_plain_text - if plain_text_renderer.respond_to? :call - plain_text_renderer.call content - else - plain_text_renderer.render content - end - 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 deleted file mode 100644 index d662da7b2..000000000 --- a/lib/rails_ext/action_text_tag_helper.rb +++ /dev/null @@ -1,36 +0,0 @@ -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: 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 deleted file mode 100644 index d4d4be335..000000000 --- a/lib/rails_ext/active_storage_sluggable.rb +++ /dev/null @@ -1,32 +0,0 @@ -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.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/script/migrate-to-multi-tenant b/script/migrate-to-multi-tenant index 8d035f37e..79db5c0e0 100755 --- a/script/migrate-to-multi-tenant +++ b/script/migrate-to-multi-tenant @@ -214,9 +214,6 @@ class EnactTenanting assert_count Tagging, Tagging.count assert_count Watch, Watch.count - # we're only copying the markdown records that are still accessible. - assert_count ActionText::Markdown, Comment.count - assert_count Event, Event.count assert_count Notification, Notification.count @@ -228,22 +225,7 @@ class EnactTenanting end def update_action_text_urls - update_counter["ActionText::Markdown"] = 0 - TENANTS.each do |tenant| - tenanted_domain = sprintf(TENANTED_DOMAIN, tenant: tenant) - - ApplicationRecord.with_tenant(tenant) do - ActionText::Markdown.all.each do |markdown| - content = markdown.content - next unless content =~ %r{#{CURRENT_DOMAIN}/u/} - - content.gsub!(%r{#{CURRENT_DOMAIN}/u/}, "#{tenanted_domain}/u/") - - markdown.update_column :content, content - update_counter["ActionText::Markdown"] += 1 - end - end - end + raise "No more markdown" end def hardlink_active_storage diff --git a/script/reset_cards_ids.rb b/script/reset_cards_ids.rb index 19305b5f2..65c556283 100644 --- a/script/reset_cards_ids.rb +++ b/script/reset_cards_ids.rb @@ -100,9 +100,9 @@ ApplicationRecord.with_each_tenant do |tenant| end # Update action_text_markdowns table (polymorphic relationship) - ActionText::Markdown.where(record_type: "Card").find_each do |markdown| - if id_mapping[markdown.record_id] - markdown.update_column(:record_id, id_mapping[markdown.record_id]) + ActionText::RichText.where(record_type: "Card").find_each do |rich_text| + if id_mapping[rich_text.record_id] + rich_text.update_column(:record_id, id_mapping[rich_text.record_id]) end end diff --git a/script/reset_collections_ids.rb b/script/reset_collections_ids.rb index 142e9e2a9..cf1d84028 100644 --- a/script/reset_collections_ids.rb +++ b/script/reset_collections_ids.rb @@ -73,9 +73,9 @@ ApplicationRecord.with_each_tenant do |tenant| end # Update action_text_markdowns table (polymorphic relationship) - ActionText::Markdown.where(record_type: "Collection").find_each do |markdown| - if id_mapping[markdown.record_id] - markdown.update_column(:record_id, id_mapping[markdown.record_id]) + ActionText::RichText.where(record_type: "Collection").find_each do |rich_text| + if id_mapping[rich_text.record_id] + rich_text.update_column(:record_id, id_mapping[rich_text.record_id]) end end diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index 5cb3a6582..75ba45147 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -46,7 +46,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest assert_equal "moon.jpg", card.image.filename.to_s assert_equal [ tags(:mobile) ], card.tags - assert_equal "Something more in-depth", card.description_plain_text.strip + assert_equal "Something more in-depth", card.description.to_plain_text.strip end test "users can only see cards in collections they have access to" do diff --git a/test/fixtures/action_text/rich_texts.yml b/test/fixtures/action_text/rich_texts.yml new file mode 100644 index 000000000..8b371ea60 --- /dev/null +++ b/test/fixtures/action_text/rich_texts.yml @@ -0,0 +1,4 @@ +# one: +# record: name_of_fixture (ClassOfFixture) +# name: content +# body:

In a million stars!

From 0693617d8317742beac06807c743d22df3fcb8fc Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 23 May 2025 11:53:22 +0200 Subject: [PATCH 02/58] Some initial work to reuse styles with lexical --- app/assets/stylesheets/actiontext-lexical.css | 15 + app/assets/stylesheets/actiontext.css | 440 ------------------ ...down-content.css => rich-text-content.css} | 80 +++- app/views/cards/comments/_comment.html.erb | 2 +- app/views/cards/comments/_new.html.erb | 2 +- app/views/cards/comments/edit.html.erb | 2 +- app/views/cards/container/_title.html.erb | 4 +- app/views/cards/edit.html.erb | 2 +- 8 files changed, 100 insertions(+), 447 deletions(-) create mode 100644 app/assets/stylesheets/actiontext-lexical.css delete mode 100644 app/assets/stylesheets/actiontext.css rename app/assets/stylesheets/{markdown-content.css => rich-text-content.css} (54%) diff --git a/app/assets/stylesheets/actiontext-lexical.css b/app/assets/stylesheets/actiontext-lexical.css new file mode 100644 index 000000000..182f94f1c --- /dev/null +++ b/app/assets/stylesheets/actiontext-lexical.css @@ -0,0 +1,15 @@ +.lexical-editor__content { + padding: 1rem 0.5rem; +} + +lexical-editor { + .node--selected:has(img) { + img { + border: 2px solid blue; + } + } + + .node--selected:not(:has(img)) { + border: 2px solid blue; + } +} diff --git a/app/assets/stylesheets/actiontext.css b/app/assets/stylesheets/actiontext.css deleted file mode 100644 index 9b6bcb064..000000000 --- a/app/assets/stylesheets/actiontext.css +++ /dev/null @@ -1,440 +0,0 @@ -/* - * Default Trix editor styles. See Action Text overwrites below. -*/ - -trix-editor { - border: 1px solid #bbb; - border-radius: 3px; - margin: 0; - padding: 0.4em 0.6em; - min-height: 5em; - outline: none; } - -trix-toolbar * { - box-sizing: border-box; } - -trix-toolbar .trix-button-row { - display: flex; - flex-wrap: nowrap; - justify-content: space-between; - overflow-x: auto; } - -trix-toolbar .trix-button-group { - display: flex; - margin-bottom: 10px; - border: 1px solid #bbb; - border-top-color: #ccc; - border-bottom-color: #888; - border-radius: 3px; } - trix-toolbar .trix-button-group:not(:first-child) { - margin-left: 1.5vw; } - @media (max-width: 768px) { - trix-toolbar .trix-button-group:not(:first-child) { - margin-left: 0; } } - -trix-toolbar .trix-button-group-spacer { - flex-grow: 1; } - @media (max-width: 768px) { - trix-toolbar .trix-button-group-spacer { - display: none; } } - -trix-toolbar .trix-button { - position: relative; - float: left; - color: rgba(0, 0, 0, 0.6); - font-size: 0.75em; - font-weight: 600; - white-space: nowrap; - padding: 0 0.5em; - margin: 0; - outline: none; - border: none; - border-bottom: 1px solid #ddd; - border-radius: 0; - background: transparent; } - trix-toolbar .trix-button:not(:first-child) { - border-left: 1px solid #ccc; } - trix-toolbar .trix-button.trix-active { - background: #cbeefa; - color: black; } - trix-toolbar .trix-button:not(:disabled) { - cursor: pointer; } - trix-toolbar .trix-button:disabled { - color: rgba(0, 0, 0, 0.125); } - @media (max-width: 768px) { - trix-toolbar .trix-button { - letter-spacing: -0.01em; - padding: 0 0.3em; } } - -trix-toolbar .trix-button--icon { - font-size: inherit; - width: 2.6em; - height: 1.6em; - max-width: calc(0.8em + 4vw); - text-indent: -9999px; } - @media (max-width: 768px) { - trix-toolbar .trix-button--icon { - height: 2em; - max-width: calc(0.8em + 3.5vw); } } - trix-toolbar .trix-button--icon::before { - display: inline-block; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - opacity: 0.6; - content: ""; - background-position: center; - background-repeat: no-repeat; - background-size: contain; } - @media (max-width: 768px) { - trix-toolbar .trix-button--icon::before { - right: 6%; - left: 6%; } } - trix-toolbar .trix-button--icon.trix-active::before { - opacity: 1; } - trix-toolbar .trix-button--icon:disabled::before { - opacity: 0.125; } - -trix-toolbar .trix-button--icon-attach::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M10.5%2018V7.5c0-2.25%203-2.25%203%200V18c0%204.125-6%204.125-6%200V7.5c0-6.375%209-6.375%209%200V18%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-miterlimit%3D%2210%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E"); - top: 8%; - bottom: 4%; } - -trix-toolbar .trix-button--icon-bold::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M6.522%2019.242a.5.5%200%200%201-.5-.5V5.35a.5.5%200%200%201%20.5-.5h5.783c1.347%200%202.46.345%203.24.982.783.64%201.216%201.562%201.216%202.683%200%201.13-.587%202.129-1.476%202.71a.35.35%200%200%200%20.049.613c1.259.56%202.101%201.742%202.101%203.22%200%201.282-.483%202.334-1.363%203.063-.876.726-2.132%201.12-3.66%201.12h-5.89ZM9.27%207.347v3.362h1.97c.766%200%201.347-.17%201.733-.464.38-.291.587-.716.587-1.27%200-.53-.183-.928-.513-1.198-.334-.273-.838-.43-1.505-.43H9.27Zm0%205.606v3.791h2.389c.832%200%201.448-.177%201.853-.497.399-.315.614-.786.614-1.423%200-.62-.22-1.077-.63-1.385-.418-.313-1.053-.486-1.905-.486H9.27Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-italic::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M9%205h6.5v2h-2.23l-2.31%2010H13v2H6v-2h2.461l2.306-10H9V5Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-link::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M18.948%205.258a4.337%204.337%200%200%200-6.108%200L11.217%206.87a.993.993%200%200%200%200%201.41c.392.39%201.027.39%201.418%200l1.623-1.613a2.323%202.323%200%200%201%203.271%200%202.29%202.29%200%200%201%200%203.251l-2.393%202.38a3.021%203.021%200%200%201-4.255%200l-.05-.049a1.007%201.007%200%200%200-1.418%200%20.993.993%200%200%200%200%201.41l.05.049a5.036%205.036%200%200%200%207.091%200l2.394-2.38a4.275%204.275%200%200%200%200-6.072Zm-13.683%2013.6a4.337%204.337%200%200%200%206.108%200l1.262-1.255a.993.993%200%200%200%200-1.41%201.007%201.007%200%200%200-1.418%200L9.954%2017.45a2.323%202.323%200%200%201-3.27%200%202.29%202.29%200%200%201%200-3.251l2.344-2.331a2.579%202.579%200%200%201%203.631%200c.392.39%201.027.39%201.419%200a.993.993%200%200%200%200-1.41%204.593%204.593%200%200%200-6.468%200l-2.345%202.33a4.275%204.275%200%200%200%200%206.072Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-strike::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M6%2014.986c.088%202.647%202.246%204.258%205.635%204.258%203.496%200%205.713-1.728%205.713-4.463%200-.275-.02-.536-.062-.781h-3.461c.398.293.573.654.573%201.123%200%201.035-1.074%201.787-2.646%201.787-1.563%200-2.773-.762-2.91-1.924H6ZM6.432%2010h3.763c-.632-.314-.914-.715-.914-1.273%200-1.045.977-1.739%202.432-1.739%201.475%200%202.52.723%202.617%201.914h2.764c-.05-2.548-2.11-4.238-5.39-4.238-3.145%200-5.392%201.719-5.392%204.316%200%20.363.04.703.12%201.02ZM4%2011a1%201%200%201%200%200%202h15a1%201%200%201%200%200-2H4Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-quote::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M4.581%208.471c.44-.5%201.056-.834%201.758-.995C8.074%207.17%209.201%207.822%2010%208.752c1.354%201.578%201.33%203.555.394%205.277-.941%201.731-2.788%203.163-4.988%203.56a.622.622%200%200%201-.653-.317c-.113-.205-.121-.49.16-.764.294-.286.567-.566.791-.835.222-.266.413-.54.524-.815.113-.28.156-.597.026-.908-.128-.303-.39-.524-.72-.69a3.02%203.02%200%200%201-1.674-2.7c0-.905.283-1.59.72-2.088Zm9.419%200c.44-.5%201.055-.834%201.758-.995%201.734-.306%202.862.346%203.66%201.276%201.355%201.578%201.33%203.555.395%205.277-.941%201.731-2.789%203.163-4.988%203.56a.622.622%200%200%201-.653-.317c-.113-.205-.122-.49.16-.764.294-.286.567-.566.791-.835.222-.266.412-.54.523-.815.114-.28.157-.597.026-.908-.127-.303-.39-.524-.72-.69a3.02%203.02%200%200%201-1.672-2.701c0-.905.283-1.59.72-2.088Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-heading-1::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21.5%207.5v-3h-12v3H14v13h3v-13h4.5ZM9%2013.5h3.5v-3h-10v3H6v7h3v-7Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-code::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3.293%2011.293a1%201%200%200%200%200%201.414l4%204a1%201%200%201%200%201.414-1.414L5.414%2012l3.293-3.293a1%201%200%200%200-1.414-1.414l-4%204Zm13.414%205.414%204-4a1%201%200%200%200%200-1.414l-4-4a1%201%200%201%200-1.414%201.414L18.586%2012l-3.293%203.293a1%201%200%200%200%201.414%201.414Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-bullet-list::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%207.5a1.5%201.5%200%201%200%200-3%201.5%201.5%200%200%200%200%203ZM8%206a1%201%200%200%201%201-1h11a1%201%200%201%201%200%202H9a1%201%200%200%201-1-1Zm1%205a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm0%206a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm-2.5-5a1.5%201.5%200%201%201-3%200%201.5%201.5%200%200%201%203%200ZM5%2019.5a1.5%201.5%200%201%200%200-3%201.5%201.5%200%200%200%200%203Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-number-list::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3%204h2v4H4V5H3V4Zm5%202a1%201%200%200%201%201-1h11a1%201%200%201%201%200%202H9a1%201%200%200%201-1-1Zm1%205a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm0%206a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm-3.5-7H6v1l-1.5%202H6v1H3v-1l1.667-2H3v-1h2.5ZM3%2017v-1h3v4H3v-1h2v-.5H4v-1h1V17H3Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-undo::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3%2014a1%201%200%200%200%201%201h6a1%201%200%201%200%200-2H6.257c2.247-2.764%205.151-3.668%207.579-3.264%202.589.432%204.739%202.356%205.174%205.405a1%201%200%200%200%201.98-.283c-.564-3.95-3.415-6.526-6.825-7.095C11.084%207.25%207.63%208.377%205%2011.39V8a1%201%200%200%200-2%200v6Zm2-1Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-redo::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21%2014a1%201%200%200%201-1%201h-6a1%201%200%201%201%200-2h3.743c-2.247-2.764-5.151-3.668-7.579-3.264-2.589.432-4.739%202.356-5.174%205.405a1%201%200%200%201-1.98-.283c.564-3.95%203.415-6.526%206.826-7.095%203.08-.513%206.534.614%209.164%203.626V8a1%201%200%201%201%202%200v6Zm-2-1Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-decrease-nesting-level::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%206a1%201%200%200%201%201-1h12a1%201%200%201%201%200%202H6a1%201%200%200%201-1-1Zm4%205a1%201%200%201%200%200%202h9a1%201%200%201%200%200-2H9Zm-3%206a1%201%200%201%200%200%202h12a1%201%200%201%200%200-2H6Zm-3.707-5.707a1%201%200%200%200%200%201.414l2%202a1%201%200%201%200%201.414-1.414L4.414%2012l1.293-1.293a1%201%200%200%200-1.414-1.414l-2%202Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-button--icon-increase-nesting-level::before { - background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%206a1%201%200%200%201%201-1h12a1%201%200%201%201%200%202H6a1%201%200%200%201-1-1Zm4%205a1%201%200%201%200%200%202h9a1%201%200%201%200%200-2H9Zm-3%206a1%201%200%201%200%200%202h12a1%201%200%201%200%200-2H6Zm-2.293-2.293%202-2a1%201%200%200%200%200-1.414l-2-2a1%201%200%201%200-1.414%201.414L3.586%2012l-1.293%201.293a1%201%200%201%200%201.414%201.414Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } - -trix-toolbar .trix-dialogs { - position: relative; } - -trix-toolbar .trix-dialog { - position: absolute; - top: 0; - left: 0; - right: 0; - font-size: 0.75em; - padding: 15px 10px; - background: #fff; - box-shadow: 0 0.3em 1em #ccc; - border-top: 2px solid #888; - border-radius: 5px; - z-index: 5; } - -trix-toolbar .trix-input--dialog { - font-size: inherit; - font-weight: normal; - padding: 0.5em 0.8em; - margin: 0 10px 0 0; - border-radius: 3px; - border: 1px solid #bbb; - background-color: #fff; - box-shadow: none; - outline: none; - -webkit-appearance: none; - -moz-appearance: none; } - trix-toolbar .trix-input--dialog.validate:invalid { - box-shadow: #F00 0px 0px 1.5px 1px; } - -trix-toolbar .trix-button--dialog { - font-size: inherit; - padding: 0.5em; - border-bottom: none; } - -trix-toolbar .trix-dialog--link { - max-width: 600px; } - -trix-toolbar .trix-dialog__link-fields { - display: flex; - align-items: baseline; } - trix-toolbar .trix-dialog__link-fields .trix-input { - flex: 1; } - trix-toolbar .trix-dialog__link-fields .trix-button-group { - flex: 0 0 content; - margin: 0; } - -trix-editor [data-trix-mutable]:not(.attachment__caption-editor) { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; } - -trix-editor [data-trix-mutable]::-moz-selection, -trix-editor [data-trix-cursor-target]::-moz-selection, trix-editor [data-trix-mutable] ::-moz-selection { - background: none; } - -trix-editor [data-trix-mutable]::selection, -trix-editor [data-trix-cursor-target]::selection, trix-editor [data-trix-mutable] ::selection { - background: none; } - -trix-editor .attachment__caption-editor:focus[data-trix-mutable]::-moz-selection { - background: highlight; } - -trix-editor .attachment__caption-editor:focus[data-trix-mutable]::selection { - background: highlight; } - -trix-editor [data-trix-mutable].attachment.attachment--file { - box-shadow: 0 0 0 2px highlight; - border-color: transparent; } - -trix-editor [data-trix-mutable].attachment img { - box-shadow: 0 0 0 2px highlight; } - -trix-editor .attachment { - position: relative; } - trix-editor .attachment:hover { - cursor: default; } - -trix-editor .attachment--preview .attachment__caption:hover { - cursor: text; } - -trix-editor .attachment__progress { - position: absolute; - z-index: 1; - height: 20px; - top: calc(50% - 10px); - left: 5%; - width: 90%; - opacity: 0.9; - transition: opacity 200ms ease-in; } - trix-editor .attachment__progress[value="100"] { - opacity: 0; } - -trix-editor .attachment__caption-editor { - display: inline-block; - width: 100%; - margin: 0; - padding: 0; - font-size: inherit; - font-family: inherit; - line-height: inherit; - color: inherit; - text-align: center; - vertical-align: top; - border: none; - outline: none; - -webkit-appearance: none; - -moz-appearance: none; } - -trix-editor .attachment__toolbar { - position: absolute; - z-index: 1; - top: -0.9em; - left: 0; - width: 100%; - text-align: center; } - -trix-editor .trix-button-group { - display: inline-flex; } - -trix-editor .trix-button { - position: relative; - float: left; - color: #666; - white-space: nowrap; - font-size: 80%; - padding: 0 0.8em; - margin: 0; - outline: none; - border: none; - border-radius: 0; - background: transparent; } - trix-editor .trix-button:not(:first-child) { - border-left: 1px solid #ccc; } - trix-editor .trix-button.trix-active { - background: #cbeefa; } - trix-editor .trix-button:not(:disabled) { - cursor: pointer; } - -trix-editor .trix-button--remove { - text-indent: -9999px; - display: inline-block; - padding: 0; - outline: none; - width: 1.8em; - height: 1.8em; - line-height: 1.8em; - border-radius: 50%; - background-color: #fff; - border: 2px solid highlight; - box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.25); } - trix-editor .trix-button--remove::before { - display: inline-block; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - opacity: 0.7; - content: ""; - background-image: url("data:image/svg+xml,%3Csvg%20height%3D%2224%22%20width%3D%2224%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M19%206.41%2017.59%205%2012%2010.59%206.41%205%205%206.41%2010.59%2012%205%2017.59%206.41%2019%2012%2013.41%2017.59%2019%2019%2017.59%2013.41%2012z%22%2F%3E%3Cpath%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E"); - background-position: center; - background-repeat: no-repeat; - background-size: 90%; } - trix-editor .trix-button--remove:hover { - border-color: #333; } - trix-editor .trix-button--remove:hover::before { - opacity: 1; } - -trix-editor .attachment__metadata-container { - position: relative; } - -trix-editor .attachment__metadata { - position: absolute; - left: 50%; - top: 2em; - transform: translate(-50%, 0); - max-width: 90%; - padding: 0.1em 0.6em; - font-size: 0.8em; - color: #fff; - background-color: rgba(0, 0, 0, 0.7); - border-radius: 3px; } - trix-editor .attachment__metadata .attachment__name { - display: inline-block; - max-width: 100%; - vertical-align: bottom; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } - trix-editor .attachment__metadata .attachment__size { - margin-left: 0.2em; - white-space: nowrap; } - -.trix-content { - line-height: 1.5; - overflow-wrap: break-word; - word-break: break-word; } - .trix-content * { - box-sizing: border-box; - margin: 0; - padding: 0; } - .trix-content h1 { - font-size: 1.2em; - line-height: 1.2; } - .trix-content blockquote { - border: 0 solid #ccc; - border-left-width: 0.3em; - margin-left: 0.3em; - padding-left: 0.6em; } - .trix-content [dir=rtl] blockquote, - .trix-content blockquote[dir=rtl] { - border-width: 0; - border-right-width: 0.3em; - margin-right: 0.3em; - padding-right: 0.6em; } - .trix-content li { - margin-left: 1em; } - .trix-content [dir=rtl] li { - margin-right: 1em; } - .trix-content pre { - display: inline-block; - width: 100%; - vertical-align: top; - font-family: monospace; - font-size: 0.9em; - padding: 0.5em; - white-space: pre; - background-color: #eee; - overflow-x: auto; } - .trix-content img { - max-width: 100%; - height: auto; } - .trix-content .attachment { - display: inline-block; - position: relative; - max-width: 100%; } - .trix-content .attachment a { - color: inherit; - text-decoration: none; } - .trix-content .attachment a:hover, .trix-content .attachment a:visited:hover { - color: inherit; } - .trix-content .attachment__caption { - text-align: center; } - .trix-content .attachment__caption .attachment__name + .attachment__size::before { - content: ' \2022 '; } - .trix-content .attachment--preview { - width: 100%; - text-align: center; } - .trix-content .attachment--preview .attachment__caption { - color: #666; - font-size: 0.9em; - line-height: 1.2; } - .trix-content .attachment--file { - color: #333; - line-height: 1; - margin: 0 2px 2px 2px; - padding: 0.4em 1em; - border: 1px solid #bbb; - border-radius: 5px; } - .trix-content .attachment-gallery { - display: flex; - flex-wrap: wrap; - position: relative; } - .trix-content .attachment-gallery .attachment { - flex: 1 0 33%; - padding: 0 0.5em; - max-width: 33%; } - .trix-content .attachment-gallery.attachment-gallery--2 .attachment, .trix-content .attachment-gallery.attachment-gallery--4 .attachment { - flex-basis: 50%; - max-width: 50%; } - -/* - * We need to override trix.css’s image gallery styles to accommodate the - * element we wrap around attachments. Otherwise, - * images in galleries will be squished by the max-width: 33%; rule. -*/ -.trix-content .attachment-gallery > action-text-attachment, -.trix-content .attachment-gallery > .attachment { - flex: 1 0 33%; - padding: 0 0.5em; - max-width: 33%; -} - -.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment, -.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment, -.trix-content .attachment-gallery.attachment-gallery--4 > .attachment { - flex-basis: 50%; - max-width: 50%; -} - -.trix-content action-text-attachment .attachment { - padding: 0 !important; - max-width: 100% !important; -} diff --git a/app/assets/stylesheets/markdown-content.css b/app/assets/stylesheets/rich-text-content.css similarity index 54% rename from app/assets/stylesheets/markdown-content.css rename to app/assets/stylesheets/rich-text-content.css index 81589501b..b10aa1a8f 100644 --- a/app/assets/stylesheets/markdown-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -1,5 +1,5 @@ @layer components { - .markdown-content { + .rich-text-content { :where(h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6) { font-weight: 800; hyphens: auto; @@ -106,5 +106,83 @@ display: inline-block; } } + + figure { + display: inline-block; + position: relative; + block-size: auto; + max-inline-size: 100%; + margin-inline: 0; + width: 100%; + + img { + display: block; + max-width: 100%; + margin-inline: auto; + max-inline-size: 100%; + height: auto; + user-select: none; + block-size: auto; + } + } + + /* Code */ + code:not([data-language]) { + background-color: #f5f5f5; + border: 1px solid #e0e0e0; + border-radius: 4px; + } + + code[class*="language-"], + pre[class*="language-"] { + background: #f9f9fc; + color: #2d2d2d; + font-family: 'Fira Code', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 0.95rem; + line-height: 1.6; + direction: ltr; + text-align: left; + white-space: pre; + word-break: break-word; + tab-size: 2; + hyphens: none; + border-radius: 8px; + padding: 1em; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); + } + + /* Syntax token colors */ + .code-token__attr { + color: #b000b5; /* violet-ish for keywords and attributes */ + } + + .code-token__property { + color: #007acc; /* cool blue for numbers, booleans, constants, etc. */ + } + + .code-token__selector { + color: #22863a; /* green for strings, selectors, inserted, etc. */ + } + + .code-token__comment { + color: #90a4ae; /* soft gray-blue for comments */ + font-style: italic; + } + + .code-token__operator { + color: #d73a49; /* red for operators, URLs, etc. */ + } + + .code-token__function { + color: #005cc5; /* bold blue for function and class names */ + } + + .code-token__variable { + color: #e36209; /* orange for variables, important things */ + } + + .code-token__punctuation { + color: #607d8b; /* muted blue-gray for punctuation */ + } } } diff --git a/app/views/cards/comments/_comment.html.erb b/app/views/cards/comments/_comment.html.erb index 02787e49b..18141b1c4 100644 --- a/app/views/cards/comments/_comment.html.erb +++ b/app/views/cards/comments/_comment.html.erb @@ -28,7 +28,7 @@ <% end %>
-
+
<%= comment.body %>
diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb index 0a41b43cf..ec3fa2e8b 100644 --- a/app/views/cards/comments/_new.html.erb +++ b/app/views/cards/comments/_new.html.erb @@ -4,7 +4,7 @@
-
+
<%= form_with model: Comment.new, url: card_comments_path(card), class: "flex flex-column gap full-width", data: { controller: "form paste local-save", local_save_key_value: "comment-#{card.id}", diff --git a/app/views/cards/comments/edit.html.erb b/app/views/cards/comments/edit.html.erb index 7fa50ec44..4a28c1069 100644 --- a/app/views/cards/comments/edit.html.erb +++ b/app/views/cards/comments/edit.html.erb @@ -5,7 +5,7 @@
-
+
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width", data: { controller: "form paste", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> <%= rich_textarea_toolbar "editor-toolbar" %> diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index ed3e8f489..424741f5c 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -4,7 +4,7 @@

<%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>

-
+
<%= card.description %>
@@ -17,7 +17,7 @@ autofocus: card.title.blank?, data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %> -
+
<%= rich_textarea_toolbar "editor-toolbar" %> <%= form.rich_textarea :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", toolbar: "the-toolbar", diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index cd955a621..8e15452d7 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -1,5 +1,5 @@ <%= turbo_frame_tag @card, :edit do %> - <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "form paste" } do |form| %> + <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content rich-text-content", data: { controller: "form paste" } do |form| %>

<%= form.label :title, class: "flex flex-column align-center" do %> <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title", From 85f03b5bf0c186cebf133a3df80080a30f100591 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 23 May 2025 12:40:29 +0200 Subject: [PATCH 03/58] Use new default toolbar --- app/assets/stylesheets/actiontext-lexical.css | 45 +++++++++++++++++++ app/assets/stylesheets/rich-text-content.css | 9 +++- app/helpers/rich_textarea_helper.rb | 29 ------------ app/views/cards/comments/_new.html.erb | 3 +- app/views/cards/comments/edit.html.erb | 3 +- app/views/cards/container/_title.html.erb | 2 - app/views/cards/edit.html.erb | 3 -- 7 files changed, 55 insertions(+), 39 deletions(-) delete mode 100644 app/helpers/rich_textarea_helper.rb diff --git a/app/assets/stylesheets/actiontext-lexical.css b/app/assets/stylesheets/actiontext-lexical.css index 182f94f1c..0b2990209 100644 --- a/app/assets/stylesheets/actiontext-lexical.css +++ b/app/assets/stylesheets/actiontext-lexical.css @@ -13,3 +13,48 @@ lexical-editor { border: 2px solid blue; } } + +lexical-toolbar { + background-color: inherit; + border-block-end: 1px solid var(--color-ink-medium); + border-radius: 0; + color: currentColor; + display: inline-flex; + font-size: inherit; + max-inline-size: 100%; + overflow-x: auto; + 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(--color-ink-lighter); + } + + &:is(:active) { + background-color: var(--color-selected); + } + } +} diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index b10aa1a8f..86b1d18da 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -72,7 +72,10 @@ :is(blockquote, .quote) { font-style: italic; - margin: 0 3ch; + margin: 1ch 3ch; + border-inline-start: 0.25rem solid var(--color-ink-lighter); + margin-inline: 0; + padding: 0.5rem 1rem; :is(p) { hyphens: none; @@ -127,6 +130,10 @@ } /* Code */ + code[data-language] { + display: block; + } + code:not([data-language]) { background-color: #f5f5f5; border: 1px solid #e0e0e0; diff --git a/app/helpers/rich_textarea_helper.rb b/app/helpers/rich_textarea_helper.rb deleted file mode 100644 index c6651ac2e..000000000 --- a/app/helpers/rich_textarea_helper.rb +++ /dev/null @@ -1,29 +0,0 @@ -module RichTextareaHelper - def rich_textarea_toolbar(id = "editor-toolbar") - content_tag("lexical-toolbar", id: id) do - safe_join([ - # Inline formatting - rich_textarea_toolbar_button("Bold", "bold"), - rich_textarea_toolbar_button("Italic", "italic"), - rich_textarea_toolbar_button("Link", "link"), - - # Block-level formatting - rich_textarea_toolbar_button("Heading", "formatElement", data: { payload: "h2" }), - rich_textarea_toolbar_button("Subheading", "formatElement", data: { payload: "h3" }), - rich_textarea_toolbar_button("Code Block", "insertCodeBlock"), - - # Lists - rich_textarea_toolbar_button("• Bullet List", "insertUnorderedList"), - rich_textarea_toolbar_button("1. Numbered List", "insertOrderedList"), - - # Attachments - rich_textarea_toolbar_button("Upload Attachment", "uploadAttachments") - ]) - end - end - - def rich_textarea_toolbar_button(label, command, data: {}, **properties) - data[:command] = command - content_tag(:button, label, type: "button", data: data, **properties) - end -end diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb index ec3fa2e8b..a74f01712 100644 --- a/app/views/cards/comments/_new.html.erb +++ b/app/views/cards/comments/_new.html.erb @@ -9,8 +9,7 @@ data: { controller: "form paste local-save", local_save_key_value: "comment-#{card.id}", action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> - <%= rich_textarea_toolbar "editor-toolbar" %> - <%= form.rich_textarea :body, class: "input comment__input", toolbar: "editor-toolbar", required: true, placeholder: new_comment_placeholder(card), + <%= form.rich_textarea :body, class: "input comment__input", required: true, placeholder: new_comment_placeholder(card), data: { local_save_target: "input", action: "house-md:change->local-save#save turbo:morph-element->local-save#restoreContent" } %> <%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %> Post this comment diff --git a/app/views/cards/comments/edit.html.erb b/app/views/cards/comments/edit.html.erb index 4a28c1069..2a150347b 100644 --- a/app/views/cards/comments/edit.html.erb +++ b/app/views/cards/comments/edit.html.erb @@ -8,8 +8,7 @@
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width", data: { controller: "form paste", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> - <%= rich_textarea_toolbar "editor-toolbar" %> - <%= form.rich_textarea :body, class: "input comment__input", toolbar: "editor-toolbar", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %> + <%= form.rich_textarea :body, class: "input comment__input", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %>
<%= form.button class: "btn btn--reversed", type: :submit do %> Save changes diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index 424741f5c..837356fb3 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -18,9 +18,7 @@ data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>

- <%= rich_textarea_toolbar "editor-toolbar" %> <%= form.rich_textarea :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", - toolbar: "the-toolbar", placeholder: "Add some notes, context, pictures, or video about this…", data: { action: "house-md:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index 8e15452d7..01a4d6b3c 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -9,10 +9,7 @@ <% end %> - <%= rich_textarea_toolbar "editor-toolbar" %> - <%= form.rich_textarea :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", - toolbar: "editor-toolbar", placeholder: "Add some notes, context, pictures, or video about this…", data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
From 1c41b956a959cd241e2eeb82b9c0eef445cfd809 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 23 May 2025 13:25:07 +0200 Subject: [PATCH 04/58] Highlight at rendering time too --- Gemfile.lock | 14 --- app/assets/stylesheets/rich-text-content.css | 89 ++++++++++++++----- .../syntax_highlight_controller.js | 8 ++ app/views/cards/container/_title.html.erb | 2 +- 4 files changed, 74 insertions(+), 39 deletions(-) create mode 100644 app/javascript/controllers/syntax_highlight_controller.js diff --git a/Gemfile.lock b/Gemfile.lock index a0a44f206..a2e7c4a59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -132,11 +132,7 @@ PATH remote: /Users/jorge/Work/jorgemanrubia/actiontext-lexical specs: actiontext-lexical (0.1.0) - capybara - cuprite - image_processing rails (>= 8.0.2) - selenium-webdriver GEM remote: https://rubygems.org/ @@ -173,9 +169,6 @@ GEM bigdecimal rexml crass (1.0.6) - cuprite (0.17) - capybara (~> 3.0) - ferrum (~> 0.17.0) date (3.4.1) debug (1.10.0) irb (~> 1.10) @@ -198,12 +191,6 @@ GEM net-http (>= 0.5.0) faraday-retry (2.3.1) faraday (~> 2.0) - ferrum (0.17.1) - addressable (~> 2.5) - base64 (~> 0.2) - concurrent-ruby (~> 1.1) - webrick (~> 1.7) - websocket-driver (~> 0.7) ffi (1.17.2) ffi (1.17.2-arm64-darwin) ffi (1.17.2-x86_64-darwin) @@ -456,7 +443,6 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.9.1) websocket (1.2.11) websocket-driver (0.7.7) base64 diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 86b1d18da..517157dd0 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -132,6 +132,7 @@ /* Code */ code[data-language] { display: block; + white-space: pre; } code:not([data-language]) { @@ -159,37 +160,77 @@ } /* Syntax token colors */ - .code-token__attr { - color: #b000b5; /* violet-ish for keywords and attributes */ - } + code[data-language] { + /* Keywords and attributes */ + .code-token__attr, + .token.atrule, + .token.attr, + .token.keyword { + color: #d73a49; + } - .code-token__property { - color: #007acc; /* cool blue for numbers, booleans, constants, etc. */ - } + /* Constants, booleans, numbers, properties, tags */ + .code-token__property, + .token.boolean, + .token.constant, + .token.deleted, + .token.number, + .token.property, + .token.symbol, + .token.tag { + color: #005cc5; + } - .code-token__selector { - color: #22863a; /* green for strings, selectors, inserted, etc. */ - } + /* Strings, selectors, and built-in constructs */ + .code-token__selector, + .token.builtin, + .token.char, + .token.inserted, + .token.selector, + .token.string { + color: #032f62; + } - .code-token__comment { - color: #90a4ae; /* soft gray-blue for comments */ - font-style: italic; - } + /* Comments and meta information */ + .code-token__comment, + .token.cdata, + .token.comment, + .token.doctype, + .token.prolog { + color: #6a737d; + font-style: italic; + } - .code-token__operator { - color: #d73a49; /* red for operators, URLs, etc. */ - } + /* Operators and symbolic entities */ + .code-token__operator, + .token.entity, + .token.operator, + .token.url { + color: #d73a49; + } - .code-token__function { - color: #005cc5; /* bold blue for function and class names */ - } + /* Functions and class names */ + .code-token__function, + .token.class, + .token.class-name, + .token.function { + color: #6f42c1; + } - .code-token__variable { - color: #e36209; /* orange for variables, important things */ - } + /* Variables, regex, namespaces, important */ + .code-token__variable, + .token.important, + .token.namespace, + .token.regex, + .token.variable { + color: #e36209; + } - .code-token__punctuation { - color: #607d8b; /* muted blue-gray for punctuation */ + /* Punctuation */ + .code-token__punctuation, + .token.punctuation { + color: #24292e; + } } } } diff --git a/app/javascript/controllers/syntax_highlight_controller.js b/app/javascript/controllers/syntax_highlight_controller.js new file mode 100644 index 000000000..02b66a0f7 --- /dev/null +++ b/app/javascript/controllers/syntax_highlight_controller.js @@ -0,0 +1,8 @@ +import { Controller } from "@hotwired/stimulus" +import { highlightAll } from "actiontext-lexical" + +export default class extends Controller { + connect() { + highlightAll() + } +} diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index 837356fb3..4ab8a0dd2 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -4,7 +4,7 @@

<%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>

-
+
<%= card.description %>
From de87ff9c7011e16ec377d675e469c50f36ee77f1 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 23 May 2025 13:28:26 +0200 Subject: [PATCH 05/58] Refine syntax higlighting --- app/assets/stylesheets/rich-text-content.css | 2 +- app/views/cards/comments/_comment.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 517157dd0..57686f5a0 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -188,7 +188,7 @@ .token.inserted, .token.selector, .token.string { - color: #032f62; + color: #22863a; } /* Comments and meta information */ diff --git a/app/views/cards/comments/_comment.html.erb b/app/views/cards/comments/_comment.html.erb index 18141b1c4..feedc3667 100644 --- a/app/views/cards/comments/_comment.html.erb +++ b/app/views/cards/comments/_comment.html.erb @@ -28,7 +28,7 @@ <% end %>
-
+
<%= comment.body %>
From 7b6b81444b93ee7cd3a72a5dfc3e0dee486e7dca Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 23 May 2025 14:34:16 +0200 Subject: [PATCH 06/58] Minimal styles for non-image attachments --- app/assets/stylesheets/rich-text-content.css | 38 ++++++++++++++++ app/views/active_storage/blobs/_blob.html.erb | 44 ++++++++++++++----- .../blobs/web/_representation.html.erb | 23 ++++++++++ 3 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 app/views/active_storage/blobs/web/_representation.html.erb diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 57686f5a0..03cda4643 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -110,6 +110,8 @@ } } + /* Attachments */ + figure { display: inline-block; position: relative; @@ -129,6 +131,42 @@ } } + .attachment { + border: 1px solid #cdbbfa; + border-radius: 16px; + padding: 24px; + background-color: white; + box-shadow: 0 0 0 2px #e3d8fd; + font-family: sans-serif; + display: inline-block; + + &:not(:has(img,embed,video)) { + max-width: 25rem; + } + + embed,video { + max-width: 100%; + } + } + + .attachment__caption { + display: flex; + flex-direction: column; + gap: 4px; + } + + .attachment__name { + font-size: 24px; + font-weight: 700; + color: #1f1f1f; + } + + .attachment__size { + font-size: 16px; + font-weight: 600; + color: #888; + } + /* Code */ code[data-language] { display: block; diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb index 49ba357dd..1744b9e39 100644 --- a/app/views/active_storage/blobs/_blob.html.erb +++ b/app/views/active_storage/blobs/_blob.html.erb @@ -1,14 +1,34 @@ -
attachment--<%= blob.filename.extension %>"> - <% if blob.representable? %> - <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %> - <% end %> +<% if blob.representable? %> +
+ <%= render "active_storage/blobs/web/representation", blob: blob %> -
- <% if caption = blob.try(:caption) %> - <%= caption %> - <% else %> - <%= blob.filename %> +
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <% end %> <%= number_to_human_size blob.byte_size %> - <% end %> -
-
+ <%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__file-link", download: blob.filename, title: "Download #{blob.filename}" do %> + Download + <% end %> + +
+<% else %> + <%= link_to rails_blob_path(blob, disposition: :attachment), + class: "attachment attachment--file attachment--#{blob.filename.extension}", + download: blob.filename, + title: "Download #{blob.filename}" do + %> + <%= render "active_storage/blobs/web/representation", blob: blob %> + +
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <% end %> + <%= number_to_human_size blob.byte_size %> +
+ <% end %> +<% end %> diff --git a/app/views/active_storage/blobs/web/_representation.html.erb b/app/views/active_storage/blobs/web/_representation.html.erb new file mode 100644 index 000000000..ca497fd1d --- /dev/null +++ b/app/views/active_storage/blobs/web/_representation.html.erb @@ -0,0 +1,23 @@ +<% size = local_assigns[:in_gallery] ? [800, 600] : [1024, 768] %> + +<% if blob.video? %> + <%= tag.video \ + src: rails_blob_path(blob), + controls: true, + preload: :none, + width: "100%", height: "100%" %> +<% elsif blob.audio? %> + +<% elsif blob.content_type == "application/pdf" %> + +<% elsif blob.variable? %> + <%= image_tag url_for(blob.variant(loader: { n: -1 }, resize_to_limit: size)) %> +<% elsif blob.previewable? %> + <%= image_tag url_for(blob.preview(resize_to_limit: size)) %> +<% else %> +
+ <%= file_extension_for(blob) %> +
+<% end %> From 06f952525a707915442d335322a40a6b812cdaaa Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 23 May 2025 17:08:20 +0200 Subject: [PATCH 07/58] Point to the right branch --- Gemfile | 2 +- Gemfile.lock | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 85052847e..5901ff05b 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,7 @@ gem "geared_pagination", "~> 1.2" gem "rqrcode" gem "rouge" gem "jbuilder" -gem "actiontext-lexical", path: "/Users/jorge/Work/jorgemanrubia/actiontext-lexical" +gem "actiontext-lexical", github: "basecamp/actiontext-lexical", branch: "initial" gem "image_processing", "~> 1.14" # Telemetry and logging diff --git a/Gemfile.lock b/Gemfile.lock index a2e7c4a59..37cf3eef9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,11 @@ +GIT + remote: https://github.com/basecamp/actiontext-lexical.git + revision: 9d5b0f37ddbdc98382a6258edaffbf0c2a371464 + branch: initial + specs: + actiontext-lexical (0.1.0) + rails (>= 8.0.2) + GIT remote: https://github.com/basecamp/active_record-tenanted revision: 173048c2af29e9a723811e552bc59e3154e82e2a @@ -128,12 +136,6 @@ GIT thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) -PATH - remote: /Users/jorge/Work/jorgemanrubia/actiontext-lexical - specs: - actiontext-lexical (0.1.0) - rails (>= 8.0.2) - GEM remote: https://rubygems.org/ specs: From a6d8e89846fba4fed7cf3a1bc29df8f7b9d2455e Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 25 May 2025 05:20:31 +0200 Subject: [PATCH 08/58] Update to latest --- Gemfile.lock | 2 +- app/assets/stylesheets/actiontext-lexical.css | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 37cf3eef9..3313162ec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 9d5b0f37ddbdc98382a6258edaffbf0c2a371464 + revision: 04418d71527ae39294c57297c480fef7f3d2f5df branch: initial specs: actiontext-lexical (0.1.0) diff --git a/app/assets/stylesheets/actiontext-lexical.css b/app/assets/stylesheets/actiontext-lexical.css index 0b2990209..d94175e0b 100644 --- a/app/assets/stylesheets/actiontext-lexical.css +++ b/app/assets/stylesheets/actiontext-lexical.css @@ -22,8 +22,13 @@ lexical-toolbar { display: inline-flex; font-size: inherit; max-inline-size: 100%; - overflow-x: auto; padding: 0.2em; + position: relative; + overflow: visible; + + dialog { + position: absolute; + } button, label { aspect-ratio: 4/3; From 628b75d656a417ee06e3525f1063e68e0fc23b15 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 25 May 2025 05:58:06 +0200 Subject: [PATCH 09/58] update lexical --- .DS_Store | Bin 0 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Sun, 25 May 2025 06:01:08 +0200 Subject: [PATCH 10/58] Sleep --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3313162ec..a34264514 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 04418d71527ae39294c57297c480fef7f3d2f5df + revision: e7cba257219e367a0d7ee4200f0d1fa680b8741e branch: initial specs: actiontext-lexical (0.1.0) From 1b326e932b1577de2a3b651a097a92a5be3359e8 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 25 May 2025 06:23:37 +0200 Subject: [PATCH 11/58] Update to get D&D --- Gemfile.lock | 2 +- app/assets/stylesheets/rich-text-content.css | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a34264514..1f2f29dee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: e7cba257219e367a0d7ee4200f0d1fa680b8741e + revision: 00405fe8a75f6b395e345f1db70230bbd71535d0 branch: initial specs: actiontext-lexical (0.1.0) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 03cda4643..9608470a2 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -197,6 +197,12 @@ box-shadow: 0 1px 3px rgba(0,0,0,0.05); } + .lexical-editor--drag-over { + background-color: var(--color-ink-lighter); + border: 2px dashed var(--color-ink-medium); + border-radius: 4px; + } + /* Syntax token colors */ code[data-language] { /* Keywords and attributes */ From 925d4a0730831140a79ff05489050c2620a22f50 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 25 May 2025 06:36:32 +0200 Subject: [PATCH 12/58] Update to latest --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1f2f29dee..a5e5c466e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 00405fe8a75f6b395e345f1db70230bbd71535d0 + revision: e7180bc5dfc5d70beb1ba145b74ac9f13b2b6e9d branch: initial specs: actiontext-lexical (0.1.0) From 032267f91831c03e565731a62e84599b7a5807b1 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 25 May 2025 21:38:43 +0200 Subject: [PATCH 13/58] Update to latest --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a5e5c466e..af7a2f4f7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: e7180bc5dfc5d70beb1ba145b74ac9f13b2b6e9d + revision: 69ac06661e3553e44ecdb71a92b613aa4bd67e7f branch: initial specs: actiontext-lexical (0.1.0) From b8a810df8c8fa84d2633475519e9082c02570df8 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 25 May 2025 23:14:40 +0200 Subject: [PATCH 14/58] Script to convert markdowns --- Gemfile | 2 +- Gemfile.lock | 17 ++-- script/convert_markdowns_to_rich_text.rb | 116 +++++++++++++++++++++++ 3 files changed, 127 insertions(+), 8 deletions(-) create mode 100755 script/convert_markdowns_to_rich_text.rb diff --git a/Gemfile b/Gemfile index 5901ff05b..c0455d262 100644 --- a/Gemfile +++ b/Gemfile @@ -24,6 +24,7 @@ gem "thruster", require: false gem "bcrypt", "~> 3.1.7" gem "geared_pagination", "~> 1.2" gem "rqrcode" +gem "redcarpet" gem "rouge" gem "jbuilder" gem "actiontext-lexical", github: "basecamp/actiontext-lexical", branch: "initial" @@ -52,4 +53,3 @@ group :test do gem "webmock" gem "vcr" end - diff --git a/Gemfile.lock b/Gemfile.lock index af7a2f4f7..3a0a48e59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,15 +26,16 @@ GIT GIT remote: https://github.com/crmne/ruby_llm.git - revision: 8412d51e869f1ee8e5229831a9a0bb5e9b8f3847 + revision: 87a86f70cb64e23d5b76994edf8080618f07e071 specs: - ruby_llm (1.2.0) + ruby_llm (1.3.0rc1) base64 event_stream_parser (~> 1) - faraday (~> 2) - faraday-multipart (~> 1) - faraday-net_http (~> 3) - faraday-retry (~> 2) + faraday (>= 1.10.0) + faraday-multipart (>= 1) + faraday-net_http (>= 1) + faraday-retry (>= 1) + marcel (~> 1.0) zeitwerk (~> 2) GIT @@ -205,7 +206,7 @@ GEM addressable (>= 2.5.0) globalid (1.2.1) activesupport (>= 6.1) - hashdiff (1.1.2) + hashdiff (1.2.0) i18n (1.14.7) concurrent-ruby (~> 1.0) image_processing (1.14.0) @@ -327,6 +328,7 @@ GEM rdoc (6.14.0) erb psych (>= 4.0.0) + redcarpet (3.6.1) regexp_parser (2.10.0) reline (0.6.1) io-console (~> 0.5) @@ -479,6 +481,7 @@ DEPENDENCIES puma (>= 5.0) rails! rails_structured_logging! + redcarpet rouge rqrcode rubocop-rails-omakase diff --git a/script/convert_markdowns_to_rich_text.rb b/script/convert_markdowns_to_rich_text.rb new file mode 100755 index 000000000..29e71b31b --- /dev/null +++ b/script/convert_markdowns_to_rich_text.rb @@ -0,0 +1,116 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "redcarpet" +require "nokogiri" + +class ActionText::Markdown < ApplicationRecord + belongs_to :record, polymorphic: true +end + +class MarkdownToActionTextConverter + ATTACHMENT_URL_REGEX = %r{/u/(?[^\/\s\)]+)} + + def initialize(html) + @doc = Nokogiri::HTML::DocumentFragment.parse(html) + @attachments = [] + end + + def convert + process_images + process_links + [ @doc.to_html, @attachments ] + end + + private + + def process_images + @doc.css("img").each do |img| + src = img["src"].presence + if src && match = src.match(ATTACHMENT_URL_REGEX) + if (attachment = find_attachment(match[:slug])) + puts "REEMPLAZO #{build_attachment_node(attachment).to_html}" + img.replace(build_attachment_node(attachment)) + @attachments << attachment + end + end + end + end + + def process_links + @doc.css("a").each do |link| + href = link["href"].presence + + if href && match = href.match(ATTACHMENT_URL_REGEX) + if (attachment = find_attachment(match[:slug])) + link.replace(build_attachment_node(attachment)) + @attachments << attachment + end + end + end + end + + def build_attachment_node(attachment) + html = ActionText::Attachment.from_attachable(attachment).to_html + fragment = Nokogiri::HTML::DocumentFragment.parse(html) + + node = fragment.at_css("action-text-attachment") + node["url"] = Rails.application.routes.url_helpers.rails_blob_path(attachment.blob, only_path: true) + + fragment + end + + def find_attachment(slug) + ActiveStorage::Attachment.find_by(slug: slug) + end +end + +class RedcarpetRenderer + def self.render(markdown) + renderer = Redcarpet::Render::HTML.new + markdowner = Redcarpet::Markdown.new(renderer, + autolink: true, + tables: true, + fenced_code_blocks: true, + strikethrough: true, + superscript: true, + ) + markdowner.render(markdown.to_s) + end +end + +ApplicationRecord.with_each_tenant do |tenant| + puts "Processing tenant: #{tenant}" + + ActionText::Markdown.find_each do |markdown| + next unless markdown.record + + html = RedcarpetRenderer.render(markdown.content.to_s) + converter = MarkdownToActionTextConverter.new(html) + rich_text_html, attachments = converter.convert + + rich_text = ActionText::RichText.create!( + name: markdown.name, + record: markdown.record, + body: rich_text_html + ) + + attachments.each do |attachment| + attachment.update!(record: rich_text) + end + + puts "✓ Created rich text for #{markdown.record_type}##{markdown.record_id} (#{markdown.name})" + rescue => e + warn "✗ Failed to process markdown ##{markdown.id}: #{e.class} - #{e.message}" + end +end + +# create_table "action_text_markdowns", force: :cascade do |t| +# t.text "content", default: "", null: false +# t.datetime "created_at", null: false +# t.string "name", null: false +# t.integer "record_id", null: false +# t.string "record_type", null: false +# t.datetime "updated_at", null: false +# t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record" +# end From 3c0686fb71bf31d9b1a5cc446e069f5733253046 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 26 May 2025 17:53:47 +0200 Subject: [PATCH 15/58] update lexical --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3a0a48e59..1e9891a8e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 69ac06661e3553e44ecdb71a92b613aa4bd67e7f + revision: 1d486d271d1f962099eee7b82560a309bcb7ea03 branch: initial specs: actiontext-lexical (0.1.0) From 62c01627ec318cbf3b92f536b26299650a22d150 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 26 May 2025 14:56:33 -0500 Subject: [PATCH 16/58] First stab at styles --- app/assets/stylesheets/_global.css | 11 + app/assets/stylesheets/actiontext-lexical.css | 66 +++- app/assets/stylesheets/cards.css | 7 + app/assets/stylesheets/comments.css | 23 +- app/assets/stylesheets/rich-text-content.css | 349 +++++++----------- app/views/cards/comments/_new.html.erb | 2 +- app/views/cards/comments/edit.html.erb | 2 +- app/views/cards/container/_title.html.erb | 2 +- app/views/cards/edit.html.erb | 4 +- 9 files changed, 212 insertions(+), 254 deletions(-) diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index aad4508fc..5b2a966af 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -177,6 +177,17 @@ --color-card-7: oklch(var(--lch-purple-medium)); --color-card-8: oklch(var(--lch-pink-medium)); + /* Colors: Syntax highlighting */ + --color-code-token__att: oklch(var(--lch-blue-dark)); + --color-code-token__comment: oklch(var(--lch-ink-medium)); + --color-code-token__function: oklch(var(--lch-purple-dark)); + --color-code-token__operator: oklch(var(--lch-red-dark)); + --color-code-token__property: oklch(var(--lch-purple-dark)); + --color-code-token__punctuation: oklch(var(--lch-ink-dark)); + --color-code-token__selector: oklch(var(--lch-green-dark)); + --color-code-token__variable: oklch(var(--lch-red-dark)); + + @media (prefers-color-scheme: dark) { --lch-canvas: 20% 0.0195 232.58; --lch-ink-inverted: var(--lch-black); diff --git a/app/assets/stylesheets/actiontext-lexical.css b/app/assets/stylesheets/actiontext-lexical.css index d94175e0b..a3850e2e2 100644 --- a/app/assets/stylesheets/actiontext-lexical.css +++ b/app/assets/stylesheets/actiontext-lexical.css @@ -1,42 +1,73 @@ -.lexical-editor__content { - padding: 1rem 0.5rem; -} - lexical-editor { - .node--selected:has(img) { - img { - border: 2px solid blue; + .node--selected { + &:not(:has(img)) { + box-shadow: 0 0 0 var(--hover-size) var(--hover-color); + } + + &:has(img) { + img { + box-shadow: 0 0 0 var(--hover-size) var(--hover-color); + } } } +} - .node--selected:not(:has(img)) { - border: 2px solid blue; +.lexical-editor__content { + --outline-size: max(2px, 0.08em); + + margin: var(--block-space-half) 0; + min-block-size: calc(7lh + var(--block-space)); + outline: 0; + padding: var(--block-space-half) var(--inline-space); + + * { + &:first-child { + margin-block-start: 0; + } + + &:last-child { + margin-block-end: 0; + } } } +.lexical-editor--drag-over { + background-color: var(--color-selected); + border-radius: 4px; + outline: 2px dashed var(--color-selected-dark); +} + +#link-dialog { + background-color: var(--color-canvas); + border: 1px solid var(--color-ink-lighter); + border-radius: 0.5em; + box-shadow: var(--shadow); + color: var(--color-ink); + padding: var(--block-space-half) var(--inline-space); +} + lexical-toolbar { background-color: inherit; - border-block-end: 1px solid var(--color-ink-medium); - border-radius: 0; + border-block-end: 1px solid var(--color-ink-light); color: currentColor; - display: inline-flex; + display: flex; font-size: inherit; + margin: 0; max-inline-size: 100%; - padding: 0.2em; + padding: 0.2em 0; position: relative; - overflow: visible; dialog { position: absolute; } - button, label { - aspect-ratio: 4/3; + button { + aspect-ratio: 4/3.5; appearance: none; background-color: transparent; block-size: 2em; border: none; - border-radius: 0.5em; + border-radius: 0.2em; color: currentColor; cursor: pointer; display: grid; @@ -56,6 +87,7 @@ lexical-toolbar { &:is(:focus, :hover) { background-color: var(--color-ink-lighter); + box-shadow: none; } &:is(:active) { diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 4e3c237f7..d13b60498 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -150,6 +150,13 @@ } } + .card-field__description { + lexical-toolbar { + border-block-start: 1px solid var(--color-ink-light); + margin-block-start: var(--block-margin) !important; + } + } + .card__content { flex: 2 1 auto; max-inline-size: 100%; diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index 8f9b26172..f2babf69b 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -1,7 +1,7 @@ @layer components { .comments { --avatar-size: 2.33em; - --comment-padding-block: var(--block-space); + --comment-padding-block: var(--block-space-half); --comment-padding-inline: var(--inline-space-double); --comment-max: 70ch; --reaction-size: 2rem; @@ -37,13 +37,15 @@ } .comment__author { + margin-inline-end: calc(var(--comment-padding-inline) * -0.5); + .btn { font-weight: inherit; } } .comment__avatar { - margin: calc(var(--comment-padding-block) * 0.5) calc(var(--comment-padding-inline) * -0.75); + margin: calc(var(--comment-padding-block) * 0.75) calc(var(--comment-padding-inline) * -0.75); z-index: 0; .comment--mine_ & { @@ -52,7 +54,7 @@ } .comment__body { - p { + * { &:first-child { margin-block-start: 0; } @@ -71,6 +73,8 @@ border-radius: 0.2em; padding: var(--comment-padding-block) + var(--comment-padding-inline) + calc(var(--comment-padding-block) * 1.5) var(--comment-padding-inline); word-wrap: break-word; } @@ -105,17 +109,4 @@ inline-size: calc(var(--comment-padding-inline) * 0.75); } } - - .comment__input { - --input-border-size: 0; - --input-padding: 0; - - min-block-size: calc(9lh + (2 * var(--comment-padding-block))); - min-inline-size: 100%; - - @supports (field-sizing: content) { - field-sizing: content; - min-block-size: calc(9lh + (2 * var(--comment-padding-block))); - } - } } diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 9608470a2..e6664e2af 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -1,107 +1,49 @@ @layer components { .rich-text-content { - :where(h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6) { + --block-margin: 0.65lh; + + :where(h1, h2, h3, h4, h5, h6) { + display: block; font-weight: 800; hyphens: auto; letter-spacing: -0.02ch; line-height: 1.1; - margin-block: 0; + margin-block: var(--block-margin); overflow-wrap: break-word; text-wrap: balance; } - :where(h1, h2, h3, h4, h5, h6) { display: block; } - :where(h1, h2, h3, h4, h5, h6) { margin-block: 0.65em; } + :where(h1) { font-size: 2em; } + :where(h2) { font-size: 1.5em; } + :where(h3) { font-size: 1.17em; } + :where(h4) { font-size: 1em; } + :where(h5) { font-size: 0.83em; } + :where(h6) { font-size: 0.67em; } - :is(h1, .h1) { font-size: 2em; } - :is(h2, .h2) { font-size: 1.5em; } - :is(h3, .h3) { font-size: 1.17em; } - :is(h4, .h4) { font-size: 1em; } - :is(h5, .h5) { font-size: 0.83em; } - :is(h6, .h6) { font-size: 0.67em; } - - :where(p, ul, ol, dl, blockquote, pre, figure, table, hr) { - margin-block: 0.65lh; + :where(p, ul, ol, dl, blockquote, figure) { + margin-block: var(--block-margin); overflow-wrap: break-word; text-wrap: pretty; } - :where(hr) { - border-color: var(--color-ink-medium); - border-style: var(--border-style, solid) none none; - margin: 2lh auto; - } - - :where(strong) { + :where(b, strong) { font-weight: 700; } - :is(pre, code, .pre, .code) { - background-color: var(--color-ink-lightest); - border: 1px solid var(--color-ink-lighter); - border-radius: 0.3em; - font-family: var(--font-mono); - font-size: 0.85em; - } - - :is(pre) { - overflow-x: auto; - /* text-wrap: nowrap; */ - } - - :where(code) { - padding: 0.1em 0.3em; - } - - :is(pre, .pre) { - border-radius: 0.5em; - padding: 0.5lh 2ch; - - :is(code, .code) { - background-color: transparent; - border: 0; - font-size: 1em; - padding: 0; - } - } - - :where(p) { + :where(p, blockquote) { hyphens: auto; letter-spacing: -0.005ch; } - :is(blockquote, .quote) { + :where(blockquote) { + border-inline-start: 0.25em solid var(--color-ink-lighter); font-style: italic; - margin: 1ch 3ch; - border-inline-start: 0.25rem solid var(--color-ink-lighter); - margin-inline: 0; - padding: 0.5rem 1rem; - - :is(p) { - hyphens: none; - } - } - - :where(table, th, td) { - border: 1px solid var(--color-ink-medium); - } - - :where(table) { - border-collapse: collapse; - margin: 1lh 0; - } - - :where(th, td) { - padding: 0.2lh 1ch; - text-align: start; - } - - :where(th) { - border-block-end-width: 3px; - font-weight: 700; + margin: var(--block-margin) 0; + padding: 0.5lh 2ch; } :where(img, video, embed, object) { + max-inline-size: 25rem; max-block-size: 32rem; /* Links should hug media contained within */ @@ -111,169 +53,144 @@ } /* Attachments */ - - figure { - display: inline-block; - position: relative; + :where(figure) { block-size: auto; - max-inline-size: 100%; + display: inline-block; + inline-size: 100%; + position: relative; margin-inline: 0; - width: 100%; + max-inline-size: 100%; - img { + :where(img) { + block-size: auto; display: block; - max-width: 100%; margin-inline: auto; max-inline-size: 100%; - height: auto; user-select: none; - block-size: auto; - } - } - - .attachment { - border: 1px solid #cdbbfa; - border-radius: 16px; - padding: 24px; - background-color: white; - box-shadow: 0 0 0 2px #e3d8fd; - font-family: sans-serif; - display: inline-block; - - &:not(:has(img,embed,video)) { - max-width: 25rem; } - embed,video { - max-width: 100%; + :where(figcaption) { + color: var(--color-ink-dark); + display: flex; + font-size: var(--text-small); + flex-wrap: wrap; + line-height: 1.3; + gap: 0.5ch; + justify-content: center; + margin: var(--block-space-half) var(--inline-space); + text-align: center; } - } - .attachment__caption { - display: flex; - flex-direction: column; - gap: 4px; - } + :where(a) { + --hover-size: 0; - .attachment__name { - font-size: 24px; - font-weight: 700; - color: #1f1f1f; + color: var(--color-link); + text-decoration: underline; + } } .attachment__size { - font-size: 16px; - font-weight: 600; - color: #888; + &:before { + content: "· "; + } + + &:after { + content: " ·"; + } } + + :where(code, pre) { + background-color: var(--color-canvas); + border: 1px solid var(--color-ink-lighter); + border-radius: 0.3em; + color: var(--color-ink); + font-family: var(--font-mono); + font-size: 0.85em; + font-weight: 500; + margin-block: var(--block-margin); + padding: 0.1em 0.3em; - /* Code */ - code[data-language] { - display: block; - white-space: pre; - } + &[data-language] { + border-radius: 0.5em; + display: block; + hyphens: none; + overflow-x: auto; + padding: 0.5lh 2ch; + tab-size: 2; + text-wrap: nowrap; + white-space: pre; + word-break: break-word; - code:not([data-language]) { - background-color: #f5f5f5; - border: 1px solid #e0e0e0; - border-radius: 4px; - } + /* Keywords and attributes */ + .code-token__attr, + .token.atrule, + .token.attr, + .token.keyword { + color: var(--color-code-token__att); + } - code[class*="language-"], - pre[class*="language-"] { - background: #f9f9fc; - color: #2d2d2d; - font-family: 'Fira Code', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; - font-size: 0.95rem; - line-height: 1.6; - direction: ltr; - text-align: left; - white-space: pre; - word-break: break-word; - tab-size: 2; - hyphens: none; - border-radius: 8px; - padding: 1em; - box-shadow: 0 1px 3px rgba(0,0,0,0.05); - } + /* Constants, booleans, numbers, properties, tags */ + .code-token__property, + .token.boolean, + .token.constant, + .token.deleted, + .token.number, + .token.property, + .token.symbol, + .token.tag { + color: var(--color-code-token__property); + } - .lexical-editor--drag-over { - background-color: var(--color-ink-lighter); - border: 2px dashed var(--color-ink-medium); - border-radius: 4px; - } + /* Strings, selectors, and built-in constructs */ + .code-token__selector, + .token.builtin, + .token.char, + .token.inserted, + .token.selector, + .token.string { + color: var(--color-code-token__selector); + } - /* Syntax token colors */ - code[data-language] { - /* Keywords and attributes */ - .code-token__attr, - .token.atrule, - .token.attr, - .token.keyword { - color: #d73a49; - } + /* Comments and meta information */ + .code-token__comment, + .token.cdata, + .token.comment, + .token.doctype, + .token.prolog { + color: var(--color-code-token__comment); + font-style: italic; + } - /* Constants, booleans, numbers, properties, tags */ - .code-token__property, - .token.boolean, - .token.constant, - .token.deleted, - .token.number, - .token.property, - .token.symbol, - .token.tag { - color: #005cc5; - } + /* Operators and symbolic entities */ + .code-token__operator, + .token.entity, + .token.operator, + .token.url { + color: var(--color-code-token__operator); + } - /* Strings, selectors, and built-in constructs */ - .code-token__selector, - .token.builtin, - .token.char, - .token.inserted, - .token.selector, - .token.string { - color: #22863a; - } + /* Functions and class names */ + .code-token__function, + .token.class, + .token.class-name, + .token.function { + color: var(--color-code-token__function); + } - /* Comments and meta information */ - .code-token__comment, - .token.cdata, - .token.comment, - .token.doctype, - .token.prolog { - color: #6a737d; - font-style: italic; - } + /* Variables, regex, namespaces, important */ + .code-token__variable, + .token.important, + .token.namespace, + .token.regex, + .token.variable { + color: var(--color-code-token__variable); + } - /* Operators and symbolic entities */ - .code-token__operator, - .token.entity, - .token.operator, - .token.url { - color: #d73a49; - } - - /* Functions and class names */ - .code-token__function, - .token.class, - .token.class-name, - .token.function { - color: #6f42c1; - } - - /* Variables, regex, namespaces, important */ - .code-token__variable, - .token.important, - .token.namespace, - .token.regex, - .token.variable { - color: #e36209; - } - - /* Punctuation */ - .code-token__punctuation, - .token.punctuation { - color: #24292e; + /* Punctuation */ + .code-token__punctuation, + .token.punctuation { + color: var(--color-code-token__punctuation); + } } } } diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb index a74f01712..c65e36fd3 100644 --- a/app/views/cards/comments/_new.html.erb +++ b/app/views/cards/comments/_new.html.erb @@ -9,7 +9,7 @@ data: { controller: "form paste local-save", local_save_key_value: "comment-#{card.id}", action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> - <%= form.rich_textarea :body, class: "input comment__input", required: true, placeholder: new_comment_placeholder(card), + <%= form.rich_textarea :body, required: true, placeholder: new_comment_placeholder(card), data: { local_save_target: "input", action: "house-md:change->local-save#save turbo:morph-element->local-save#restoreContent" } %> <%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %> Post this comment diff --git a/app/views/cards/comments/edit.html.erb b/app/views/cards/comments/edit.html.erb index 2a150347b..2cd668298 100644 --- a/app/views/cards/comments/edit.html.erb +++ b/app/views/cards/comments/edit.html.erb @@ -8,7 +8,7 @@
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width", data: { controller: "form paste", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> - <%= form.rich_textarea :body, class: "input comment__input", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %> + <%= form.rich_textarea :body, required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %>
<%= form.button class: "btn btn--reversed", type: :submit do %> Save changes diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index 4ab8a0dd2..b0cfd7070 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -18,7 +18,7 @@ data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
- <%= form.rich_textarea :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + <%= form.rich_textarea :description, class: "card-field__description", placeholder: "Add some notes, context, pictures, or video about this…", data: { action: "house-md:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index 01a4d6b3c..1317c6641 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -1,5 +1,5 @@ <%= turbo_frame_tag @card, :edit do %> - <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content rich-text-content", data: { controller: "form paste" } do |form| %> + <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "form paste" } do |form| %>

<%= form.label :title, class: "flex flex-column align-center" do %> <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title", @@ -9,7 +9,7 @@ <% end %>

- <%= form.rich_textarea :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + <%= form.rich_textarea :description, class: "card-field__description rich-text-content", placeholder: "Add some notes, context, pictures, or video about this…", data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
From a1a16c4857873e7983c2822f75b99c6fbfe88446 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 26 May 2025 15:44:57 -0500 Subject: [PATCH 17/58] Style link dialog --- app/assets/stylesheets/actiontext-lexical.css | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/actiontext-lexical.css b/app/assets/stylesheets/actiontext-lexical.css index a3850e2e2..c2693a8e0 100644 --- a/app/assets/stylesheets/actiontext-lexical.css +++ b/app/assets/stylesheets/actiontext-lexical.css @@ -12,6 +12,25 @@ lexical-editor { } } +.lexical-dialog-actions { + display: flex; + font-size: var(--text-x-small); + flex: 1 1 0%; + gap: var(--inline-space-half); + margin-block-start: var(--block-space-half); + + .btn { + --radius: 0.3em; + + inline-size: 100%; + justify-content: center; + } + + span { + inline-size: 100%; + } +} + .lexical-editor__content { --outline-size: max(2px, 0.08em); @@ -37,15 +56,6 @@ lexical-editor { outline: 2px dashed var(--color-selected-dark); } -#link-dialog { - background-color: var(--color-canvas); - border: 1px solid var(--color-ink-lighter); - border-radius: 0.5em; - box-shadow: var(--shadow); - color: var(--color-ink); - padding: var(--block-space-half) var(--inline-space); -} - lexical-toolbar { background-color: inherit; border-block-end: 1px solid var(--color-ink-light); @@ -58,10 +68,16 @@ lexical-toolbar { position: relative; dialog { + background-color: var(--color-canvas); + border: 1px solid var(--color-ink-lighter); + border-radius: 0.5em; + box-shadow: var(--shadow); + color: var(--color-ink); + padding: var(--block-space) calc(var(--inline-space) * 1.5); position: absolute; } - button { + > button { aspect-ratio: 4/3.5; appearance: none; background-color: transparent; From 6166afba3649fb5b03210ffcfa1dc95abcfd4a20 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 26 May 2025 17:01:07 -0500 Subject: [PATCH 18/58] Adjust icons --- app/assets/stylesheets/actiontext-lexical.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/actiontext-lexical.css b/app/assets/stylesheets/actiontext-lexical.css index c2693a8e0..c0ab455a5 100644 --- a/app/assets/stylesheets/actiontext-lexical.css +++ b/app/assets/stylesheets/actiontext-lexical.css @@ -94,7 +94,7 @@ lexical-toolbar { svg { -webkit-touch-callout: none; - block-size: 0.9em; + block-size: 0.85em; fill: currentColor; grid-area: 1/1; inline-size: auto; From e2b3374971a0d6ee9e51ee2b5a772c6844520f93 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 26 May 2025 17:02:06 -0500 Subject: [PATCH 19/58] Update dependency --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1e9891a8e..d4fe9a493 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 1d486d271d1f962099eee7b82560a309bcb7ea03 + revision: 580f16e45e6d0a052c084cf1b42b3a73c06bc1c7 branch: initial specs: actiontext-lexical (0.1.0) From 7ef8e2f041a6044edd24bd92578baa9276a9b42f Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 26 May 2025 17:15:11 -0500 Subject: [PATCH 20/58] Align progress bar --- app/assets/stylesheets/rich-text-content.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index e6664e2af..548852e4e 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -87,6 +87,11 @@ color: var(--color-link); text-decoration: underline; } + + :where(progress) { + inline-size: 100%; + margin: auto; + } } .attachment__size { From c32c6af76520f410ec8a9bee79ae1e5bbc0ff979 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 26 May 2025 17:21:02 -0500 Subject: [PATCH 21/58] Style caption input --- Gemfile.lock | 2 +- app/assets/stylesheets/rich-text-content.css | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d4fe9a493..c1f23cb81 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 580f16e45e6d0a052c084cf1b42b3a73c06bc1c7 + revision: f2300462f0dd968fdb82f774a6646bc91dee8a5f branch: initial specs: actiontext-lexical (0.1.0) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 548852e4e..ea7be0776 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -79,6 +79,18 @@ justify-content: center; margin: var(--block-space-half) var(--inline-space); text-align: center; + + .input { + inline-size: 100%; + max-inline-size: 100%; + text-align: center; + + @supports (field-sizing: content) { + field-sizing: content; + inline-size: auto; + min-inline-size: 20ch; + } + } } :where(a) { From b2509ce80658e0e2bcb699f08c53f3ef0ed9ccb3 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 26 May 2025 17:28:08 -0500 Subject: [PATCH 22/58] Fix a couple of spacing wonks --- app/assets/stylesheets/rich-text-content.css | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index ea7be0776..568cd2350 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -115,7 +115,21 @@ content: " ·"; } } - + + /* Attachment junk get wrapped in a paragraph causing unwanted space */ + p:has(action-text-attachment) { + display: none; + + + * { + margin-block-start: 0; + } + } + + /* Avoid extra space due to empty paragraphs */ + p:empty { + display: none; + } + :where(code, pre) { background-color: var(--color-canvas); border: 1px solid var(--color-ink-lighter); From 4aa1cef99d6f1229176c08f146a5c7e37410cf5f Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 27 May 2025 12:30:44 +0200 Subject: [PATCH 23/58] Show previeable content in the editor Embedded PDF previewing isnt' reliable (not working in Safari with active storage). Instead let's align the editor with Active Storage previews and support pdf previews --- Dockerfile | 2 +- app/views/active_storage/blobs/web/_representation.html.erb | 2 -- app/views/layouts/application.html.erb | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index e0712a39a..64436b2bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ FROM base # Install packages needed for deployment RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libsqlite3-0 libvips && \ + apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Copy built artifacts: gems, application diff --git a/app/views/active_storage/blobs/web/_representation.html.erb b/app/views/active_storage/blobs/web/_representation.html.erb index ca497fd1d..40456e8c7 100644 --- a/app/views/active_storage/blobs/web/_representation.html.erb +++ b/app/views/active_storage/blobs/web/_representation.html.erb @@ -10,8 +10,6 @@ -<% elsif blob.content_type == "application/pdf" %> - <% elsif blob.variable? %> <%= image_tag url_for(blob.variant(loader: { n: -1 }, resize_to_limit: size)) %> <% elsif blob.previewable? %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 0e04cbca9..b667bfb47 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -3,7 +3,7 @@ <%= page_title_tag %> - + From 41acd15fa41fb56e4c7a18310076cdbbe5582faf Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 27 May 2025 12:31:46 +0200 Subject: [PATCH 24/58] Update lexical --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c1f23cb81..a74702f71 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: f2300462f0dd968fdb82f774a6646bc91dee8a5f + revision: 45e8eb695886a46460580b6ba535029167d8d8f1 branch: initial specs: actiontext-lexical (0.1.0) From 393205a132c99e1492beaa468a47a81752f129d6 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 27 May 2025 12:45:00 +0200 Subject: [PATCH 25/58] Update lexical --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a74702f71..6f97bbdc2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 45e8eb695886a46460580b6ba535029167d8d8f1 + revision: ba5b378b61ae67e87103702981e576277f16ffac branch: initial specs: actiontext-lexical (0.1.0) From 108f711d1f13239049505182d1ec4360ee753865 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 27 May 2025 12:55:09 +0200 Subject: [PATCH 26/58] Inline missing helper --- app/views/active_storage/blobs/web/_representation.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/active_storage/blobs/web/_representation.html.erb b/app/views/active_storage/blobs/web/_representation.html.erb index 40456e8c7..bbb912baa 100644 --- a/app/views/active_storage/blobs/web/_representation.html.erb +++ b/app/views/active_storage/blobs/web/_representation.html.erb @@ -16,6 +16,6 @@ <%= image_tag url_for(blob.preview(resize_to_limit: size)) %> <% else %>
- <%= file_extension_for(blob) %> + <%= blob.filename.extension&.downcase.presence || "unknown" %>
<% end %> From 7eaba8b835ef697cb0b482d877dcdfbae3d7758c Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 27 May 2025 13:30:09 +0200 Subject: [PATCH 27/58] Update --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6f97bbdc2..0c2035954 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: ba5b378b61ae67e87103702981e576277f16ffac + revision: 11ed6d4ff17025ca89c4743b058d5a989b10b45d branch: initial specs: actiontext-lexical (0.1.0) From bdbd556560df06c411be1b418be8344361f31e2e Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 27 May 2025 14:55:25 -0500 Subject: [PATCH 28/58] Center and allow natural width --- app/assets/stylesheets/rich-text-content.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 568cd2350..bc70854f2 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -43,7 +43,7 @@ } :where(img, video, embed, object) { - max-inline-size: 25rem; + margin-inline: auto; max-block-size: 32rem; /* Links should hug media contained within */ From fc0d0cd30056f806a8b60fe73f61d911eccb656a Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 27 May 2025 22:21:58 -0500 Subject: [PATCH 29/58] Style non-previewable attachment in editor and rendered --- Gemfile.lock | 2 +- app/assets/stylesheets/rich-text-content.css | 141 +++++++++++++----- app/views/active_storage/blobs/_blob.html.erb | 36 +++-- .../blobs/web/_representation.html.erb | 4 +- 4 files changed, 125 insertions(+), 58 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0c2035954..19fd6c728 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 11ed6d4ff17025ca89c4743b058d5a989b10b45d + revision: 916553ccdf1acfa9bbd86b8f1981e795a2bf9f87 branch: initial specs: actiontext-lexical (0.1.0) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index bc70854f2..3ca33efe2 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -53,7 +53,7 @@ } /* Attachments */ - :where(figure) { + .attachment { block-size: auto; display: inline-block; inline-size: 100%; @@ -61,7 +61,7 @@ margin-inline: 0; max-inline-size: 100%; - :where(img) { + &:is(figure) img { block-size: auto; display: block; margin-inline: auto; @@ -69,54 +69,114 @@ user-select: none; } - :where(figcaption) { - color: var(--color-ink-dark); - display: flex; - font-size: var(--text-small); - flex-wrap: wrap; - line-height: 1.3; - gap: 0.5ch; - justify-content: center; - margin: var(--block-space-half) var(--inline-space); - text-align: center; - - .input { - inline-size: 100%; - max-inline-size: 100%; - text-align: center; - - @supports (field-sizing: content) { - field-sizing: content; - inline-size: auto; - min-inline-size: 20ch; - } - } - } - - :where(a) { - --hover-size: 0; - - color: var(--color-link); - text-decoration: underline; - } - :where(progress) { inline-size: 100%; margin: auto; } } - .attachment__size { - &:before { - content: "· "; - } + .attachment__caption { + color: var(--color-ink-dark); + display: flex; + font-size: var(--text-small); + flex-wrap: wrap; + line-height: 1.3; + gap: 0.5ch; + justify-content: center; + margin: var(--block-space-half) var(--inline-space); + text-align: center; - &:after { - content: " ·"; + .input { + inline-size: 100%; + max-inline-size: 100%; + text-align: center; + + @supports (field-sizing: content) { + field-sizing: content; + inline-size: auto; + min-inline-size: 20ch; + } } } - /* Attachment junk get wrapped in a paragraph causing unwanted space */ + .attachment__figure { + --hover-size: 0; + } + + .attachment--file { + align-items: center; + display: inline-flex; + + .attachment__caption { + flex-direction: column; + flex-wrap: nowrap; + justify-content: flex-start; + text-align: start; + + strong { + color: tx(--color-ink); + } + } + } + + .attachment__icon { + aspect-ratio: 4/5; + background-color: var(--color-canvas); + block-size: calc(2.5lh + 0.5ch); + border: 2px solid var(--color-ink-lighter); + border-radius: 0.3em; + color: var(--color-ink); + display: inline-grid; + font-size: var(--text-small); + font-weight: 800; + inline-size: auto; + place-content: center; + + .attachment--psd &, + .attachment--ai &, + .attachment--eps &, + .attachment--indd &, + .attachment--svg & { + color: oklch(var(--lch-red-medium)); + background-color: oklch(var(--lch-red-lightest)); + border-color: oklch(var(--lch-red-light)); + } + + .attachment--css & + .attachment--htm &, + .attachment--html &, + .attachment--rb &, + .attachment--erb &, + .attachment--ts &, + .attachment--js & { + color: oklch(var(--lch-purple-medium)); + background-color: oklch(var(--lch-purple-lightest)); + border-color: oklch(var(--lch-purple-light)); + } + + .attachment--txt &, + .attachment--rtf &, + .attachment--md &, + .attachment--doc &, + .attachment--docx &, + .attachment--xls &, + .attachment--xlsx &, + .attachment--ppt &, + .attachment--pptx & { + color: oklch(var(--lch-blue-medium)); + background-color: oklch(var(--lch-blue-lightest)); + border-color: oklch(var(--lch-blue-light)); + } + } + + .attachment__link { + --hover-size: 0; + + color: var(--color-link); + text-decoration: underline; + } + + /* Attachment junk gets wrapped in a paragraph causing unwanted space */ p:has(action-text-attachment) { display: none; @@ -130,6 +190,7 @@ display: none; } + /* Code */ :where(code, pre) { background-color: var(--color-canvas); border: 1px solid var(--color-ink-lighter); diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb index 1744b9e39..6a35805e0 100644 --- a/app/views/active_storage/blobs/_blob.html.erb +++ b/app/views/active_storage/blobs/_blob.html.erb @@ -8,27 +8,35 @@ <% else %> <%= blob.filename %> <% end %> + · <%= number_to_human_size blob.byte_size %> - <%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__file-link", download: blob.filename, title: "Download #{blob.filename}" do %> + · + <%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__link", download: blob.filename, title: "Download #{blob.filename}" do %> Download <% end %> <% else %> - <%= link_to rails_blob_path(blob, disposition: :attachment), - class: "attachment attachment--file attachment--#{blob.filename.extension}", - download: blob.filename, - title: "Download #{blob.filename}" do - %> - <%= render "active_storage/blobs/web/representation", blob: blob %> +
+ <%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__figure", download: blob.filename, title: "Download #{blob.filename}" do %> + <%= render "active_storage/blobs/web/representation", blob: blob %> + <% end %>
- <% if caption = blob.try(:caption) %> - <%= caption %> - <% else %> - <%= blob.filename %> - <% end %> - <%= number_to_human_size blob.byte_size %> +
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <% end %> +
+
+ <%= number_to_human_size blob.byte_size %> + · + <%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__link", download: blob.filename, title: "Download #{blob.filename}" do %> + Download + <% end %> +
- <% end %> +
<% end %> diff --git a/app/views/active_storage/blobs/web/_representation.html.erb b/app/views/active_storage/blobs/web/_representation.html.erb index bbb912baa..cd7400668 100644 --- a/app/views/active_storage/blobs/web/_representation.html.erb +++ b/app/views/active_storage/blobs/web/_representation.html.erb @@ -15,7 +15,5 @@ <% elsif blob.previewable? %> <%= image_tag url_for(blob.preview(resize_to_limit: size)) %> <% else %> -
- <%= blob.filename.extension&.downcase.presence || "unknown" %> -
+ <%= blob.filename.extension&.downcase.presence || "unknown" %> <% end %> From f62c986a22eab052db8d6891c6c586ae20184e80 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 28 May 2025 09:29:13 -0500 Subject: [PATCH 30/58] Tighten blocks, add explicit spacing --- app/assets/stylesheets/rich-text-content.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 3ca33efe2..3fcd9c7f4 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -20,7 +20,7 @@ :where(h5) { font-size: 0.83em; } :where(h6) { font-size: 0.67em; } - :where(p, ul, ol, dl, blockquote, figure) { + :where(p, ul, ol, dl, blockquote, figure, .attachment) { margin-block: var(--block-margin); overflow-wrap: break-word; text-wrap: pretty; @@ -81,7 +81,7 @@ font-size: var(--text-small); flex-wrap: wrap; line-height: 1.3; - gap: 0.5ch; + column-gap: 0.5ch; justify-content: center; margin: var(--block-space-half) var(--inline-space); text-align: center; From b50a6e57b65a2912c67f4b1a4c25358fe28bfcaa Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 28 May 2025 09:29:44 -0500 Subject: [PATCH 31/58] Typo --- app/assets/stylesheets/rich-text-content.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 3fcd9c7f4..f0ffe2136 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -114,7 +114,7 @@ text-align: start; strong { - color: tx(--color-ink); + color: var(--color-ink); } } } From d767e158bb523263f9c40e983fecff0f87391999 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 28 May 2025 09:31:06 -0500 Subject: [PATCH 32/58] Update Lexical --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 19fd6c728..c83cc13ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 916553ccdf1acfa9bbd86b8f1981e795a2bf9f87 + revision: 00721e1ce57f5363862f0a4d53efbbbc534cdf78 branch: initial specs: actiontext-lexical (0.1.0) From 5e2c1c3501a07c7296c557dfa456df970c618fd8 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 28 May 2025 10:26:32 -0500 Subject: [PATCH 33/58] Add a little more pizzazz to file icons --- app/assets/stylesheets/rich-text-content.css | 42 ++++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index f0ffe2136..512824a51 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -123,23 +123,41 @@ aspect-ratio: 4/5; background-color: var(--color-canvas); block-size: calc(2.5lh + 0.5ch); - border: 2px solid var(--color-ink-lighter); + border: 2px solid var(--color-ink-medium); border-radius: 0.3em; - color: var(--color-ink); + color: var(--color-ink-dark); display: inline-grid; font-size: var(--text-small); font-weight: 800; inline-size: auto; + overflow: clip; + padding-block-start: 15%; place-content: center; + position: relative; + text-transform: uppercase; + + &::before { + background-color: var(--color-ink-medium); + block-size: 20%; + content: ""; + display: block; + inline-size: 100%; + inset: 0 0 auto; + position: absolute; + } .attachment--psd &, .attachment--ai &, .attachment--eps &, .attachment--indd &, .attachment--svg & { - color: oklch(var(--lch-red-medium)); + color: oklch(var(--lch-red-dark)); background-color: oklch(var(--lch-red-lightest)); - border-color: oklch(var(--lch-red-light)); + border-color: oklch(var(--lch-red-medium)); + + &::before { + background-color: oklch(var(--lch-red-medium)); + } } .attachment--css & @@ -149,9 +167,13 @@ .attachment--erb &, .attachment--ts &, .attachment--js & { - color: oklch(var(--lch-purple-medium)); + color: oklch(var(--lch-purple-dark)); background-color: oklch(var(--lch-purple-lightest)); - border-color: oklch(var(--lch-purple-light)); + border-color: oklch(var(--lch-purple-medium)); + + &::before { + background-color: oklch(var(--lch-purple-medium)); + } } .attachment--txt &, @@ -163,9 +185,13 @@ .attachment--xlsx &, .attachment--ppt &, .attachment--pptx & { - color: oklch(var(--lch-blue-medium)); + color: oklch(var(--lch-blue-dark)); background-color: oklch(var(--lch-blue-lightest)); - border-color: oklch(var(--lch-blue-light)); + border-color: oklch(var(--lch-blue-medium)); + + &::before { + background-color: oklch(var(--lch-blue-medium)); + } } } From adc84a7054c98eb2297fb431a460a3e9a3ad4e7d Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 28 May 2025 11:13:00 -0500 Subject: [PATCH 34/58] Improve color associations, add a few more types --- app/assets/stylesheets/rich-text-content.css | 30 +++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 512824a51..18434bda5 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -147,10 +147,14 @@ } .attachment--psd &, + .attachment--key &, + .attachment--sketch &, .attachment--ai &, .attachment--eps &, .attachment--indd &, - .attachment--svg & { + .attachment--svg &, + .attachment--ppt &, + .attachment--pptx & { color: oklch(var(--lch-red-dark)); background-color: oklch(var(--lch-red-lightest)); border-color: oklch(var(--lch-red-medium)); @@ -160,7 +164,9 @@ } } - .attachment--css & + .attachment--css &, + .attachment--php &, + .attachment--json &, .attachment--htm &, .attachment--html &, .attachment--rb &, @@ -177,14 +183,11 @@ } .attachment--txt &, + .attachment--pages &, .attachment--rtf &, .attachment--md &, .attachment--doc &, - .attachment--docx &, - .attachment--xls &, - .attachment--xlsx &, - .attachment--ppt &, - .attachment--pptx & { + .attachment--docx & { color: oklch(var(--lch-blue-dark)); background-color: oklch(var(--lch-blue-lightest)); border-color: oklch(var(--lch-blue-medium)); @@ -193,6 +196,19 @@ background-color: oklch(var(--lch-blue-medium)); } } + + .attachment--csv &, + .attachment--numbers &, + .attachment--xls &, + .attachment--xlsx & { + color: oklch(var(--lch-green-dark)); + background-color: oklch(var(--lch-green-lightest)); + border-color: oklch(var(--lch-green-medium)); + + &::before { + background-color: oklch(var(--lch-green-medium)); + } + } } .attachment__link { From 224fb1893e1dd58e681a92df6b9d2c6f192e033e Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 28 May 2025 11:13:59 -0500 Subject: [PATCH 35/58] Can't reliably use percentages here --- app/assets/stylesheets/rich-text-content.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 18434bda5..52254f84d 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -131,14 +131,14 @@ font-weight: 800; inline-size: auto; overflow: clip; - padding-block-start: 15%; + padding-block-start: 0.4lh; place-content: center; position: relative; text-transform: uppercase; &::before { background-color: var(--color-ink-medium); - block-size: 20%; + block-size: 0.5lh; content: ""; display: block; inline-size: 100%; From 2594bf90a70b8125ab7504aebe842448ecad9cae Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 28 May 2025 22:36:06 +0200 Subject: [PATCH 36/58] Bye house! --- app/javascript/application.js | 1 - config/importmap.rb | 1 - vendor/javascript/house.min.js | 1 - 3 files changed, 3 deletions(-) delete mode 100644 vendor/javascript/house.min.js diff --git a/app/javascript/application.js b/app/javascript/application.js index df3dda654..36d861cc7 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -2,7 +2,6 @@ import "@hotwired/turbo-rails" import "initializers" import "controllers" -import "house" import "actiontext-lexical" import "@rails/actiontext" diff --git a/config/importmap.rb b/config/importmap.rb index ead746d7e..2abe3a805 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -5,7 +5,6 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js" pin "@hotwired/stimulus", to: "stimulus.min.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin "@rails/request.js", to: "@rails--request.js" # @0.0.11 -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/vendor/javascript/house.min.js b/vendor/javascript/house.min.js deleted file mode 100644 index c81e324e5..000000000 --- a/vendor/javascript/house.min.js +++ /dev/null @@ -1 +0,0 @@ -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.#i()&&(this.innerHTML=this.#r())}#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})};#i(){return 0===this.children.length}#r(){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:r,endOffset:o,endContainer:l}=t,a=i(e,r,n);if(t.collapsed)return new s(e,a,a);const c=i(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,i=!1;for(;e.nextNode()&&(!s||!i);){const r=e.currentNode.textContent.length;n+r>=this.start&&!s&&(s=!0,t.setStart(e.currentNode,this.start-n)),n+r>=this.end&&!i&&(i=!0,t.setEnd(e.currentNode,this.end-n)),n+=r}return t}isEqual(t){return this.container===t.container&&this.start===t.start&&this.end===t.end}}function i(t,e,n){return e.nodeType===Node.ELEMENT_NODE?function(t,e,n){const s=e.childNodes[n];let i=0;const r=document.createTreeWalker(t);for(;r.nextNode();){if(r.currentNode===s)return i;r.currentNode.nodeType===Node.TEXT_NODE&&(i+=r.currentNode.textContent.length)}return i}(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 r{#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),i=-1===s?e.length:s+1;return{start:n,end:i,content:e.slice(n,i)}}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 r(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),i=`${s}${t}${this.content.slice(n)}`,r={start:s.length+t.length,end:s.length+t.length};this.#h(),this.#d(i,r)}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:i}=this.currentSelection,r=` ${n}[${t}](${e}) `;this.replaceText(r,s,i),this.selection.select({start:s+r.length,end:s+r.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.#b("> ")}toggleBulletList(){this.selection.isBulletList?this.#f("- "):this.#b("- ")}toggleNumberList(){this.selection.isNumberList?this.#f(/\d+\. /):this.#b(`${this.#L+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)+"",this.contentElement.classList.toggle("house-md-content-empty",this.isEmpty)}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.#w(e,n):this.#E(e,n)}#E(t,e=t){const{start:n,end:s,selection:i}=this.currentSelection,r=`${t}${i}${e}`;this.replaceText(r,n,s),this.selection.select({start:n,end:n+i.length+t.length+e.length})}#w(t,e=t){const{start:n,end:s,selection:i}=this.currentSelection;i.startsWith(t)&&i.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,i=s?"title":n;this.replaceText(`[${i}](url)`,t,e),s?this.selection.select({start:t+1,end:t+i.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.#L+1}. `,this.currentSelection.end+1,this.currentSelection.end+1))}#b(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#L(){const t=this.selection.previousLine().content.match(/^(\d+). /);return t?parseInt(t[1]):0}}class c{#A;constructor(t){this.#A=t}asPlainText(){const t=(new window.DOMParser).parseFromString(this.#A,"text/html");return this.#S(t.body).trim()}#S(t){return t.nodeType===Node.TEXT_NODE?t.nodeValue:Array.from(t.childNodes).map(((e,n)=>{const s=this.#k(e),i=n>0&&this.#k(t.childNodes[n-1]);return(s&&i?"\n\n":"")+this.#S(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 i=t.getTargetRanges()[0],{start:r,end:o}=s.fromDOMRange(i,e);n.call(this,t,r,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)}insertLineBreakHandler(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;static observedAttributes=["initialized"];constructor(){super(),this.internals=this.attachInternals(),this.internals.ariaRole="textbox"}attributeChangedCallback(t,e,n){"initialized"===t&&this.isConnected&&null!=e&&e!==n&&requestAnimationFrame((()=>this.#H()))}connectedCallback(){const t=this.querySelector(".house-md-content")?.textContent??this.textContent;this.document=new a(t,this),this.#y(),this.#N(),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.#_()}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.#D(),this.contentWrapper=document.createElement("div"),this.contentWrapper.classList.add("house-md-content"),this.contentWrapper.setAttribute("contenteditable",!0),this.contentWrapper.setAttribute("role","textbox"),this.contentWrapper.setAttribute("aria-multiline",!0),this.append(this.contentWrapper),this.getAttribute("tabindex")?(this.contentWrapper.setAttribute("tabindex",this.getAttribute("tabindex")),this.removeAttribute("tabindex")):this.contentWrapper.setAttribute("tabindex",0)}#D(){if(this.getAttribute("toolbar"))return document.getElementById(this.getAttribute("toolbar"));{const t=document.createElement("house-md-toolbar");return this.prepend(t),t}}#H(){this.disconnectedCallback(),this.getAttribute("toolbar")||this.toolbar.remove(),this.contentWrapper.remove(),this.connectedCallback()}#N(){this.addEventListener("beforeinput",this.#I),this.addEventListener("keydown",this.#B),this.addEventListener("dragover",this.#F),this.addEventListener("drop",this.#R),this.toolbar.addEventListener("house-md:toolbar-action",this.#M),document.addEventListener("selectionchange",this.#z),this.internals.form.addEventListener("submit",this.validate)}#_(){this.removeEventListener("beforeinput",this.#I),this.removeEventListener("keydown",this.#B),this.removeEventListener("dragover",this.#F),this.removeEventListener("drop",this.#R),this.toolbar.removeEventListener("house-md:toolbar-action",this.#M),document.removeEventListener("selectionchange",this.#z),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=({detail:t})=>{this.actionHandler.handleAction(t.houseMdAction,t)};#z=()=>{this.document.selection.update()}}customElements.define("house-md",g);class v extends HTMLElement{#W;#$;#O;constructor(){super(),this.percentComplete=0,this.status="pending"}connectedCallback(){this.#t(),this.#q(),this.addEventListener("click",this.#e)}disconnectedCallback(){this.removeEventListener("click",this.#e)}async#q(){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.#P,this.xhr.onload=this.#U,this.xhr.onerror=this.#K,this.xhr.onabort=this.#Q,this.xhr.send(t),this.status="uploading",this.#t()}#t(){this.setAttribute("status",this.status),this.innerHTML=`\n ${this.#V()}\n ${this.#X()}\n ${this.#j()}\n ${this.#Z()}\n `}#V(){return"failed"===this.status?"":""}#X(){return`
${this.file.name}
`}#j(){return`${this.percentComplete}%`}#Z(){return this.#O?`
${this.#O}
`:""}#P=t=>{t.lengthComputable&&(this.percentComplete=t.loaded/t.total*100),this.#t()};#U=async()=>{if(this.xhr.status>=400)this.status="failed",this.#O=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.#J()}this.#t()};#K=()=>{this.status="failed",this.#t()};#Q=()=>{this.status="aborted",this.#t()};#e=t=>{t.target.matches("[data-md-action=close]")&&this.remove()};async#J(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,r as Selection,n as Toolbar,v as Upload}; From 594161b98285370fc026ceccd8bf10d3479db524 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 28 May 2025 23:20:14 +0200 Subject: [PATCH 37/58] Make autosave work with lexical --- .../controllers/local_save_controller.js | 4 +-- .../controllers/paste_controller.js | 28 ------------------- app/views/cards/comments/_new.html.erb | 6 ++-- app/views/cards/comments/edit.html.erb | 2 +- app/views/cards/container/_title.html.erb | 4 +-- app/views/cards/edit.html.erb | 4 +-- 6 files changed, 10 insertions(+), 38 deletions(-) delete mode 100644 app/javascript/controllers/paste_controller.js diff --git a/app/javascript/controllers/local_save_controller.js b/app/javascript/controllers/local_save_controller.js index 26da25f43..8b49fb2f8 100644 --- a/app/javascript/controllers/local_save_controller.js +++ b/app/javascript/controllers/local_save_controller.js @@ -47,8 +47,8 @@ export default class extends Controller { } #triggerChangeEvent(newValue) { - if (this.inputTarget.tagName === "HOUSE-MD") { - this.inputTarget.dispatchEvent(new CustomEvent('house-md:change', { + if (this.inputTarget.tagName === "LEXICAL-EDITOR") { + this.inputTarget.dispatchEvent(new CustomEvent('actiontext:change', { bubbles: true, detail: { previousContent: '', diff --git a/app/javascript/controllers/paste_controller.js b/app/javascript/controllers/paste_controller.js deleted file mode 100644 index 79db7d7db..000000000 --- a/app/javascript/controllers/paste_controller.js +++ /dev/null @@ -1,28 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - pasteFiles(event) { - const editor = event.target.closest("house-md") - if (!editor) return - - const files = event.clipboardData?.files - if (!files?.length) return - - event.preventDefault() - - for (const file of files) { - const uploadEvent = new CustomEvent("house-md:before-upload", { - bubbles: true, - detail: { file }, - cancelable: true - }) - - if (editor.dispatchEvent(uploadEvent)) { - const upload = document.createElement("house-md-upload") - upload.file = file - upload.uploadsURL = editor.dataset.uploadsUrl - editor.appendChild(upload) - } - } - } -} \ No newline at end of file diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb index c65e36fd3..68659dd38 100644 --- a/app/views/cards/comments/_new.html.erb +++ b/app/views/cards/comments/_new.html.erb @@ -6,11 +6,11 @@
<%= form_with model: Comment.new, url: card_comments_path(card), class: "flex flex-column gap full-width", - data: { controller: "form paste local-save", + data: { controller: "form local-save", local_save_key_value: "comment-#{card.id}", - action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> + action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %> <%= form.rich_textarea :body, required: true, placeholder: new_comment_placeholder(card), - data: { local_save_target: "input", action: "house-md:change->local-save#save turbo:morph-element->local-save#restoreContent" } %> + data: { local_save_target: "input", action: "actiontext:change->local-save#save turbo:morph-element->local-save#restoreContent" } %> <%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %> Post this comment <% end %> diff --git a/app/views/cards/comments/edit.html.erb b/app/views/cards/comments/edit.html.erb index 2cd668298..726693c47 100644 --- a/app/views/cards/comments/edit.html.erb +++ b/app/views/cards/comments/edit.html.erb @@ -7,7 +7,7 @@
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width", - data: { controller: "form paste", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> + data: { controller: "form", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %> <%= form.rich_textarea :body, required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %>
<%= form.button class: "btn btn--reversed", type: :submit do %> diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index b0cfd7070..8bbe68550 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -10,7 +10,7 @@
<% end %> <% else %> - <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "paste auto-save" } do |form| %> + <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "auto-save" } do |form| %>

<%= form.text_area :title, placeholder: "Name it…", class: "input input--textarea full-width borderless txt-align-start", @@ -20,7 +20,7 @@
<%= form.rich_textarea :description, class: "card-field__description", placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "house-md:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> + data: { action: "actiontext:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
<% end %> <% end %> diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index 1317c6641..c02e2a635 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -1,5 +1,5 @@ <%= turbo_frame_tag @card, :edit do %> - <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "form paste" } do |form| %> + <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "form" } do |form| %>

<%= form.label :title, class: "flex flex-column align-center" do %> <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title", @@ -11,7 +11,7 @@ <%= form.rich_textarea :description, class: "card-field__description rich-text-content", placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> + data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>

<%= form.button "Save changes", type: :submit, class: "btn" %> <%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %> From 4c88c192f9f801ec776e468ac7430ad8890fd818 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 28 May 2025 23:21:05 +0200 Subject: [PATCH 38/58] Update lexical --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c83cc13ad..c40b60958 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 00721e1ce57f5363862f0a4d53efbbbc534cdf78 + revision: 23d1effaa144e1115633af038efe15c4e07daf0e branch: initial specs: actiontext-lexical (0.1.0) From a69dbdafe155539a3235a8836650694d0feb2cc2 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 28 May 2025 17:18:20 -0500 Subject: [PATCH 39/58] Simple format excerpts in activity --- app/helpers/excerpt_helper.rb | 12 ++++++++++++ app/views/events/event/_comment.html.erb | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 app/helpers/excerpt_helper.rb diff --git a/app/helpers/excerpt_helper.rb b/app/helpers/excerpt_helper.rb new file mode 100644 index 000000000..9ae0d3274 --- /dev/null +++ b/app/helpers/excerpt_helper.rb @@ -0,0 +1,12 @@ +module ExcerptHelper + def format_excerpt(content, length: 200) + return "" if content.blank? + + text = content.to_plain_text + text = text.gsub(/^>\s*(.*)$/m, '> \1') + text = text.gsub(/^[-*]\s*(.*)$/m, '• \1') + text = text.gsub(/^\d+\.\s*(.*)$/m) { |m| m } + text = text.gsub(/\s+/, " ").strip + text.truncate(length) + end +end diff --git a/app/views/events/event/_comment.html.erb b/app/views/events/event/_comment.html.erb index b437349bb..ed0cbb79c 100644 --- a/app/views/events/event/_comment.html.erb +++ b/app/views/events/event/_comment.html.erb @@ -1,5 +1,5 @@ <%= render "events/event/layout", card: event.eventable.card, event: event do %> - <%= strip_tags(event&.eventable.body.to_s).truncate(200) -%> + <%= format_excerpt(event&.eventable.body, length: 200) -%> <% end %> From 515cb2afd2b7cc289c9b1f089372a64f317c4fcf Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 00:38:07 +0200 Subject: [PATCH 40/58] Update lexical --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c40b60958..d9b3c3297 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 23d1effaa144e1115633af038efe15c4e07daf0e + revision: 8804035239fc0ba320dcb3cce47d56297af37634 branch: initial specs: actiontext-lexical (0.1.0) From 3eeba253ad2972771b95682e88b0774665550b33 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 00:39:06 +0200 Subject: [PATCH 41/58] Don't choke when rendering commands where the card was deleted --- app/models/command/go_to_card.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/command/go_to_card.rb b/app/models/command/go_to_card.rb index 0943413f3..a330d733b 100644 --- a/app/models/command/go_to_card.rb +++ b/app/models/command/go_to_card.rb @@ -4,7 +4,7 @@ class Command::GoToCard < Command validates_presence_of :card def title - "Visit card '#{card.title}'" + "Visit card '#{card&.title}'" end def execute @@ -13,6 +13,6 @@ class Command::GoToCard < Command private def card - user.accessible_cards.find(card_id) + user.accessible_cards.find_by_id(card_id) end end From ecf82d8703b09f2d940cdd916d42e7309b1f8232 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 12:37:16 +0200 Subject: [PATCH 42/58] Make tests pass with action text --- Gemfile | 2 +- Gemfile.lock | 3 +-- .../cards/comments_controller_test.rb | 4 +-- test/controllers/uploads_controller_test.rb | 25 ------------------- test/fixtures/action_text/markdowns.yml | 14 ----------- test/fixtures/action_text/rich_texts.yml | 20 ++++++++++++--- test/test_helper.rb | 2 +- test/test_helpers/action_text_test_helper.rb | 21 ++++++++++++++++ 8 files changed, 42 insertions(+), 49 deletions(-) delete mode 100644 test/controllers/uploads_controller_test.rb delete mode 100644 test/fixtures/action_text/markdowns.yml create mode 100644 test/test_helpers/action_text_test_helper.rb diff --git a/Gemfile b/Gemfile index c0455d262..d5b333b2c 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ gem "rqrcode" gem "redcarpet" gem "rouge" gem "jbuilder" -gem "actiontext-lexical", github: "basecamp/actiontext-lexical", branch: "initial" +gem "actiontext-lexical", github: "basecamp/actiontext-lexical" gem "image_processing", "~> 1.14" # Telemetry and logging diff --git a/Gemfile.lock b/Gemfile.lock index d9b3c3297..b3cefa8c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 8804035239fc0ba320dcb3cce47d56297af37634 - branch: initial + revision: 19038b86b8617062adbb930990edaacc1f643e3d specs: actiontext-lexical (0.1.0) rails (>= 8.0.2) diff --git a/test/controllers/cards/comments_controller_test.rb b/test/controllers/cards/comments_controller_test.rb index 690cf0656..012503e41 100644 --- a/test/controllers/cards/comments_controller_test.rb +++ b/test/controllers/cards/comments_controller_test.rb @@ -17,11 +17,11 @@ class Cards::CommentsControllerTest < ActionDispatch::IntegrationTest put card_comment_path(cards(:logo), comments(:logo_agreement_kevin)), params: { comment: { body: "I've changed my mind" } } assert_response :success - assert_equal "I've changed my mind", comments(:logo_agreement_kevin).reload.body.content + assert_action_text "I've changed my mind", comments(:logo_agreement_kevin).reload.body end test "update another user's comment" do - assert_no_changes "comments(:logo_agreement_jz).body.content" do + assert_no_changes ->{ comments(:logo_agreement_jz).reload.body.to_s } do put card_comment_path(cards(:logo), comments(:logo_agreement_jz)), params: { comment: { body: "I've changed my mind" } } end diff --git a/test/controllers/uploads_controller_test.rb b/test/controllers/uploads_controller_test.rb deleted file mode 100644 index 3f5f54af5..000000000 --- a/test/controllers/uploads_controller_test.rb +++ /dev/null @@ -1,25 +0,0 @@ -require "test_helper" - -class UploadsControllerTest < ActionDispatch::IntegrationTest - setup do - sign_in_as :kevin - end - - test "create" do - assert_changes -> { ActiveStorage::Attachment.count }, 1 do - post uploads_path(format: "json"), params: { file: fixture_file_upload("moon.jpg", "image/jpeg") }, as: :xhr - end - - assert_response :success - assert_equal ActiveStorage::Attachment.last.slug_url(host: "#{ApplicationRecord.current_tenant}.example.com", port: nil), response.parsed_body["fileUrl"] - assert_equal "image/jpeg", response.parsed_body["mimetype"] - assert_equal "moon.jpg", response.parsed_body["fileName"] - end - - test "show" do - accounts("37s").uploads.attach fixture_file_upload("moon.jpg", "image/jpeg") - get upload_path(slug: accounts("37s").uploads.last.slug) - assert_response :redirect - assert_match /\/rails\/active_storage\/.*\/moon\.jpg/, @response.redirect_url - end -end diff --git a/test/fixtures/action_text/markdowns.yml b/test/fixtures/action_text/markdowns.yml deleted file mode 100644 index 519cd408b..000000000 --- a/test/fixtures/action_text/markdowns.yml +++ /dev/null @@ -1,14 +0,0 @@ -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/action_text/rich_texts.yml b/test/fixtures/action_text/rich_texts.yml index 8b371ea60..9a86c3b30 100644 --- a/test/fixtures/action_text/rich_texts.yml +++ b/test/fixtures/action_text/rich_texts.yml @@ -1,4 +1,16 @@ -# one: -# record: name_of_fixture (ClassOfFixture) -# name: content -# body:

In a million stars!

+logo_agreement_jz: + record: logo_agreement_jz (Comment) + name: body + body: I agree. + +logo_agreement_kevin: + record: logo_agreement_kevin (Comment) + name: body + body: Same, let’s do it. + +layout_overflowing_david: + record: layout_overflowing_david (Comment) + name: body + body: The text is overflowing the container. + + diff --git a/test/test_helper.rb b/test/test_helper.rb index 6e27d82fa..4e0a9fcee 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -22,7 +22,7 @@ module ActiveSupport fixtures :all include ActiveJob::TestHelper - include CardTestHelper, ChangeTestHelper, SessionTestHelper + include ActionTextTestHelper, CardTestHelper, ChangeTestHelper, SessionTestHelper end end diff --git a/test/test_helpers/action_text_test_helper.rb b/test/test_helpers/action_text_test_helper.rb new file mode 100644 index 000000000..037f2561f --- /dev/null +++ b/test/test_helpers/action_text_test_helper.rb @@ -0,0 +1,21 @@ +module ActionTextTestHelper + def assert_action_text(expected_html, content) + assert_equal_html <<~HTML, content.to_s +
#{expected_html} + HTML + end + + def assert_equal_html(expected, actual) + assert_equal normalize_html(expected), normalize_html(actual) + end + + def normalize_html(html) + Nokogiri::HTML.fragment(html).tap do |fragment| + fragment.traverse do |node| + if node.text? + node.content = node.text.strip.gsub(/\s+/, ' ') + end + end + end.to_html.strip + end +end From 34eb8f167234aa74889c42201e075b39be9678b6 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 12:38:07 +0200 Subject: [PATCH 43/58] Format --- test/controllers/cards/comments_controller_test.rb | 2 +- test/test_helpers/action_text_test_helper.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/controllers/cards/comments_controller_test.rb b/test/controllers/cards/comments_controller_test.rb index 012503e41..76ff0f6fa 100644 --- a/test/controllers/cards/comments_controller_test.rb +++ b/test/controllers/cards/comments_controller_test.rb @@ -21,7 +21,7 @@ class Cards::CommentsControllerTest < ActionDispatch::IntegrationTest end test "update another user's comment" do - assert_no_changes ->{ comments(:logo_agreement_jz).reload.body.to_s } do + assert_no_changes -> { comments(:logo_agreement_jz).reload.body.to_s } do put card_comment_path(cards(:logo), comments(:logo_agreement_jz)), params: { comment: { body: "I've changed my mind" } } end diff --git a/test/test_helpers/action_text_test_helper.rb b/test/test_helpers/action_text_test_helper.rb index 037f2561f..24070515e 100644 --- a/test/test_helpers/action_text_test_helper.rb +++ b/test/test_helpers/action_text_test_helper.rb @@ -13,7 +13,7 @@ module ActionTextTestHelper Nokogiri::HTML.fragment(html).tap do |fragment| fragment.traverse do |node| if node.text? - node.content = node.text.strip.gsub(/\s+/, ' ') + node.content = node.text.strip.gsub(/\s+/, " ") end end end.to_html.strip From 9287d03d78c332e5402d96dd7f72ce484c6c1ef0 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 12:39:14 +0200 Subject: [PATCH 44/58] Cleanup --- script/convert_markdowns_to_rich_text.rb | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/script/convert_markdowns_to_rich_text.rb b/script/convert_markdowns_to_rich_text.rb index 29e71b31b..fabe99a92 100755 --- a/script/convert_markdowns_to_rich_text.rb +++ b/script/convert_markdowns_to_rich_text.rb @@ -29,7 +29,6 @@ class MarkdownToActionTextConverter src = img["src"].presence if src && match = src.match(ATTACHMENT_URL_REGEX) if (attachment = find_attachment(match[:slug])) - puts "REEMPLAZO #{build_attachment_node(attachment).to_html}" img.replace(build_attachment_node(attachment)) @attachments << attachment end @@ -104,13 +103,3 @@ ApplicationRecord.with_each_tenant do |tenant| warn "✗ Failed to process markdown ##{markdown.id}: #{e.class} - #{e.message}" end end - -# create_table "action_text_markdowns", force: :cascade do |t| -# t.text "content", default: "", null: false -# t.datetime "created_at", null: false -# t.string "name", null: false -# t.integer "record_id", null: false -# t.string "record_type", null: false -# t.datetime "updated_at", null: false -# t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record" -# end From 1f051896a1c3781e36e6a2cf4308b2e676768fed Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 12:51:08 +0200 Subject: [PATCH 45/58] Format --- script/convert_markdowns_to_rich_text.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/script/convert_markdowns_to_rich_text.rb b/script/convert_markdowns_to_rich_text.rb index fabe99a92..1a43db239 100755 --- a/script/convert_markdowns_to_rich_text.rb +++ b/script/convert_markdowns_to_rich_text.rb @@ -23,7 +23,6 @@ class MarkdownToActionTextConverter end private - def process_images @doc.css("img").each do |img| src = img["src"].presence From 6c8c9d30d10ff7c3afb432692c4ce3ee7c1a7c34 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 12:51:12 +0200 Subject: [PATCH 46/58] Fix merge issues --- Gemfile.lock | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 5b9940aad..81baa8d86 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,10 @@ +GIT + remote: https://github.com/basecamp/actiontext-lexical.git + revision: 19038b86b8617062adbb930990edaacc1f643e3d + specs: + actiontext-lexical (0.1.0) + rails (>= 8.0.2) + GIT remote: https://github.com/basecamp/active_record-tenanted revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b @@ -188,6 +195,10 @@ GEM net-http (>= 0.5.0) faraday-retry (2.3.1) faraday (~> 2.0) + ffi (1.17.2) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -199,6 +210,9 @@ GEM hashdiff (1.1.2) i18n (1.14.7) concurrent-ruby (~> 1.0) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) importmap-rails (2.1.0) actionpack (>= 6.0.0) activesupport (>= 6.0.0) @@ -236,6 +250,9 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + mini_magick (5.2.0) + benchmark + logger mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.25.5) @@ -354,6 +371,9 @@ GEM rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) + ruby-vips (2.2.3) + ffi (~> 1.12) + logger rubyzip (2.4.1) securerandom (0.4.1) selenium-webdriver (4.32.0) @@ -448,6 +468,7 @@ PLATFORMS x86_64-mingw32 DEPENDENCIES + actiontext-lexical! active_record-tenanted! bcrypt (~> 3.1.7) bootsnap @@ -456,6 +477,7 @@ DEPENDENCIES capybara debug geared_pagination (~> 1.2) + image_processing (~> 1.14) importmap-rails jbuilder kamal From 1e18fa87a98349cf0b1e73d60abe015130d42233 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 13:01:29 +0200 Subject: [PATCH 47/58] Update lexical --- Gemfile | 2 +- Gemfile.lock | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 054167d6e..07bc1e4a6 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ gem "rqrcode" gem "redcarpet" gem "rouge" gem "jbuilder" -gem "actiontext-lexical", github: "basecamp/actiontext-lexical" +gem "actiontext-lexical", github: "basecamp/actiontext-lexical", branch: "main" gem "image_processing", "~> 1.14" gem "platform_agent" diff --git a/Gemfile.lock b/Gemfile.lock index 81baa8d86..a9d15566b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,7 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 19038b86b8617062adbb930990edaacc1f643e3d + revision: 182d61aeaf588861014046f9bb1ed3c5d1612b7e + branch: main specs: actiontext-lexical (0.1.0) rails (>= 8.0.2) From 6d88975d462677d0cbc80e112f1f5544314e9f36 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 13:07:20 +0200 Subject: [PATCH 48/58] Use existing method --- test/test_helpers/action_text_test_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_helpers/action_text_test_helper.rb b/test/test_helpers/action_text_test_helper.rb index 24070515e..09a26fceb 100644 --- a/test/test_helpers/action_text_test_helper.rb +++ b/test/test_helpers/action_text_test_helper.rb @@ -13,7 +13,7 @@ module ActionTextTestHelper Nokogiri::HTML.fragment(html).tap do |fragment| fragment.traverse do |node| if node.text? - node.content = node.text.strip.gsub(/\s+/, " ") + node.content = node.text.squish end end end.to_html.strip From 0acfe2bcb716c6d4b0e33d60fc29f623a4eb84b3 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 13:32:38 +0200 Subject: [PATCH 49/58] Update lexical --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a9d15566b..ef74ca4fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical.git - revision: 182d61aeaf588861014046f9bb1ed3c5d1612b7e + revision: 777b2a4a933db9cb4cd515c4569dfcaf8cae45b5 branch: main specs: actiontext-lexical (0.1.0) From f00fffe801c67a0ef0e9214ca3048d8b021e9772 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 13:34:16 +0200 Subject: [PATCH 50/58] Remove trace --- app/javascript/controllers/local_save_controller.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/javascript/controllers/local_save_controller.js b/app/javascript/controllers/local_save_controller.js index 8b49fb2f8..5f9fc83a3 100644 --- a/app/javascript/controllers/local_save_controller.js +++ b/app/javascript/controllers/local_save_controller.js @@ -34,7 +34,6 @@ export default class extends Controller { if (savedContent) { savedContent = `
${savedContent}
` // temporary for old markdown saves - console.debug("Es", savedContent); this.inputTarget.value = savedContent this.#triggerChangeEvent(savedContent) } From c1f1183bb0eea29ab59fd67964b0fbd2e8adf991 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 13:39:15 +0200 Subject: [PATCH 51/58] Update actiontext lexical --- Gemfile | 2 +- Gemfile.lock | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 07bc1e4a6..8c1219952 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ gem "rqrcode" gem "redcarpet" gem "rouge" gem "jbuilder" -gem "actiontext-lexical", github: "basecamp/actiontext-lexical", branch: "main" +gem "actiontext-lexical", bc: "actiontext-lexical" gem "image_processing", "~> 1.14" gem "platform_agent" diff --git a/Gemfile.lock b/Gemfile.lock index ef74ca4fa..0ed3a138e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,6 @@ GIT - remote: https://github.com/basecamp/actiontext-lexical.git + remote: https://github.com/basecamp/actiontext-lexical revision: 777b2a4a933db9cb4cd515c4569dfcaf8cae45b5 - branch: main specs: actiontext-lexical (0.1.0) rails (>= 8.0.2) From 08d8b2e5ff9c7ebb8d9fbbe2b97c8ef3697f7add Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 14:22:27 +0200 Subject: [PATCH 52/58] Revert "Replace House with Lexical" --- .DS_Store | Bin 6148 -> 0 bytes Dockerfile | 2 +- Gemfile | 2 - Gemfile.lock | 22 -- app/assets/stylesheets/_global.css | 11 - app/assets/stylesheets/actiontext-lexical.css | 113 ------ app/assets/stylesheets/cards.css | 7 - app/assets/stylesheets/comments.css | 23 +- app/assets/stylesheets/markdown-content.css | 110 ++++++ app/assets/stylesheets/rich-text-content.css | 331 ------------------ app/helpers/excerpt_helper.rb | 12 - app/helpers/notifications_helper.rb | 2 +- app/javascript/application.js | 4 +- .../controllers/local_save_controller.js | 7 +- .../controllers/paste_controller.js | 28 ++ .../syntax_highlight_controller.js | 8 - app/models/card.rb | 2 +- app/models/comment.rb | 6 +- app/models/concerns/mentions.rb | 8 +- app/views/active_storage/blobs/_blob.html.erb | 42 --- .../blobs/web/_representation.html.erb | 19 - app/views/cards/comments/_comment.html.erb | 4 +- app/views/cards/comments/_new.html.erb | 10 +- app/views/cards/comments/edit.html.erb | 6 +- app/views/cards/container/_title.html.erb | 12 +- app/views/cards/edit.html.erb | 10 +- app/views/events/event/_comment.html.erb | 2 +- .../action_text/contents/_content.html.erb | 3 - app/views/layouts/application.html.erb | 2 +- config/importmap.rb | 4 +- config/initializers/markdown.rb | 7 + ...4_create_action_text_tables.action_text.rb | 26 -- db/schema.rb | 12 +- db/schema_cache.yml | 196 +++++------ lib/markdown_renderer.rb | 81 +++++ lib/rails_ext/action_text_has_markdown.rb | 47 +++ lib/rails_ext/action_text_markdown.rb | 44 +++ lib/rails_ext/action_text_tag_helper.rb | 36 ++ lib/rails_ext/active_storage_sluggable.rb | 32 ++ script/convert_markdowns_to_rich_text.rb | 104 ------ script/migrate-to-multi-tenant | 20 +- script/reset_cards_ids.rb | 6 +- script/reset_collections_ids.rb | 6 +- .../cards/comments_controller_test.rb | 4 +- test/controllers/cards_controller_test.rb | 2 +- test/controllers/uploads_controller_test.rb | 25 ++ .../{rich_texts.yml => markdowns.yml} | 8 +- test/test_helper.rb | 2 +- test/test_helpers/action_text_test_helper.rb | 21 -- vendor/javascript/house.min.js | 1 + 50 files changed, 576 insertions(+), 916 deletions(-) delete mode 100644 .DS_Store delete mode 100644 app/assets/stylesheets/actiontext-lexical.css create mode 100644 app/assets/stylesheets/markdown-content.css delete mode 100644 app/assets/stylesheets/rich-text-content.css delete mode 100644 app/helpers/excerpt_helper.rb create mode 100644 app/javascript/controllers/paste_controller.js delete mode 100644 app/javascript/controllers/syntax_highlight_controller.js delete mode 100644 app/views/active_storage/blobs/_blob.html.erb delete mode 100644 app/views/active_storage/blobs/web/_representation.html.erb delete mode 100644 app/views/layouts/action_text/contents/_content.html.erb create mode 100644 config/initializers/markdown.rb delete mode 100644 db/migrate/20250523084734_create_action_text_tables.action_text.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 delete mode 100755 script/convert_markdowns_to_rich_text.rb create mode 100644 test/controllers/uploads_controller_test.rb rename test/fixtures/action_text/{rich_texts.yml => markdowns.yml} (69%) delete mode 100644 test/test_helpers/action_text_test_helper.rb create mode 100644 vendor/javascript/house.min.js diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 1.14" gem "platform_agent" # Telemetry and logging diff --git a/Gemfile.lock b/Gemfile.lock index 0ed3a138e..5b9940aad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,3 @@ -GIT - remote: https://github.com/basecamp/actiontext-lexical - revision: 777b2a4a933db9cb4cd515c4569dfcaf8cae45b5 - specs: - actiontext-lexical (0.1.0) - rails (>= 8.0.2) - GIT remote: https://github.com/basecamp/active_record-tenanted revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b @@ -195,10 +188,6 @@ GEM net-http (>= 0.5.0) faraday-retry (2.3.1) faraday (~> 2.0) - ffi (1.17.2) - ffi (1.17.2-arm64-darwin) - ffi (1.17.2-x86_64-darwin) - ffi (1.17.2-x86_64-linux-gnu) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -210,9 +199,6 @@ GEM hashdiff (1.1.2) i18n (1.14.7) concurrent-ruby (~> 1.0) - image_processing (1.14.0) - mini_magick (>= 4.9.5, < 6) - ruby-vips (>= 2.0.17, < 3) importmap-rails (2.1.0) actionpack (>= 6.0.0) activesupport (>= 6.0.0) @@ -250,9 +236,6 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) - mini_magick (5.2.0) - benchmark - logger mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.25.5) @@ -371,9 +354,6 @@ GEM rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) - ruby-vips (2.2.3) - ffi (~> 1.12) - logger rubyzip (2.4.1) securerandom (0.4.1) selenium-webdriver (4.32.0) @@ -468,7 +448,6 @@ PLATFORMS x86_64-mingw32 DEPENDENCIES - actiontext-lexical! active_record-tenanted! bcrypt (~> 3.1.7) bootsnap @@ -477,7 +456,6 @@ DEPENDENCIES capybara debug geared_pagination (~> 1.2) - image_processing (~> 1.14) importmap-rails jbuilder kamal diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 5b2a966af..aad4508fc 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -177,17 +177,6 @@ --color-card-7: oklch(var(--lch-purple-medium)); --color-card-8: oklch(var(--lch-pink-medium)); - /* Colors: Syntax highlighting */ - --color-code-token__att: oklch(var(--lch-blue-dark)); - --color-code-token__comment: oklch(var(--lch-ink-medium)); - --color-code-token__function: oklch(var(--lch-purple-dark)); - --color-code-token__operator: oklch(var(--lch-red-dark)); - --color-code-token__property: oklch(var(--lch-purple-dark)); - --color-code-token__punctuation: oklch(var(--lch-ink-dark)); - --color-code-token__selector: oklch(var(--lch-green-dark)); - --color-code-token__variable: oklch(var(--lch-red-dark)); - - @media (prefers-color-scheme: dark) { --lch-canvas: 20% 0.0195 232.58; --lch-ink-inverted: var(--lch-black); diff --git a/app/assets/stylesheets/actiontext-lexical.css b/app/assets/stylesheets/actiontext-lexical.css deleted file mode 100644 index c0ab455a5..000000000 --- a/app/assets/stylesheets/actiontext-lexical.css +++ /dev/null @@ -1,113 +0,0 @@ -lexical-editor { - .node--selected { - &:not(:has(img)) { - box-shadow: 0 0 0 var(--hover-size) var(--hover-color); - } - - &:has(img) { - img { - box-shadow: 0 0 0 var(--hover-size) var(--hover-color); - } - } - } -} - -.lexical-dialog-actions { - display: flex; - font-size: var(--text-x-small); - flex: 1 1 0%; - gap: var(--inline-space-half); - margin-block-start: var(--block-space-half); - - .btn { - --radius: 0.3em; - - inline-size: 100%; - justify-content: center; - } - - span { - inline-size: 100%; - } -} - -.lexical-editor__content { - --outline-size: max(2px, 0.08em); - - margin: var(--block-space-half) 0; - min-block-size: calc(7lh + var(--block-space)); - outline: 0; - padding: var(--block-space-half) var(--inline-space); - - * { - &:first-child { - margin-block-start: 0; - } - - &:last-child { - margin-block-end: 0; - } - } -} - -.lexical-editor--drag-over { - background-color: var(--color-selected); - border-radius: 4px; - outline: 2px dashed var(--color-selected-dark); -} - -lexical-toolbar { - background-color: inherit; - border-block-end: 1px solid var(--color-ink-light); - color: currentColor; - display: flex; - font-size: inherit; - margin: 0; - max-inline-size: 100%; - padding: 0.2em 0; - position: relative; - - dialog { - background-color: var(--color-canvas); - border: 1px solid var(--color-ink-lighter); - border-radius: 0.5em; - box-shadow: var(--shadow); - color: var(--color-ink); - padding: var(--block-space) calc(var(--inline-space) * 1.5); - position: absolute; - } - - > button { - aspect-ratio: 4/3.5; - appearance: none; - background-color: transparent; - block-size: 2em; - border: none; - border-radius: 0.2em; - 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.85em; - fill: currentColor; - grid-area: 1/1; - inline-size: auto; - user-select: none; - } - - &:is(:focus, :hover) { - background-color: var(--color-ink-lighter); - box-shadow: none; - } - - &:is(:active) { - background-color: var(--color-selected); - } - } -} diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index d13b60498..4e3c237f7 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -150,13 +150,6 @@ } } - .card-field__description { - lexical-toolbar { - border-block-start: 1px solid var(--color-ink-light); - margin-block-start: var(--block-margin) !important; - } - } - .card__content { flex: 2 1 auto; max-inline-size: 100%; diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index 05d3518bc..2a5e47960 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -1,7 +1,7 @@ @layer components { .comments { --avatar-size: 2.33em; - --comment-padding-block: var(--block-space-half); + --comment-padding-block: var(--block-space); --comment-padding-inline: var(--inline-space-double); --comment-max: 70ch; --reaction-size: 2rem; @@ -47,7 +47,7 @@ } .comment__avatar { - margin: calc(var(--comment-padding-block) * 0.75) calc(var(--comment-padding-inline) * -0.75); + margin: calc(var(--comment-padding-block) * 0.5) calc(var(--comment-padding-inline) * -0.75); z-index: 0; .comment--mine_ & { @@ -64,7 +64,7 @@ text-align: center; } - * { + p { &:first-child { margin-block-start: 0; } @@ -83,8 +83,6 @@ border-radius: 0.2em; padding: var(--comment-padding-block) - var(--comment-padding-inline) - calc(var(--comment-padding-block) * 1.5) var(--comment-padding-inline); word-wrap: break-word; } @@ -115,7 +113,7 @@ line-height: 1.2; max-inline-size: var(--comment-max); text-align: center; - + &::before { /* Make up space for lack of avatar */ @@ -135,4 +133,17 @@ text-align: center !important; } } + + .comment__input { + --input-border-size: 0; + --input-padding: 0; + + min-block-size: calc(9lh + (2 * var(--comment-padding-block))); + min-inline-size: 100%; + + @supports (field-sizing: content) { + field-sizing: content; + min-block-size: calc(9lh + (2 * var(--comment-padding-block))); + } + } } diff --git a/app/assets/stylesheets/markdown-content.css b/app/assets/stylesheets/markdown-content.css new file mode 100644 index 000000000..81589501b --- /dev/null +++ b/app/assets/stylesheets/markdown-content.css @@ -0,0 +1,110 @@ +@layer components { + .markdown-content { + :where(h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6) { + font-weight: 800; + hyphens: auto; + letter-spacing: -0.02ch; + line-height: 1.1; + margin-block: 0; + overflow-wrap: break-word; + text-wrap: balance; + } + + :where(h1, h2, h3, h4, h5, h6) { display: block; } + :where(h1, h2, h3, h4, h5, h6) { margin-block: 0.65em; } + + :is(h1, .h1) { font-size: 2em; } + :is(h2, .h2) { font-size: 1.5em; } + :is(h3, .h3) { font-size: 1.17em; } + :is(h4, .h4) { font-size: 1em; } + :is(h5, .h5) { font-size: 0.83em; } + :is(h6, .h6) { font-size: 0.67em; } + + :where(p, ul, ol, dl, blockquote, pre, figure, table, hr) { + margin-block: 0.65lh; + overflow-wrap: break-word; + text-wrap: pretty; + } + + :where(hr) { + border-color: var(--color-ink-medium); + border-style: var(--border-style, solid) none none; + margin: 2lh auto; + } + + :where(strong) { + font-weight: 700; + } + + :is(pre, code, .pre, .code) { + background-color: var(--color-ink-lightest); + border: 1px solid var(--color-ink-lighter); + border-radius: 0.3em; + font-family: var(--font-mono); + font-size: 0.85em; + } + + :is(pre) { + overflow-x: auto; + /* text-wrap: nowrap; */ + } + + :where(code) { + padding: 0.1em 0.3em; + } + + :is(pre, .pre) { + border-radius: 0.5em; + padding: 0.5lh 2ch; + + :is(code, .code) { + background-color: transparent; + border: 0; + font-size: 1em; + padding: 0; + } + } + + :where(p) { + hyphens: auto; + letter-spacing: -0.005ch; + } + + :is(blockquote, .quote) { + font-style: italic; + margin: 0 3ch; + + :is(p) { + hyphens: none; + } + } + + :where(table, th, td) { + border: 1px solid var(--color-ink-medium); + } + + :where(table) { + border-collapse: collapse; + margin: 1lh 0; + } + + :where(th, td) { + padding: 0.2lh 1ch; + text-align: start; + } + + :where(th) { + border-block-end-width: 3px; + font-weight: 700; + } + + :where(img, video, embed, object) { + max-block-size: 32rem; + + /* Links should hug media contained within */ + a:has(&) { + display: inline-block; + } + } + } +} diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css deleted file mode 100644 index 52254f84d..000000000 --- a/app/assets/stylesheets/rich-text-content.css +++ /dev/null @@ -1,331 +0,0 @@ -@layer components { - .rich-text-content { - --block-margin: 0.65lh; - - :where(h1, h2, h3, h4, h5, h6) { - display: block; - font-weight: 800; - hyphens: auto; - letter-spacing: -0.02ch; - line-height: 1.1; - margin-block: var(--block-margin); - overflow-wrap: break-word; - text-wrap: balance; - } - - :where(h1) { font-size: 2em; } - :where(h2) { font-size: 1.5em; } - :where(h3) { font-size: 1.17em; } - :where(h4) { font-size: 1em; } - :where(h5) { font-size: 0.83em; } - :where(h6) { font-size: 0.67em; } - - :where(p, ul, ol, dl, blockquote, figure, .attachment) { - margin-block: var(--block-margin); - overflow-wrap: break-word; - text-wrap: pretty; - } - - :where(b, strong) { - font-weight: 700; - } - - :where(p, blockquote) { - hyphens: auto; - letter-spacing: -0.005ch; - } - - :where(blockquote) { - border-inline-start: 0.25em solid var(--color-ink-lighter); - font-style: italic; - margin: var(--block-margin) 0; - padding: 0.5lh 2ch; - } - - :where(img, video, embed, object) { - margin-inline: auto; - max-block-size: 32rem; - - /* Links should hug media contained within */ - a:has(&) { - display: inline-block; - } - } - - /* Attachments */ - .attachment { - block-size: auto; - display: inline-block; - inline-size: 100%; - position: relative; - margin-inline: 0; - max-inline-size: 100%; - - &:is(figure) img { - block-size: auto; - display: block; - margin-inline: auto; - max-inline-size: 100%; - user-select: none; - } - - :where(progress) { - inline-size: 100%; - margin: auto; - } - } - - .attachment__caption { - color: var(--color-ink-dark); - display: flex; - font-size: var(--text-small); - flex-wrap: wrap; - line-height: 1.3; - column-gap: 0.5ch; - justify-content: center; - margin: var(--block-space-half) var(--inline-space); - text-align: center; - - .input { - inline-size: 100%; - max-inline-size: 100%; - text-align: center; - - @supports (field-sizing: content) { - field-sizing: content; - inline-size: auto; - min-inline-size: 20ch; - } - } - } - - .attachment__figure { - --hover-size: 0; - } - - .attachment--file { - align-items: center; - display: inline-flex; - - .attachment__caption { - flex-direction: column; - flex-wrap: nowrap; - justify-content: flex-start; - text-align: start; - - strong { - color: var(--color-ink); - } - } - } - - .attachment__icon { - aspect-ratio: 4/5; - background-color: var(--color-canvas); - block-size: calc(2.5lh + 0.5ch); - border: 2px solid var(--color-ink-medium); - border-radius: 0.3em; - color: var(--color-ink-dark); - display: inline-grid; - font-size: var(--text-small); - font-weight: 800; - inline-size: auto; - overflow: clip; - padding-block-start: 0.4lh; - place-content: center; - position: relative; - text-transform: uppercase; - - &::before { - background-color: var(--color-ink-medium); - block-size: 0.5lh; - content: ""; - display: block; - inline-size: 100%; - inset: 0 0 auto; - position: absolute; - } - - .attachment--psd &, - .attachment--key &, - .attachment--sketch &, - .attachment--ai &, - .attachment--eps &, - .attachment--indd &, - .attachment--svg &, - .attachment--ppt &, - .attachment--pptx & { - color: oklch(var(--lch-red-dark)); - background-color: oklch(var(--lch-red-lightest)); - border-color: oklch(var(--lch-red-medium)); - - &::before { - background-color: oklch(var(--lch-red-medium)); - } - } - - .attachment--css &, - .attachment--php &, - .attachment--json &, - .attachment--htm &, - .attachment--html &, - .attachment--rb &, - .attachment--erb &, - .attachment--ts &, - .attachment--js & { - color: oklch(var(--lch-purple-dark)); - background-color: oklch(var(--lch-purple-lightest)); - border-color: oklch(var(--lch-purple-medium)); - - &::before { - background-color: oklch(var(--lch-purple-medium)); - } - } - - .attachment--txt &, - .attachment--pages &, - .attachment--rtf &, - .attachment--md &, - .attachment--doc &, - .attachment--docx & { - color: oklch(var(--lch-blue-dark)); - background-color: oklch(var(--lch-blue-lightest)); - border-color: oklch(var(--lch-blue-medium)); - - &::before { - background-color: oklch(var(--lch-blue-medium)); - } - } - - .attachment--csv &, - .attachment--numbers &, - .attachment--xls &, - .attachment--xlsx & { - color: oklch(var(--lch-green-dark)); - background-color: oklch(var(--lch-green-lightest)); - border-color: oklch(var(--lch-green-medium)); - - &::before { - background-color: oklch(var(--lch-green-medium)); - } - } - } - - .attachment__link { - --hover-size: 0; - - color: var(--color-link); - text-decoration: underline; - } - - /* Attachment junk gets wrapped in a paragraph causing unwanted space */ - p:has(action-text-attachment) { - display: none; - - + * { - margin-block-start: 0; - } - } - - /* Avoid extra space due to empty paragraphs */ - p:empty { - display: none; - } - - /* Code */ - :where(code, pre) { - background-color: var(--color-canvas); - border: 1px solid var(--color-ink-lighter); - border-radius: 0.3em; - color: var(--color-ink); - font-family: var(--font-mono); - font-size: 0.85em; - font-weight: 500; - margin-block: var(--block-margin); - padding: 0.1em 0.3em; - - &[data-language] { - border-radius: 0.5em; - display: block; - hyphens: none; - overflow-x: auto; - padding: 0.5lh 2ch; - tab-size: 2; - text-wrap: nowrap; - white-space: pre; - word-break: break-word; - - /* Keywords and attributes */ - .code-token__attr, - .token.atrule, - .token.attr, - .token.keyword { - color: var(--color-code-token__att); - } - - /* Constants, booleans, numbers, properties, tags */ - .code-token__property, - .token.boolean, - .token.constant, - .token.deleted, - .token.number, - .token.property, - .token.symbol, - .token.tag { - color: var(--color-code-token__property); - } - - /* Strings, selectors, and built-in constructs */ - .code-token__selector, - .token.builtin, - .token.char, - .token.inserted, - .token.selector, - .token.string { - color: var(--color-code-token__selector); - } - - /* Comments and meta information */ - .code-token__comment, - .token.cdata, - .token.comment, - .token.doctype, - .token.prolog { - color: var(--color-code-token__comment); - font-style: italic; - } - - /* Operators and symbolic entities */ - .code-token__operator, - .token.entity, - .token.operator, - .token.url { - color: var(--color-code-token__operator); - } - - /* Functions and class names */ - .code-token__function, - .token.class, - .token.class-name, - .token.function { - color: var(--color-code-token__function); - } - - /* Variables, regex, namespaces, important */ - .code-token__variable, - .token.important, - .token.namespace, - .token.regex, - .token.variable { - color: var(--color-code-token__variable); - } - - /* Punctuation */ - .code-token__punctuation, - .token.punctuation { - color: var(--color-code-token__punctuation); - } - } - } - } -} diff --git a/app/helpers/excerpt_helper.rb b/app/helpers/excerpt_helper.rb deleted file mode 100644 index 9ae0d3274..000000000 --- a/app/helpers/excerpt_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -module ExcerptHelper - def format_excerpt(content, length: 200) - return "" if content.blank? - - text = content.to_plain_text - text = text.gsub(/^>\s*(.*)$/m, '> \1') - text = text.gsub(/^[-*]\s*(.*)$/m, '• \1') - text = text.gsub(/^\d+\.\s*(.*)$/m) { |m| m } - text = text.gsub(/\s+/, " ").strip - text.truncate(length) - end -end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 8dab7c110..57fd63eff 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -58,7 +58,7 @@ module NotificationsHelper def comment_notification_body(event) comment = event.eventable - "#{strip_tags(comment.body.to_s).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body.to_s).truncate(200)}" + "#{strip_tags(comment.body_html).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body_html).truncate(200)}" end def card_notification_title(card) diff --git a/app/javascript/application.js b/app/javascript/application.js index 36d861cc7..c2917aa3e 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -2,6 +2,4 @@ import "@hotwired/turbo-rails" import "initializers" import "controllers" - -import "actiontext-lexical" -import "@rails/actiontext" +import "house" diff --git a/app/javascript/controllers/local_save_controller.js b/app/javascript/controllers/local_save_controller.js index 5f9fc83a3..5d9b89a72 100644 --- a/app/javascript/controllers/local_save_controller.js +++ b/app/javascript/controllers/local_save_controller.js @@ -30,10 +30,9 @@ export default class extends Controller { async restoreContent() { await nextFrame() - let savedContent = localStorage.getItem(this.keyValue) + const savedContent = localStorage.getItem(this.keyValue) if (savedContent) { - savedContent = `
${savedContent}
` // temporary for old markdown saves this.inputTarget.value = savedContent this.#triggerChangeEvent(savedContent) } @@ -46,8 +45,8 @@ export default class extends Controller { } #triggerChangeEvent(newValue) { - if (this.inputTarget.tagName === "LEXICAL-EDITOR") { - this.inputTarget.dispatchEvent(new CustomEvent('actiontext:change', { + if (this.inputTarget.tagName === "HOUSE-MD") { + this.inputTarget.dispatchEvent(new CustomEvent('house-md:change', { bubbles: true, detail: { previousContent: '', diff --git a/app/javascript/controllers/paste_controller.js b/app/javascript/controllers/paste_controller.js new file mode 100644 index 000000000..79db7d7db --- /dev/null +++ b/app/javascript/controllers/paste_controller.js @@ -0,0 +1,28 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + pasteFiles(event) { + const editor = event.target.closest("house-md") + if (!editor) return + + const files = event.clipboardData?.files + if (!files?.length) return + + event.preventDefault() + + for (const file of files) { + const uploadEvent = new CustomEvent("house-md:before-upload", { + bubbles: true, + detail: { file }, + cancelable: true + }) + + if (editor.dispatchEvent(uploadEvent)) { + const upload = document.createElement("house-md-upload") + upload.file = file + upload.uploadsURL = editor.dataset.uploadsUrl + editor.appendChild(upload) + } + } + } +} \ No newline at end of file diff --git a/app/javascript/controllers/syntax_highlight_controller.js b/app/javascript/controllers/syntax_highlight_controller.js deleted file mode 100644 index 02b66a0f7..000000000 --- a/app/javascript/controllers/syntax_highlight_controller.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Controller } from "@hotwired/stimulus" -import { highlightAll } from "actiontext-lexical" - -export default class extends Controller { - connect() { - highlightAll() - } -} diff --git a/app/models/card.rb b/app/models/card.rb index 69588ea88..8707b2bf2 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -9,7 +9,7 @@ class Card < ApplicationRecord has_many :comments, dependent: :destroy has_one_attached :image, dependent: :purge_later - has_rich_text :description + has_markdown :description before_save :set_default_title, if: :published? diff --git a/app/models/comment.rb b/app/models/comment.rb index f62f457a0..ebb1229a4 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -5,7 +5,7 @@ class Comment < ApplicationRecord belongs_to :creator, class_name: "User", default: -> { Current.user } has_many :reactions, dependent: :delete_all - has_rich_text :body + has_markdown :body searchable_by :body_plain_text, using: :comments_search_index, as: :body scope :chronologically, -> { order created_at: :asc, id: :desc } @@ -19,10 +19,6 @@ class Comment < ApplicationRecord end private - def body_plain_text - body.to_plain_text - end - def watch_card_by_creator card.watch_by creator end diff --git a/app/models/concerns/mentions.rb b/app/models/concerns/mentions.rb index e8efb550c..08e0362cb 100644 --- a/app/models/concerns/mentions.rb +++ b/app/models/concerns/mentions.rb @@ -14,7 +14,7 @@ module Mentions end def mentionable_content - rich_text_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ") + markdown_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ") end private @@ -32,12 +32,12 @@ module Mentions collection.users end - def rich_text_associations - self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::RichText } + def markdown_associations + self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::Markdown } end def mentionable_content_changed? - rich_text_associations.any? { send(it.name).body_previously_changed? } + markdown_associations.any? { send(it.name).content_previously_changed? } end def create_mentions_later diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb deleted file mode 100644 index 6a35805e0..000000000 --- a/app/views/active_storage/blobs/_blob.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -<% if blob.representable? %> -
- <%= render "active_storage/blobs/web/representation", blob: blob %> - -
- <% if caption = blob.try(:caption) %> - <%= caption %> - <% else %> - <%= blob.filename %> - <% end %> - · - <%= number_to_human_size blob.byte_size %> - · - <%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__link", download: blob.filename, title: "Download #{blob.filename}" do %> - Download - <% end %> -
-
-<% else %> -
- <%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__figure", download: blob.filename, title: "Download #{blob.filename}" do %> - <%= render "active_storage/blobs/web/representation", blob: blob %> - <% end %> - -
-
- <% if caption = blob.try(:caption) %> - <%= caption %> - <% else %> - <%= blob.filename %> - <% end %> -
-
- <%= number_to_human_size blob.byte_size %> - · - <%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__link", download: blob.filename, title: "Download #{blob.filename}" do %> - Download - <% end %> -
-
-
-<% end %> diff --git a/app/views/active_storage/blobs/web/_representation.html.erb b/app/views/active_storage/blobs/web/_representation.html.erb deleted file mode 100644 index cd7400668..000000000 --- a/app/views/active_storage/blobs/web/_representation.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -<% size = local_assigns[:in_gallery] ? [800, 600] : [1024, 768] %> - -<% if blob.video? %> - <%= tag.video \ - src: rails_blob_path(blob), - controls: true, - preload: :none, - width: "100%", height: "100%" %> -<% elsif blob.audio? %> - -<% elsif blob.variable? %> - <%= image_tag url_for(blob.variant(loader: { n: -1 }, resize_to_limit: size)) %> -<% elsif blob.previewable? %> - <%= image_tag url_for(blob.preview(resize_to_limit: size)) %> -<% else %> - <%= blob.filename.extension&.downcase.presence || "unknown" %> -<% end %> diff --git a/app/views/cards/comments/_comment.html.erb b/app/views/cards/comments/_comment.html.erb index a1e863bf9..800c44bcd 100644 --- a/app/views/cards/comments/_comment.html.erb +++ b/app/views/cards/comments/_comment.html.erb @@ -28,8 +28,8 @@ <% end %>
-
- <%= comment.body %> +
+ <%= sanitize comment.body_html %>
<%= render "cards/comments/reactions/reactions", comment: comment %> diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb index 68659dd38..c4645fe86 100644 --- a/app/views/cards/comments/_new.html.erb +++ b/app/views/cards/comments/_new.html.erb @@ -4,13 +4,13 @@
-
+
<%= form_with model: Comment.new, url: card_comments_path(card), class: "flex flex-column gap full-width", - data: { controller: "form local-save", + data: { controller: "form paste local-save", local_save_key_value: "comment-#{card.id}", - action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %> - <%= form.rich_textarea :body, required: true, placeholder: new_comment_placeholder(card), - data: { local_save_target: "input", action: "actiontext:change->local-save#save turbo:morph-element->local-save#restoreContent" } %> + action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> + <%= form.markdown_area :body, class: "input comment__input", required: true, placeholder: new_comment_placeholder(card), + data: { local_save_target: "input", action: "house-md:change->local-save#save turbo:morph-element->local-save#restoreContent" } %> <%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %> Post this comment <% end %> diff --git a/app/views/cards/comments/edit.html.erb b/app/views/cards/comments/edit.html.erb index 726693c47..342afc590 100644 --- a/app/views/cards/comments/edit.html.erb +++ b/app/views/cards/comments/edit.html.erb @@ -5,10 +5,10 @@
-
+
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width", - data: { controller: "form", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %> - <%= form.rich_textarea :body, required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %> + data: { controller: "form paste", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> + <%= form.markdown_area :body, class: "input comment__input", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %>
<%= form.button class: "btn btn--reversed", type: :submit do %> Save changes diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index 8bbe68550..4e4c2c9f5 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -4,23 +4,23 @@

<%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>

-
- <%= card.description %> +
+ <%= sanitize card.description_html %>
<% end %> <% else %> - <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "auto-save" } do |form| %> + <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "paste auto-save" } do |form| %>

<%= form.text_area :title, placeholder: "Name it…", class: "input input--textarea full-width borderless txt-align-start", autofocus: card.title.blank?, data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>

-
- <%= form.rich_textarea :description, class: "card-field__description", +
+ <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "actiontext:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %> + data: { action: "house-md:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
<% end %> <% end %> diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index c02e2a635..499563bb4 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -1,5 +1,5 @@ <%= turbo_frame_tag @card, :edit do %> - <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "form" } do |form| %> + <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "form paste" } do |form| %>

<%= form.label :title, class: "flex flex-column align-center" do %> <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title", @@ -8,10 +8,10 @@ data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> <% end %>

- - <%= form.rich_textarea :description, class: "card-field__description rich-text-content", - placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %> +
+ <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + placeholder: "Add some notes, context, pictures, or video about this…", + data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
<%= form.button "Save changes", type: :submit, class: "btn" %> <%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %> diff --git a/app/views/events/event/_comment.html.erb b/app/views/events/event/_comment.html.erb index ed0cbb79c..22f4596e8 100644 --- a/app/views/events/event/_comment.html.erb +++ b/app/views/events/event/_comment.html.erb @@ -1,5 +1,5 @@ <%= render "events/event/layout", card: event.eventable.card, event: event do %> - <%= format_excerpt(event&.eventable.body, length: 200) -%> + <%= strip_tags(event&.eventable.body_html).truncate(200) -%> <% end %> diff --git a/app/views/layouts/action_text/contents/_content.html.erb b/app/views/layouts/action_text/contents/_content.html.erb deleted file mode 100644 index 0dec72775..000000000 --- a/app/views/layouts/action_text/contents/_content.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -
- <%= yield -%> -
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index b667bfb47..0e04cbca9 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -3,7 +3,7 @@ <%= page_title_tag %> - + diff --git a/config/importmap.rb b/config/importmap.rb index 2abe3a805..65dc7629c 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -5,11 +5,9 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js" pin "@hotwired/stimulus", to: "stimulus.min.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin "@rails/request.js", to: "@rails--request.js" # @0.0.11 +pin "house", to: "house.min.js" pin_all_from "app/javascript/controllers", under: "controllers" pin_all_from "app/javascript/helpers", under: "helpers" pin_all_from "app/javascript/initializers", under: "initializers" pin "marked" # @15.0.11 -pin "actiontext-lexical" -pin "@rails/activestorage", to: "activestorage.esm.js" -pin "@rails/actiontext", to: "actiontext.esm.js" diff --git a/config/initializers/markdown.rb b/config/initializers/markdown.rb new file mode 100644 index 000000000..04d5c8e78 --- /dev/null +++ b/config/initializers/markdown.rb @@ -0,0 +1,7 @@ +ActiveSupport.on_load :action_text_markdown do + require "markdown_renderer" + require "redcarpet/render_strip" + + ActionText::Markdown.html_renderer = ->(content) { MarkdownRenderer.build.render(content) } + ActionText::Markdown.plain_text_renderer = Redcarpet::Markdown.new(Redcarpet::Render::StripDown) +end diff --git a/db/migrate/20250523084734_create_action_text_tables.action_text.rb b/db/migrate/20250523084734_create_action_text_tables.action_text.rb deleted file mode 100644 index c1c0a9d8c..000000000 --- a/db/migrate/20250523084734_create_action_text_tables.action_text.rb +++ /dev/null @@ -1,26 +0,0 @@ -# This migration comes from action_text (originally 20180528164100) -class CreateActionTextTables < ActiveRecord::Migration[6.0] - def change - # Use Active Record's configured type for primary and foreign keys - primary_key_type, foreign_key_type = primary_and_foreign_key_types - - create_table :action_text_rich_texts, id: primary_key_type do |t| - t.string :name, null: false - t.text :body, size: :long - t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type - - t.timestamps - - t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true - end - end - - private - def primary_and_foreign_key_types - config = Rails.configuration.generators - setting = config.options[config.orm][:primary_key_type] - primary_key_type = setting || :primary_key - foreign_key_type = setting || :bigint - [ primary_key_type, foreign_key_type ] - end -end diff --git a/db/schema.rb b/db/schema.rb index 098674f3a..834b2bcc9 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.1].define(version: 2025_05_23_084734) do +ActiveRecord::Schema[8.1].define(version: 2025_05_15_125505) do create_table "accesses", force: :cascade do |t| t.integer "collection_id", null: false t.datetime "created_at", null: false @@ -39,16 +39,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_05_23_084734) do t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record" end - create_table "action_text_rich_texts", force: :cascade do |t| - t.text "body" - t.datetime "created_at", null: false - t.string "name", null: false - t.bigint "record_id", null: false - t.string "record_type", null: false - t.datetime "updated_at", null: false - t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true - end - create_table "active_storage_attachments", force: :cascade do |t| t.bigint "blob_id", null: false t.datetime "created_at", null: false diff --git a/db/schema_cache.yml b/db/schema_cache.yml index 6cd19a67c..fbaacdc9c 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -1,7 +1,7 @@ --- !ruby/object:ActiveRecord::ConnectionAdapters::SchemaCache columns: accesses: - - &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: collection_id cast_type: &1 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer @@ -80,7 +80,7 @@ columns: default_function: collation: comment: - - &25 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: user_id cast_type: *1 @@ -118,13 +118,13 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: content - cast_type: &11 !ruby/object:ActiveRecord::Type::Text + cast_type: &14 !ruby/object:ActiveRecord::Type::Text true: t false: f precision: scale: limit: - sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: TEXT type: :text limit: @@ -159,30 +159,17 @@ columns: collation: comment: - *9 - action_text_rich_texts: - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - auto_increment: - name: body - cast_type: *11 - sql_type_metadata: *12 - 'null': true - default: - default_function: - collation: - comment: - - *5 - - *6 - - *10 + active_storage_attachments: - &16 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: - name: record_id - cast_type: &14 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer + name: blob_id + cast_type: &11 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer precision: scale: limit: max: 9223372036854775808 min: -9223372036854775808 - sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: bigint type: :integer limit: @@ -193,23 +180,19 @@ columns: default_function: collation: comment: - - *13 - - *9 - active_storage_attachments: - - &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - *5 + - *6 + - *10 + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: - name: blob_id - cast_type: *14 - sql_type_metadata: *15 + name: record_id + cast_type: *11 + sql_type_metadata: *12 'null': false default: default_function: collation: comment: - - *5 - - *6 - - *10 - - *16 - *13 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -225,8 +208,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: byte_size - cast_type: *14 - sql_type_metadata: *15 + cast_type: *11 + sql_type_metadata: *12 'null': false default: default_function: @@ -264,7 +247,7 @@ columns: collation: comment: - *6 - - &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: key cast_type: *7 @@ -277,8 +260,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: metadata - cast_type: *11 - sql_type_metadata: *12 + cast_type: *14 + sql_type_metadata: *15 'null': true default: default_function: @@ -295,7 +278,7 @@ columns: collation: comment: active_storage_variant_records: - - *17 + - *16 - *6 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -309,7 +292,7 @@ columns: comment: ar_internal_metadata: - *5 - - *18 + - *17 - *9 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -322,7 +305,7 @@ columns: collation: comment: assignees_filters: - - &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: assignee_id cast_type: *1 @@ -332,7 +315,7 @@ columns: default_function: collation: comment: - - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: filter_id cast_type: *1 @@ -343,7 +326,7 @@ columns: collation: comment: assigners_filters: - - &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: assigner_id cast_type: *1 @@ -353,11 +336,11 @@ columns: default_function: collation: comment: - - *19 + - *18 assignments: + - *19 - *20 - - *21 - - &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: card_id cast_type: *1 @@ -385,14 +368,14 @@ columns: - *6 - *9 card_goldnesses: - - *22 + - *21 - *5 - *6 - *9 cards: - - *23 + - *22 - *5 - - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: creator_id cast_type: *1 @@ -445,14 +428,14 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: status - cast_type: *11 - sql_type_metadata: *12 + cast_type: *14 + sql_type_metadata: *15 'null': false default: creating default_function: collation: comment: - - &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: title cast_type: *7 @@ -478,7 +461,7 @@ columns: comment: - *9 closures: - - *22 + - *21 - *5 - *6 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column @@ -506,11 +489,11 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: all_access - cast_type: &32 !ruby/object:ActiveModel::Type::Boolean + cast_type: &31 !ruby/object:ActiveModel::Type::Boolean precision: scale: limit: - sql_type_metadata: &33 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &32 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: boolean type: :boolean limit: @@ -524,15 +507,15 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: auto_close_period - cast_type: *14 - sql_type_metadata: *15 + cast_type: *11 + sql_type_metadata: *12 'null': true default: default_function: collation: comment: - *5 - - *24 + - *23 - *6 - *10 - *9 @@ -547,18 +530,18 @@ columns: collation: comment: collections_filters: - - *23 - - *19 + - *22 + - *18 commands: - *5 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: data - cast_type: &26 !ruby/object:ActiveRecord::Type::Json + cast_type: &25 !ruby/object:ActiveRecord::Type::Json precision: scale: limit: - sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &26 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: json type: :json limit: @@ -573,8 +556,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: line - cast_type: *11 - sql_type_metadata: *12 + cast_type: *14 + sql_type_metadata: *15 'null': true default: default_function: @@ -601,16 +584,16 @@ columns: collation: comment: - *9 - - *25 - comments: - - *22 - - *5 - *24 + comments: + - *21 + - *5 + - *23 - *6 - *9 creators_filters: - - *24 - - *19 + - *23 + - *18 events: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -622,9 +605,9 @@ columns: default_function: collation: comment: - - *23 + - *22 - *5 - - *24 + - *23 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: eventable_id @@ -649,8 +632,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: particulars - cast_type: *26 - sql_type_metadata: *27 + cast_type: *25 + sql_type_metadata: *26 'null': true default: "{}" default_function: @@ -659,12 +642,12 @@ columns: - *9 filters: - *5 - - *24 + - *23 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: fields - cast_type: *26 - sql_type_metadata: *27 + cast_type: *25 + sql_type_metadata: *26 'null': false default: "{}" default_function: @@ -683,7 +666,7 @@ columns: comment: - *9 filters_stages: - - *19 + - *18 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: stage_id @@ -695,8 +678,8 @@ columns: collation: comment: filters_tags: - - *19 - - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - *18 + - &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: tag_id cast_type: *1 @@ -729,7 +712,7 @@ columns: default_function: collation: comment: - - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &27 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: source_id cast_type: *1 @@ -739,7 +722,7 @@ columns: default_function: collation: comment: - - &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: source_type cast_type: *7 @@ -773,16 +756,16 @@ columns: default_function: collation: comment: + - *27 - *28 - - *29 - *9 - - *25 + - *24 pins: - - *22 + - *21 - *5 - *6 - *9 - - *25 + - *24 reactions: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -899,24 +882,24 @@ columns: default_function: collation: comment: - - *25 + - *24 taggings: - - *22 + - *21 - *5 - *6 - - *30 + - *29 - *9 tags: - *5 - *6 - - *31 + - *30 - *9 users: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: active - cast_type: *32 - sql_type_metadata: *33 + cast_type: *31 + sql_type_metadata: *32 'null': false default: true default_function: @@ -957,16 +940,16 @@ columns: comment: - *9 watches: - - *22 + - *21 - *5 - *6 - *9 - - *25 + - *24 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: watching - cast_type: *32 - sql_type_metadata: *33 + cast_type: *31 + sql_type_metadata: *32 'null': false default: true default_function: @@ -1006,7 +989,6 @@ primary_keys: accesses: id accounts: id action_text_markdowns: id - action_text_rich_texts: id active_storage_attachments: id active_storage_blobs: id active_storage_variant_records: id @@ -1045,7 +1027,6 @@ data_sources: accesses: true accounts: true action_text_markdowns: true - action_text_rich_texts: true active_storage_attachments: true active_storage_blobs: true active_storage_variant_records: true @@ -1150,25 +1131,6 @@ indexes: nulls_not_distinct: comment: valid: true - action_text_rich_texts: - - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - table: action_text_rich_texts - name: index_action_text_rich_texts_uniqueness - unique: true - columns: - - record_type - - record_id - - name - lengths: {} - orders: {} - opclasses: {} - where: - type: - using: - include: - nulls_not_distinct: - comment: - valid: true active_storage_attachments: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: active_storage_attachments @@ -2199,4 +2161,4 @@ indexes: comment: valid: true workflows: [] -version: 20250523084734 +version: 20250515125505 diff --git a/lib/markdown_renderer.rb b/lib/markdown_renderer.rb new file mode 100644 index 000000000..e90e86fb4 --- /dev/null +++ b/lib/markdown_renderer.rb @@ -0,0 +1,81 @@ +require "rouge/plugins/redcarpet" + +class MarkdownRenderer < Redcarpet::Render::HTML + include Rouge::Plugins::Redcarpet + + def self.build + Redcarpet::Markdown.new MarkdownRenderer.new(filter_html: false), + autolink: true, highlight: true, no_intra_emphasis: true, + fenced_code_blocks: true, lax_spacing: true, strikethrough: true, tables: true + end + + def initialize(...) + super + @id_counts = Hash.new 0 + end + + def header(text, header_level) + unique_id(text).then do |id| + <<~HTML.chomp + + #{text} + + HTML + end + end + + def image(url, title, alt_text) + <<~HTML.chomp + + #{alt_text} + + HTML + end + + def code_block(code, language) + block_code(code, language) # call Rouge Redcarpet plugin + end + + def link(url, title, content) + return video_tag(url, title) if video_url?(url) + + attributes = { href: url } + attributes[:title] = title if title + attributes["data-turbo-frame"] = "_top" + + "#{content}" + end + + def autolink(url, link_type) + attributes = { href: url } + attributes["data-turbo-frame"] = "_top" + + "#{url}" + end + + private + attr_reader :id_counts + + def html_attributes(attributes) + attributes.map { |key, value| %Q(#{key}="#{value}") }.join(" ") + end + + 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 + + def video_url?(url) + File.extname(url).downcase.match?(/\.(mp4|webm|ogg|mov|avi|mkv)/) + end + + def video_tag(url, title) + <<~HTML.chomp + + HTML + 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..b792657d5 --- /dev/null +++ b/lib/rails_ext/action_text_has_markdown.rb @@ -0,0 +1,47 @@ +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}_html + #{name}.to_html + end + + def #{name}_plain_text + #{name}.to_plain_text + 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..cd19fa001 --- /dev/null +++ b/lib/rails_ext/action_text_markdown.rb @@ -0,0 +1,44 @@ +module ActionText + class Markdown < Record + mattr_accessor :html_renderer + mattr_accessor :plain_text_renderer + + belongs_to :record, polymorphic: true, touch: true + + def to_html + to_unsafe_html.html_safe + end + + def to_unsafe_html + if html_renderer.respond_to? :call + html_renderer.call content + else + html_renderer.render content + end + end + + def to_plain_text + if plain_text_renderer.respond_to? :call + plain_text_renderer.call content + else + plain_text_renderer.render content + end + 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..d662da7b2 --- /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: 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..d4d4be335 --- /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.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/script/convert_markdowns_to_rich_text.rb b/script/convert_markdowns_to_rich_text.rb deleted file mode 100755 index 1a43db239..000000000 --- a/script/convert_markdowns_to_rich_text.rb +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env ruby - -require_relative "../config/environment" -require "redcarpet" -require "nokogiri" - -class ActionText::Markdown < ApplicationRecord - belongs_to :record, polymorphic: true -end - -class MarkdownToActionTextConverter - ATTACHMENT_URL_REGEX = %r{/u/(?[^\/\s\)]+)} - - def initialize(html) - @doc = Nokogiri::HTML::DocumentFragment.parse(html) - @attachments = [] - end - - def convert - process_images - process_links - [ @doc.to_html, @attachments ] - end - - private - def process_images - @doc.css("img").each do |img| - src = img["src"].presence - if src && match = src.match(ATTACHMENT_URL_REGEX) - if (attachment = find_attachment(match[:slug])) - img.replace(build_attachment_node(attachment)) - @attachments << attachment - end - end - end - end - - def process_links - @doc.css("a").each do |link| - href = link["href"].presence - - if href && match = href.match(ATTACHMENT_URL_REGEX) - if (attachment = find_attachment(match[:slug])) - link.replace(build_attachment_node(attachment)) - @attachments << attachment - end - end - end - end - - def build_attachment_node(attachment) - html = ActionText::Attachment.from_attachable(attachment).to_html - fragment = Nokogiri::HTML::DocumentFragment.parse(html) - - node = fragment.at_css("action-text-attachment") - node["url"] = Rails.application.routes.url_helpers.rails_blob_path(attachment.blob, only_path: true) - - fragment - end - - def find_attachment(slug) - ActiveStorage::Attachment.find_by(slug: slug) - end -end - -class RedcarpetRenderer - def self.render(markdown) - renderer = Redcarpet::Render::HTML.new - markdowner = Redcarpet::Markdown.new(renderer, - autolink: true, - tables: true, - fenced_code_blocks: true, - strikethrough: true, - superscript: true, - ) - markdowner.render(markdown.to_s) - end -end - -ApplicationRecord.with_each_tenant do |tenant| - puts "Processing tenant: #{tenant}" - - ActionText::Markdown.find_each do |markdown| - next unless markdown.record - - html = RedcarpetRenderer.render(markdown.content.to_s) - converter = MarkdownToActionTextConverter.new(html) - rich_text_html, attachments = converter.convert - - rich_text = ActionText::RichText.create!( - name: markdown.name, - record: markdown.record, - body: rich_text_html - ) - - attachments.each do |attachment| - attachment.update!(record: rich_text) - end - - puts "✓ Created rich text for #{markdown.record_type}##{markdown.record_id} (#{markdown.name})" - rescue => e - warn "✗ Failed to process markdown ##{markdown.id}: #{e.class} - #{e.message}" - end -end diff --git a/script/migrate-to-multi-tenant b/script/migrate-to-multi-tenant index 79db5c0e0..8d035f37e 100755 --- a/script/migrate-to-multi-tenant +++ b/script/migrate-to-multi-tenant @@ -214,6 +214,9 @@ class EnactTenanting assert_count Tagging, Tagging.count assert_count Watch, Watch.count + # we're only copying the markdown records that are still accessible. + assert_count ActionText::Markdown, Comment.count + assert_count Event, Event.count assert_count Notification, Notification.count @@ -225,7 +228,22 @@ class EnactTenanting end def update_action_text_urls - raise "No more markdown" + update_counter["ActionText::Markdown"] = 0 + TENANTS.each do |tenant| + tenanted_domain = sprintf(TENANTED_DOMAIN, tenant: tenant) + + ApplicationRecord.with_tenant(tenant) do + ActionText::Markdown.all.each do |markdown| + content = markdown.content + next unless content =~ %r{#{CURRENT_DOMAIN}/u/} + + content.gsub!(%r{#{CURRENT_DOMAIN}/u/}, "#{tenanted_domain}/u/") + + markdown.update_column :content, content + update_counter["ActionText::Markdown"] += 1 + end + end + end end def hardlink_active_storage diff --git a/script/reset_cards_ids.rb b/script/reset_cards_ids.rb index 65c556283..19305b5f2 100644 --- a/script/reset_cards_ids.rb +++ b/script/reset_cards_ids.rb @@ -100,9 +100,9 @@ ApplicationRecord.with_each_tenant do |tenant| end # Update action_text_markdowns table (polymorphic relationship) - ActionText::RichText.where(record_type: "Card").find_each do |rich_text| - if id_mapping[rich_text.record_id] - rich_text.update_column(:record_id, id_mapping[rich_text.record_id]) + ActionText::Markdown.where(record_type: "Card").find_each do |markdown| + if id_mapping[markdown.record_id] + markdown.update_column(:record_id, id_mapping[markdown.record_id]) end end diff --git a/script/reset_collections_ids.rb b/script/reset_collections_ids.rb index cf1d84028..142e9e2a9 100644 --- a/script/reset_collections_ids.rb +++ b/script/reset_collections_ids.rb @@ -73,9 +73,9 @@ ApplicationRecord.with_each_tenant do |tenant| end # Update action_text_markdowns table (polymorphic relationship) - ActionText::RichText.where(record_type: "Collection").find_each do |rich_text| - if id_mapping[rich_text.record_id] - rich_text.update_column(:record_id, id_mapping[rich_text.record_id]) + ActionText::Markdown.where(record_type: "Collection").find_each do |markdown| + if id_mapping[markdown.record_id] + markdown.update_column(:record_id, id_mapping[markdown.record_id]) end end diff --git a/test/controllers/cards/comments_controller_test.rb b/test/controllers/cards/comments_controller_test.rb index 76ff0f6fa..690cf0656 100644 --- a/test/controllers/cards/comments_controller_test.rb +++ b/test/controllers/cards/comments_controller_test.rb @@ -17,11 +17,11 @@ class Cards::CommentsControllerTest < ActionDispatch::IntegrationTest put card_comment_path(cards(:logo), comments(:logo_agreement_kevin)), params: { comment: { body: "I've changed my mind" } } assert_response :success - assert_action_text "I've changed my mind", comments(:logo_agreement_kevin).reload.body + assert_equal "I've changed my mind", comments(:logo_agreement_kevin).reload.body.content end test "update another user's comment" do - assert_no_changes -> { comments(:logo_agreement_jz).reload.body.to_s } do + assert_no_changes "comments(:logo_agreement_jz).body.content" do put card_comment_path(cards(:logo), comments(:logo_agreement_jz)), params: { comment: { body: "I've changed my mind" } } end diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index 75ba45147..5cb3a6582 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -46,7 +46,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest assert_equal "moon.jpg", card.image.filename.to_s assert_equal [ tags(:mobile) ], card.tags - assert_equal "Something more in-depth", card.description.to_plain_text.strip + assert_equal "Something more in-depth", card.description_plain_text.strip end test "users can only see cards in collections they have access to" do diff --git a/test/controllers/uploads_controller_test.rb b/test/controllers/uploads_controller_test.rb new file mode 100644 index 000000000..3f5f54af5 --- /dev/null +++ b/test/controllers/uploads_controller_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +class UploadsControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + end + + test "create" do + assert_changes -> { ActiveStorage::Attachment.count }, 1 do + post uploads_path(format: "json"), params: { file: fixture_file_upload("moon.jpg", "image/jpeg") }, as: :xhr + end + + assert_response :success + assert_equal ActiveStorage::Attachment.last.slug_url(host: "#{ApplicationRecord.current_tenant}.example.com", port: nil), response.parsed_body["fileUrl"] + assert_equal "image/jpeg", response.parsed_body["mimetype"] + assert_equal "moon.jpg", response.parsed_body["fileName"] + end + + test "show" do + accounts("37s").uploads.attach fixture_file_upload("moon.jpg", "image/jpeg") + get upload_path(slug: accounts("37s").uploads.last.slug) + assert_response :redirect + assert_match /\/rails\/active_storage\/.*\/moon\.jpg/, @response.redirect_url + end +end diff --git a/test/fixtures/action_text/rich_texts.yml b/test/fixtures/action_text/markdowns.yml similarity index 69% rename from test/fixtures/action_text/rich_texts.yml rename to test/fixtures/action_text/markdowns.yml index 9a86c3b30..519cd408b 100644 --- a/test/fixtures/action_text/rich_texts.yml +++ b/test/fixtures/action_text/markdowns.yml @@ -1,16 +1,14 @@ logo_agreement_jz: record: logo_agreement_jz (Comment) name: body - body: I agree. + content: I agree. logo_agreement_kevin: record: logo_agreement_kevin (Comment) name: body - body: Same, let’s do it. + content: Same, let’s do it. layout_overflowing_david: record: layout_overflowing_david (Comment) name: body - body: The text is overflowing the container. - - + content: The text is overflowing the container. diff --git a/test/test_helper.rb b/test/test_helper.rb index 4e0a9fcee..6e27d82fa 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -22,7 +22,7 @@ module ActiveSupport fixtures :all include ActiveJob::TestHelper - include ActionTextTestHelper, CardTestHelper, ChangeTestHelper, SessionTestHelper + include CardTestHelper, ChangeTestHelper, SessionTestHelper end end diff --git a/test/test_helpers/action_text_test_helper.rb b/test/test_helpers/action_text_test_helper.rb deleted file mode 100644 index 09a26fceb..000000000 --- a/test/test_helpers/action_text_test_helper.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ActionTextTestHelper - def assert_action_text(expected_html, content) - assert_equal_html <<~HTML, content.to_s -
#{expected_html} - HTML - end - - def assert_equal_html(expected, actual) - assert_equal normalize_html(expected), normalize_html(actual) - end - - def normalize_html(html) - Nokogiri::HTML.fragment(html).tap do |fragment| - fragment.traverse do |node| - if node.text? - node.content = node.text.squish - end - end - end.to_html.strip - end -end diff --git a/vendor/javascript/house.min.js b/vendor/javascript/house.min.js new file mode 100644 index 000000000..c81e324e5 --- /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.#i()&&(this.innerHTML=this.#r())}#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})};#i(){return 0===this.children.length}#r(){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:r,endOffset:o,endContainer:l}=t,a=i(e,r,n);if(t.collapsed)return new s(e,a,a);const c=i(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,i=!1;for(;e.nextNode()&&(!s||!i);){const r=e.currentNode.textContent.length;n+r>=this.start&&!s&&(s=!0,t.setStart(e.currentNode,this.start-n)),n+r>=this.end&&!i&&(i=!0,t.setEnd(e.currentNode,this.end-n)),n+=r}return t}isEqual(t){return this.container===t.container&&this.start===t.start&&this.end===t.end}}function i(t,e,n){return e.nodeType===Node.ELEMENT_NODE?function(t,e,n){const s=e.childNodes[n];let i=0;const r=document.createTreeWalker(t);for(;r.nextNode();){if(r.currentNode===s)return i;r.currentNode.nodeType===Node.TEXT_NODE&&(i+=r.currentNode.textContent.length)}return i}(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 r{#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),i=-1===s?e.length:s+1;return{start:n,end:i,content:e.slice(n,i)}}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 r(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),i=`${s}${t}${this.content.slice(n)}`,r={start:s.length+t.length,end:s.length+t.length};this.#h(),this.#d(i,r)}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:i}=this.currentSelection,r=` ${n}[${t}](${e}) `;this.replaceText(r,s,i),this.selection.select({start:s+r.length,end:s+r.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.#b("> ")}toggleBulletList(){this.selection.isBulletList?this.#f("- "):this.#b("- ")}toggleNumberList(){this.selection.isNumberList?this.#f(/\d+\. /):this.#b(`${this.#L+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)+"",this.contentElement.classList.toggle("house-md-content-empty",this.isEmpty)}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.#w(e,n):this.#E(e,n)}#E(t,e=t){const{start:n,end:s,selection:i}=this.currentSelection,r=`${t}${i}${e}`;this.replaceText(r,n,s),this.selection.select({start:n,end:n+i.length+t.length+e.length})}#w(t,e=t){const{start:n,end:s,selection:i}=this.currentSelection;i.startsWith(t)&&i.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,i=s?"title":n;this.replaceText(`[${i}](url)`,t,e),s?this.selection.select({start:t+1,end:t+i.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.#L+1}. `,this.currentSelection.end+1,this.currentSelection.end+1))}#b(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#L(){const t=this.selection.previousLine().content.match(/^(\d+). /);return t?parseInt(t[1]):0}}class c{#A;constructor(t){this.#A=t}asPlainText(){const t=(new window.DOMParser).parseFromString(this.#A,"text/html");return this.#S(t.body).trim()}#S(t){return t.nodeType===Node.TEXT_NODE?t.nodeValue:Array.from(t.childNodes).map(((e,n)=>{const s=this.#k(e),i=n>0&&this.#k(t.childNodes[n-1]);return(s&&i?"\n\n":"")+this.#S(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 i=t.getTargetRanges()[0],{start:r,end:o}=s.fromDOMRange(i,e);n.call(this,t,r,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)}insertLineBreakHandler(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;static observedAttributes=["initialized"];constructor(){super(),this.internals=this.attachInternals(),this.internals.ariaRole="textbox"}attributeChangedCallback(t,e,n){"initialized"===t&&this.isConnected&&null!=e&&e!==n&&requestAnimationFrame((()=>this.#H()))}connectedCallback(){const t=this.querySelector(".house-md-content")?.textContent??this.textContent;this.document=new a(t,this),this.#y(),this.#N(),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.#_()}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.#D(),this.contentWrapper=document.createElement("div"),this.contentWrapper.classList.add("house-md-content"),this.contentWrapper.setAttribute("contenteditable",!0),this.contentWrapper.setAttribute("role","textbox"),this.contentWrapper.setAttribute("aria-multiline",!0),this.append(this.contentWrapper),this.getAttribute("tabindex")?(this.contentWrapper.setAttribute("tabindex",this.getAttribute("tabindex")),this.removeAttribute("tabindex")):this.contentWrapper.setAttribute("tabindex",0)}#D(){if(this.getAttribute("toolbar"))return document.getElementById(this.getAttribute("toolbar"));{const t=document.createElement("house-md-toolbar");return this.prepend(t),t}}#H(){this.disconnectedCallback(),this.getAttribute("toolbar")||this.toolbar.remove(),this.contentWrapper.remove(),this.connectedCallback()}#N(){this.addEventListener("beforeinput",this.#I),this.addEventListener("keydown",this.#B),this.addEventListener("dragover",this.#F),this.addEventListener("drop",this.#R),this.toolbar.addEventListener("house-md:toolbar-action",this.#M),document.addEventListener("selectionchange",this.#z),this.internals.form.addEventListener("submit",this.validate)}#_(){this.removeEventListener("beforeinput",this.#I),this.removeEventListener("keydown",this.#B),this.removeEventListener("dragover",this.#F),this.removeEventListener("drop",this.#R),this.toolbar.removeEventListener("house-md:toolbar-action",this.#M),document.removeEventListener("selectionchange",this.#z),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=({detail:t})=>{this.actionHandler.handleAction(t.houseMdAction,t)};#z=()=>{this.document.selection.update()}}customElements.define("house-md",g);class v extends HTMLElement{#W;#$;#O;constructor(){super(),this.percentComplete=0,this.status="pending"}connectedCallback(){this.#t(),this.#q(),this.addEventListener("click",this.#e)}disconnectedCallback(){this.removeEventListener("click",this.#e)}async#q(){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.#P,this.xhr.onload=this.#U,this.xhr.onerror=this.#K,this.xhr.onabort=this.#Q,this.xhr.send(t),this.status="uploading",this.#t()}#t(){this.setAttribute("status",this.status),this.innerHTML=`\n ${this.#V()}\n ${this.#X()}\n ${this.#j()}\n ${this.#Z()}\n `}#V(){return"failed"===this.status?"":""}#X(){return`
${this.file.name}
`}#j(){return`${this.percentComplete}%`}#Z(){return this.#O?`
${this.#O}
`:""}#P=t=>{t.lengthComputable&&(this.percentComplete=t.loaded/t.total*100),this.#t()};#U=async()=>{if(this.xhr.status>=400)this.status="failed",this.#O=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.#J()}this.#t()};#K=()=>{this.status="failed",this.#t()};#Q=()=>{this.status="aborted",this.#t()};#e=t=>{t.target.matches("[data-md-action=close]")&&this.remove()};async#J(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,r as Selection,n as Toolbar,v as Upload}; From fa04016cbe2384120fa0a73457361904f169b652 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 14:25:16 +0200 Subject: [PATCH 53/58] Revert "Revert "Replace House with Lexical"" --- .DS_Store | Bin 0 -> 6148 bytes Dockerfile | 2 +- Gemfile | 2 + Gemfile.lock | 22 ++ app/assets/stylesheets/_global.css | 11 + app/assets/stylesheets/actiontext-lexical.css | 113 ++++++ app/assets/stylesheets/cards.css | 7 + app/assets/stylesheets/comments.css | 23 +- app/assets/stylesheets/markdown-content.css | 110 ------ app/assets/stylesheets/rich-text-content.css | 331 ++++++++++++++++++ app/helpers/excerpt_helper.rb | 12 + app/helpers/notifications_helper.rb | 2 +- app/javascript/application.js | 4 +- .../controllers/local_save_controller.js | 7 +- .../controllers/paste_controller.js | 28 -- .../syntax_highlight_controller.js | 8 + app/models/card.rb | 2 +- app/models/comment.rb | 6 +- app/models/concerns/mentions.rb | 8 +- app/views/active_storage/blobs/_blob.html.erb | 42 +++ .../blobs/web/_representation.html.erb | 19 + app/views/cards/comments/_comment.html.erb | 4 +- app/views/cards/comments/_new.html.erb | 10 +- app/views/cards/comments/edit.html.erb | 6 +- app/views/cards/container/_title.html.erb | 12 +- app/views/cards/edit.html.erb | 10 +- app/views/events/event/_comment.html.erb | 2 +- .../action_text/contents/_content.html.erb | 3 + app/views/layouts/application.html.erb | 2 +- config/importmap.rb | 4 +- config/initializers/markdown.rb | 7 - ...4_create_action_text_tables.action_text.rb | 26 ++ db/schema.rb | 12 +- db/schema_cache.yml | 194 +++++----- lib/markdown_renderer.rb | 81 ----- lib/rails_ext/action_text_has_markdown.rb | 47 --- lib/rails_ext/action_text_markdown.rb | 44 --- lib/rails_ext/action_text_tag_helper.rb | 36 -- lib/rails_ext/active_storage_sluggable.rb | 32 -- script/convert_markdowns_to_rich_text.rb | 104 ++++++ script/migrate-to-multi-tenant | 20 +- script/reset_cards_ids.rb | 6 +- script/reset_collections_ids.rb | 6 +- .../cards/comments_controller_test.rb | 4 +- test/controllers/cards_controller_test.rb | 2 +- test/controllers/uploads_controller_test.rb | 25 -- .../{markdowns.yml => rich_texts.yml} | 8 +- test/test_helper.rb | 2 +- test/test_helpers/action_text_test_helper.rb | 21 ++ vendor/javascript/house.min.js | 1 - 50 files changed, 915 insertions(+), 575 deletions(-) create mode 100644 .DS_Store create mode 100644 app/assets/stylesheets/actiontext-lexical.css delete mode 100644 app/assets/stylesheets/markdown-content.css create mode 100644 app/assets/stylesheets/rich-text-content.css create mode 100644 app/helpers/excerpt_helper.rb delete mode 100644 app/javascript/controllers/paste_controller.js create mode 100644 app/javascript/controllers/syntax_highlight_controller.js create mode 100644 app/views/active_storage/blobs/_blob.html.erb create mode 100644 app/views/active_storage/blobs/web/_representation.html.erb create mode 100644 app/views/layouts/action_text/contents/_content.html.erb delete mode 100644 config/initializers/markdown.rb create mode 100644 db/migrate/20250523084734_create_action_text_tables.action_text.rb delete mode 100644 lib/markdown_renderer.rb delete mode 100644 lib/rails_ext/action_text_has_markdown.rb delete mode 100644 lib/rails_ext/action_text_markdown.rb delete mode 100644 lib/rails_ext/action_text_tag_helper.rb delete mode 100644 lib/rails_ext/active_storage_sluggable.rb create mode 100755 script/convert_markdowns_to_rich_text.rb delete mode 100644 test/controllers/uploads_controller_test.rb rename test/fixtures/action_text/{markdowns.yml => rich_texts.yml} (69%) create mode 100644 test/test_helpers/action_text_test_helper.rb delete mode 100644 vendor/javascript/house.min.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 1.14" gem "platform_agent" # Telemetry and logging diff --git a/Gemfile.lock b/Gemfile.lock index 5b9940aad..0ed3a138e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,10 @@ +GIT + remote: https://github.com/basecamp/actiontext-lexical + revision: 777b2a4a933db9cb4cd515c4569dfcaf8cae45b5 + specs: + actiontext-lexical (0.1.0) + rails (>= 8.0.2) + GIT remote: https://github.com/basecamp/active_record-tenanted revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b @@ -188,6 +195,10 @@ GEM net-http (>= 0.5.0) faraday-retry (2.3.1) faraday (~> 2.0) + ffi (1.17.2) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -199,6 +210,9 @@ GEM hashdiff (1.1.2) i18n (1.14.7) concurrent-ruby (~> 1.0) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) importmap-rails (2.1.0) actionpack (>= 6.0.0) activesupport (>= 6.0.0) @@ -236,6 +250,9 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + mini_magick (5.2.0) + benchmark + logger mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.25.5) @@ -354,6 +371,9 @@ GEM rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) + ruby-vips (2.2.3) + ffi (~> 1.12) + logger rubyzip (2.4.1) securerandom (0.4.1) selenium-webdriver (4.32.0) @@ -448,6 +468,7 @@ PLATFORMS x86_64-mingw32 DEPENDENCIES + actiontext-lexical! active_record-tenanted! bcrypt (~> 3.1.7) bootsnap @@ -456,6 +477,7 @@ DEPENDENCIES capybara debug geared_pagination (~> 1.2) + image_processing (~> 1.14) importmap-rails jbuilder kamal diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index aad4508fc..5b2a966af 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -177,6 +177,17 @@ --color-card-7: oklch(var(--lch-purple-medium)); --color-card-8: oklch(var(--lch-pink-medium)); + /* Colors: Syntax highlighting */ + --color-code-token__att: oklch(var(--lch-blue-dark)); + --color-code-token__comment: oklch(var(--lch-ink-medium)); + --color-code-token__function: oklch(var(--lch-purple-dark)); + --color-code-token__operator: oklch(var(--lch-red-dark)); + --color-code-token__property: oklch(var(--lch-purple-dark)); + --color-code-token__punctuation: oklch(var(--lch-ink-dark)); + --color-code-token__selector: oklch(var(--lch-green-dark)); + --color-code-token__variable: oklch(var(--lch-red-dark)); + + @media (prefers-color-scheme: dark) { --lch-canvas: 20% 0.0195 232.58; --lch-ink-inverted: var(--lch-black); diff --git a/app/assets/stylesheets/actiontext-lexical.css b/app/assets/stylesheets/actiontext-lexical.css new file mode 100644 index 000000000..c0ab455a5 --- /dev/null +++ b/app/assets/stylesheets/actiontext-lexical.css @@ -0,0 +1,113 @@ +lexical-editor { + .node--selected { + &:not(:has(img)) { + box-shadow: 0 0 0 var(--hover-size) var(--hover-color); + } + + &:has(img) { + img { + box-shadow: 0 0 0 var(--hover-size) var(--hover-color); + } + } + } +} + +.lexical-dialog-actions { + display: flex; + font-size: var(--text-x-small); + flex: 1 1 0%; + gap: var(--inline-space-half); + margin-block-start: var(--block-space-half); + + .btn { + --radius: 0.3em; + + inline-size: 100%; + justify-content: center; + } + + span { + inline-size: 100%; + } +} + +.lexical-editor__content { + --outline-size: max(2px, 0.08em); + + margin: var(--block-space-half) 0; + min-block-size: calc(7lh + var(--block-space)); + outline: 0; + padding: var(--block-space-half) var(--inline-space); + + * { + &:first-child { + margin-block-start: 0; + } + + &:last-child { + margin-block-end: 0; + } + } +} + +.lexical-editor--drag-over { + background-color: var(--color-selected); + border-radius: 4px; + outline: 2px dashed var(--color-selected-dark); +} + +lexical-toolbar { + background-color: inherit; + border-block-end: 1px solid var(--color-ink-light); + color: currentColor; + display: flex; + font-size: inherit; + margin: 0; + max-inline-size: 100%; + padding: 0.2em 0; + position: relative; + + dialog { + background-color: var(--color-canvas); + border: 1px solid var(--color-ink-lighter); + border-radius: 0.5em; + box-shadow: var(--shadow); + color: var(--color-ink); + padding: var(--block-space) calc(var(--inline-space) * 1.5); + position: absolute; + } + + > button { + aspect-ratio: 4/3.5; + appearance: none; + background-color: transparent; + block-size: 2em; + border: none; + border-radius: 0.2em; + 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.85em; + fill: currentColor; + grid-area: 1/1; + inline-size: auto; + user-select: none; + } + + &:is(:focus, :hover) { + background-color: var(--color-ink-lighter); + box-shadow: none; + } + + &:is(:active) { + background-color: var(--color-selected); + } + } +} diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 4e3c237f7..d13b60498 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -150,6 +150,13 @@ } } + .card-field__description { + lexical-toolbar { + border-block-start: 1px solid var(--color-ink-light); + margin-block-start: var(--block-margin) !important; + } + } + .card__content { flex: 2 1 auto; max-inline-size: 100%; diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index 2a5e47960..05d3518bc 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -1,7 +1,7 @@ @layer components { .comments { --avatar-size: 2.33em; - --comment-padding-block: var(--block-space); + --comment-padding-block: var(--block-space-half); --comment-padding-inline: var(--inline-space-double); --comment-max: 70ch; --reaction-size: 2rem; @@ -47,7 +47,7 @@ } .comment__avatar { - margin: calc(var(--comment-padding-block) * 0.5) calc(var(--comment-padding-inline) * -0.75); + margin: calc(var(--comment-padding-block) * 0.75) calc(var(--comment-padding-inline) * -0.75); z-index: 0; .comment--mine_ & { @@ -64,7 +64,7 @@ text-align: center; } - p { + * { &:first-child { margin-block-start: 0; } @@ -83,6 +83,8 @@ border-radius: 0.2em; padding: var(--comment-padding-block) + var(--comment-padding-inline) + calc(var(--comment-padding-block) * 1.5) var(--comment-padding-inline); word-wrap: break-word; } @@ -113,7 +115,7 @@ line-height: 1.2; max-inline-size: var(--comment-max); text-align: center; - + &::before { /* Make up space for lack of avatar */ @@ -133,17 +135,4 @@ text-align: center !important; } } - - .comment__input { - --input-border-size: 0; - --input-padding: 0; - - min-block-size: calc(9lh + (2 * var(--comment-padding-block))); - min-inline-size: 100%; - - @supports (field-sizing: content) { - field-sizing: content; - min-block-size: calc(9lh + (2 * var(--comment-padding-block))); - } - } } diff --git a/app/assets/stylesheets/markdown-content.css b/app/assets/stylesheets/markdown-content.css deleted file mode 100644 index 81589501b..000000000 --- a/app/assets/stylesheets/markdown-content.css +++ /dev/null @@ -1,110 +0,0 @@ -@layer components { - .markdown-content { - :where(h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6) { - font-weight: 800; - hyphens: auto; - letter-spacing: -0.02ch; - line-height: 1.1; - margin-block: 0; - overflow-wrap: break-word; - text-wrap: balance; - } - - :where(h1, h2, h3, h4, h5, h6) { display: block; } - :where(h1, h2, h3, h4, h5, h6) { margin-block: 0.65em; } - - :is(h1, .h1) { font-size: 2em; } - :is(h2, .h2) { font-size: 1.5em; } - :is(h3, .h3) { font-size: 1.17em; } - :is(h4, .h4) { font-size: 1em; } - :is(h5, .h5) { font-size: 0.83em; } - :is(h6, .h6) { font-size: 0.67em; } - - :where(p, ul, ol, dl, blockquote, pre, figure, table, hr) { - margin-block: 0.65lh; - overflow-wrap: break-word; - text-wrap: pretty; - } - - :where(hr) { - border-color: var(--color-ink-medium); - border-style: var(--border-style, solid) none none; - margin: 2lh auto; - } - - :where(strong) { - font-weight: 700; - } - - :is(pre, code, .pre, .code) { - background-color: var(--color-ink-lightest); - border: 1px solid var(--color-ink-lighter); - border-radius: 0.3em; - font-family: var(--font-mono); - font-size: 0.85em; - } - - :is(pre) { - overflow-x: auto; - /* text-wrap: nowrap; */ - } - - :where(code) { - padding: 0.1em 0.3em; - } - - :is(pre, .pre) { - border-radius: 0.5em; - padding: 0.5lh 2ch; - - :is(code, .code) { - background-color: transparent; - border: 0; - font-size: 1em; - padding: 0; - } - } - - :where(p) { - hyphens: auto; - letter-spacing: -0.005ch; - } - - :is(blockquote, .quote) { - font-style: italic; - margin: 0 3ch; - - :is(p) { - hyphens: none; - } - } - - :where(table, th, td) { - border: 1px solid var(--color-ink-medium); - } - - :where(table) { - border-collapse: collapse; - margin: 1lh 0; - } - - :where(th, td) { - padding: 0.2lh 1ch; - text-align: start; - } - - :where(th) { - border-block-end-width: 3px; - font-weight: 700; - } - - :where(img, video, embed, object) { - max-block-size: 32rem; - - /* Links should hug media contained within */ - a:has(&) { - display: inline-block; - } - } - } -} diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css new file mode 100644 index 000000000..52254f84d --- /dev/null +++ b/app/assets/stylesheets/rich-text-content.css @@ -0,0 +1,331 @@ +@layer components { + .rich-text-content { + --block-margin: 0.65lh; + + :where(h1, h2, h3, h4, h5, h6) { + display: block; + font-weight: 800; + hyphens: auto; + letter-spacing: -0.02ch; + line-height: 1.1; + margin-block: var(--block-margin); + overflow-wrap: break-word; + text-wrap: balance; + } + + :where(h1) { font-size: 2em; } + :where(h2) { font-size: 1.5em; } + :where(h3) { font-size: 1.17em; } + :where(h4) { font-size: 1em; } + :where(h5) { font-size: 0.83em; } + :where(h6) { font-size: 0.67em; } + + :where(p, ul, ol, dl, blockquote, figure, .attachment) { + margin-block: var(--block-margin); + overflow-wrap: break-word; + text-wrap: pretty; + } + + :where(b, strong) { + font-weight: 700; + } + + :where(p, blockquote) { + hyphens: auto; + letter-spacing: -0.005ch; + } + + :where(blockquote) { + border-inline-start: 0.25em solid var(--color-ink-lighter); + font-style: italic; + margin: var(--block-margin) 0; + padding: 0.5lh 2ch; + } + + :where(img, video, embed, object) { + margin-inline: auto; + max-block-size: 32rem; + + /* Links should hug media contained within */ + a:has(&) { + display: inline-block; + } + } + + /* Attachments */ + .attachment { + block-size: auto; + display: inline-block; + inline-size: 100%; + position: relative; + margin-inline: 0; + max-inline-size: 100%; + + &:is(figure) img { + block-size: auto; + display: block; + margin-inline: auto; + max-inline-size: 100%; + user-select: none; + } + + :where(progress) { + inline-size: 100%; + margin: auto; + } + } + + .attachment__caption { + color: var(--color-ink-dark); + display: flex; + font-size: var(--text-small); + flex-wrap: wrap; + line-height: 1.3; + column-gap: 0.5ch; + justify-content: center; + margin: var(--block-space-half) var(--inline-space); + text-align: center; + + .input { + inline-size: 100%; + max-inline-size: 100%; + text-align: center; + + @supports (field-sizing: content) { + field-sizing: content; + inline-size: auto; + min-inline-size: 20ch; + } + } + } + + .attachment__figure { + --hover-size: 0; + } + + .attachment--file { + align-items: center; + display: inline-flex; + + .attachment__caption { + flex-direction: column; + flex-wrap: nowrap; + justify-content: flex-start; + text-align: start; + + strong { + color: var(--color-ink); + } + } + } + + .attachment__icon { + aspect-ratio: 4/5; + background-color: var(--color-canvas); + block-size: calc(2.5lh + 0.5ch); + border: 2px solid var(--color-ink-medium); + border-radius: 0.3em; + color: var(--color-ink-dark); + display: inline-grid; + font-size: var(--text-small); + font-weight: 800; + inline-size: auto; + overflow: clip; + padding-block-start: 0.4lh; + place-content: center; + position: relative; + text-transform: uppercase; + + &::before { + background-color: var(--color-ink-medium); + block-size: 0.5lh; + content: ""; + display: block; + inline-size: 100%; + inset: 0 0 auto; + position: absolute; + } + + .attachment--psd &, + .attachment--key &, + .attachment--sketch &, + .attachment--ai &, + .attachment--eps &, + .attachment--indd &, + .attachment--svg &, + .attachment--ppt &, + .attachment--pptx & { + color: oklch(var(--lch-red-dark)); + background-color: oklch(var(--lch-red-lightest)); + border-color: oklch(var(--lch-red-medium)); + + &::before { + background-color: oklch(var(--lch-red-medium)); + } + } + + .attachment--css &, + .attachment--php &, + .attachment--json &, + .attachment--htm &, + .attachment--html &, + .attachment--rb &, + .attachment--erb &, + .attachment--ts &, + .attachment--js & { + color: oklch(var(--lch-purple-dark)); + background-color: oklch(var(--lch-purple-lightest)); + border-color: oklch(var(--lch-purple-medium)); + + &::before { + background-color: oklch(var(--lch-purple-medium)); + } + } + + .attachment--txt &, + .attachment--pages &, + .attachment--rtf &, + .attachment--md &, + .attachment--doc &, + .attachment--docx & { + color: oklch(var(--lch-blue-dark)); + background-color: oklch(var(--lch-blue-lightest)); + border-color: oklch(var(--lch-blue-medium)); + + &::before { + background-color: oklch(var(--lch-blue-medium)); + } + } + + .attachment--csv &, + .attachment--numbers &, + .attachment--xls &, + .attachment--xlsx & { + color: oklch(var(--lch-green-dark)); + background-color: oklch(var(--lch-green-lightest)); + border-color: oklch(var(--lch-green-medium)); + + &::before { + background-color: oklch(var(--lch-green-medium)); + } + } + } + + .attachment__link { + --hover-size: 0; + + color: var(--color-link); + text-decoration: underline; + } + + /* Attachment junk gets wrapped in a paragraph causing unwanted space */ + p:has(action-text-attachment) { + display: none; + + + * { + margin-block-start: 0; + } + } + + /* Avoid extra space due to empty paragraphs */ + p:empty { + display: none; + } + + /* Code */ + :where(code, pre) { + background-color: var(--color-canvas); + border: 1px solid var(--color-ink-lighter); + border-radius: 0.3em; + color: var(--color-ink); + font-family: var(--font-mono); + font-size: 0.85em; + font-weight: 500; + margin-block: var(--block-margin); + padding: 0.1em 0.3em; + + &[data-language] { + border-radius: 0.5em; + display: block; + hyphens: none; + overflow-x: auto; + padding: 0.5lh 2ch; + tab-size: 2; + text-wrap: nowrap; + white-space: pre; + word-break: break-word; + + /* Keywords and attributes */ + .code-token__attr, + .token.atrule, + .token.attr, + .token.keyword { + color: var(--color-code-token__att); + } + + /* Constants, booleans, numbers, properties, tags */ + .code-token__property, + .token.boolean, + .token.constant, + .token.deleted, + .token.number, + .token.property, + .token.symbol, + .token.tag { + color: var(--color-code-token__property); + } + + /* Strings, selectors, and built-in constructs */ + .code-token__selector, + .token.builtin, + .token.char, + .token.inserted, + .token.selector, + .token.string { + color: var(--color-code-token__selector); + } + + /* Comments and meta information */ + .code-token__comment, + .token.cdata, + .token.comment, + .token.doctype, + .token.prolog { + color: var(--color-code-token__comment); + font-style: italic; + } + + /* Operators and symbolic entities */ + .code-token__operator, + .token.entity, + .token.operator, + .token.url { + color: var(--color-code-token__operator); + } + + /* Functions and class names */ + .code-token__function, + .token.class, + .token.class-name, + .token.function { + color: var(--color-code-token__function); + } + + /* Variables, regex, namespaces, important */ + .code-token__variable, + .token.important, + .token.namespace, + .token.regex, + .token.variable { + color: var(--color-code-token__variable); + } + + /* Punctuation */ + .code-token__punctuation, + .token.punctuation { + color: var(--color-code-token__punctuation); + } + } + } + } +} diff --git a/app/helpers/excerpt_helper.rb b/app/helpers/excerpt_helper.rb new file mode 100644 index 000000000..9ae0d3274 --- /dev/null +++ b/app/helpers/excerpt_helper.rb @@ -0,0 +1,12 @@ +module ExcerptHelper + def format_excerpt(content, length: 200) + return "" if content.blank? + + text = content.to_plain_text + text = text.gsub(/^>\s*(.*)$/m, '> \1') + text = text.gsub(/^[-*]\s*(.*)$/m, '• \1') + text = text.gsub(/^\d+\.\s*(.*)$/m) { |m| m } + text = text.gsub(/\s+/, " ").strip + text.truncate(length) + end +end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 57fd63eff..8dab7c110 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -58,7 +58,7 @@ module NotificationsHelper def comment_notification_body(event) comment = event.eventable - "#{strip_tags(comment.body_html).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body_html).truncate(200)}" + "#{strip_tags(comment.body.to_s).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body.to_s).truncate(200)}" end def card_notification_title(card) diff --git a/app/javascript/application.js b/app/javascript/application.js index c2917aa3e..36d861cc7 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -2,4 +2,6 @@ import "@hotwired/turbo-rails" import "initializers" import "controllers" -import "house" + +import "actiontext-lexical" +import "@rails/actiontext" diff --git a/app/javascript/controllers/local_save_controller.js b/app/javascript/controllers/local_save_controller.js index 5d9b89a72..5f9fc83a3 100644 --- a/app/javascript/controllers/local_save_controller.js +++ b/app/javascript/controllers/local_save_controller.js @@ -30,9 +30,10 @@ export default class extends Controller { async restoreContent() { await nextFrame() - const savedContent = localStorage.getItem(this.keyValue) + let savedContent = localStorage.getItem(this.keyValue) if (savedContent) { + savedContent = `
${savedContent}
` // temporary for old markdown saves this.inputTarget.value = savedContent this.#triggerChangeEvent(savedContent) } @@ -45,8 +46,8 @@ export default class extends Controller { } #triggerChangeEvent(newValue) { - if (this.inputTarget.tagName === "HOUSE-MD") { - this.inputTarget.dispatchEvent(new CustomEvent('house-md:change', { + if (this.inputTarget.tagName === "LEXICAL-EDITOR") { + this.inputTarget.dispatchEvent(new CustomEvent('actiontext:change', { bubbles: true, detail: { previousContent: '', diff --git a/app/javascript/controllers/paste_controller.js b/app/javascript/controllers/paste_controller.js deleted file mode 100644 index 79db7d7db..000000000 --- a/app/javascript/controllers/paste_controller.js +++ /dev/null @@ -1,28 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - pasteFiles(event) { - const editor = event.target.closest("house-md") - if (!editor) return - - const files = event.clipboardData?.files - if (!files?.length) return - - event.preventDefault() - - for (const file of files) { - const uploadEvent = new CustomEvent("house-md:before-upload", { - bubbles: true, - detail: { file }, - cancelable: true - }) - - if (editor.dispatchEvent(uploadEvent)) { - const upload = document.createElement("house-md-upload") - upload.file = file - upload.uploadsURL = editor.dataset.uploadsUrl - editor.appendChild(upload) - } - } - } -} \ No newline at end of file diff --git a/app/javascript/controllers/syntax_highlight_controller.js b/app/javascript/controllers/syntax_highlight_controller.js new file mode 100644 index 000000000..02b66a0f7 --- /dev/null +++ b/app/javascript/controllers/syntax_highlight_controller.js @@ -0,0 +1,8 @@ +import { Controller } from "@hotwired/stimulus" +import { highlightAll } from "actiontext-lexical" + +export default class extends Controller { + connect() { + highlightAll() + } +} diff --git a/app/models/card.rb b/app/models/card.rb index 8707b2bf2..69588ea88 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -9,7 +9,7 @@ class Card < ApplicationRecord has_many :comments, dependent: :destroy has_one_attached :image, dependent: :purge_later - has_markdown :description + has_rich_text :description before_save :set_default_title, if: :published? diff --git a/app/models/comment.rb b/app/models/comment.rb index ebb1229a4..f62f457a0 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -5,7 +5,7 @@ class Comment < ApplicationRecord belongs_to :creator, class_name: "User", default: -> { Current.user } has_many :reactions, dependent: :delete_all - has_markdown :body + has_rich_text :body searchable_by :body_plain_text, using: :comments_search_index, as: :body scope :chronologically, -> { order created_at: :asc, id: :desc } @@ -19,6 +19,10 @@ class Comment < ApplicationRecord end private + def body_plain_text + body.to_plain_text + end + def watch_card_by_creator card.watch_by creator end diff --git a/app/models/concerns/mentions.rb b/app/models/concerns/mentions.rb index 08e0362cb..e8efb550c 100644 --- a/app/models/concerns/mentions.rb +++ b/app/models/concerns/mentions.rb @@ -14,7 +14,7 @@ module Mentions end def mentionable_content - markdown_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ") + rich_text_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ") end private @@ -32,12 +32,12 @@ module Mentions collection.users end - def markdown_associations - self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::Markdown } + def rich_text_associations + self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::RichText } end def mentionable_content_changed? - markdown_associations.any? { send(it.name).content_previously_changed? } + rich_text_associations.any? { send(it.name).body_previously_changed? } end def create_mentions_later diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb new file mode 100644 index 000000000..6a35805e0 --- /dev/null +++ b/app/views/active_storage/blobs/_blob.html.erb @@ -0,0 +1,42 @@ +<% if blob.representable? %> +
+ <%= render "active_storage/blobs/web/representation", blob: blob %> + +
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <% end %> + · + <%= number_to_human_size blob.byte_size %> + · + <%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__link", download: blob.filename, title: "Download #{blob.filename}" do %> + Download + <% end %> +
+
+<% else %> +
+ <%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__figure", download: blob.filename, title: "Download #{blob.filename}" do %> + <%= render "active_storage/blobs/web/representation", blob: blob %> + <% end %> + +
+
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <% end %> +
+
+ <%= number_to_human_size blob.byte_size %> + · + <%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__link", download: blob.filename, title: "Download #{blob.filename}" do %> + Download + <% end %> +
+
+
+<% end %> diff --git a/app/views/active_storage/blobs/web/_representation.html.erb b/app/views/active_storage/blobs/web/_representation.html.erb new file mode 100644 index 000000000..cd7400668 --- /dev/null +++ b/app/views/active_storage/blobs/web/_representation.html.erb @@ -0,0 +1,19 @@ +<% size = local_assigns[:in_gallery] ? [800, 600] : [1024, 768] %> + +<% if blob.video? %> + <%= tag.video \ + src: rails_blob_path(blob), + controls: true, + preload: :none, + width: "100%", height: "100%" %> +<% elsif blob.audio? %> + +<% elsif blob.variable? %> + <%= image_tag url_for(blob.variant(loader: { n: -1 }, resize_to_limit: size)) %> +<% elsif blob.previewable? %> + <%= image_tag url_for(blob.preview(resize_to_limit: size)) %> +<% else %> + <%= blob.filename.extension&.downcase.presence || "unknown" %> +<% end %> diff --git a/app/views/cards/comments/_comment.html.erb b/app/views/cards/comments/_comment.html.erb index 800c44bcd..a1e863bf9 100644 --- a/app/views/cards/comments/_comment.html.erb +++ b/app/views/cards/comments/_comment.html.erb @@ -28,8 +28,8 @@ <% end %>
-
- <%= sanitize comment.body_html %> +
+ <%= comment.body %>
<%= render "cards/comments/reactions/reactions", comment: comment %> diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb index c4645fe86..68659dd38 100644 --- a/app/views/cards/comments/_new.html.erb +++ b/app/views/cards/comments/_new.html.erb @@ -4,13 +4,13 @@
-
+
<%= form_with model: Comment.new, url: card_comments_path(card), class: "flex flex-column gap full-width", - data: { controller: "form paste local-save", + data: { controller: "form local-save", local_save_key_value: "comment-#{card.id}", - action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> - <%= form.markdown_area :body, class: "input comment__input", required: true, placeholder: new_comment_placeholder(card), - data: { local_save_target: "input", action: "house-md:change->local-save#save turbo:morph-element->local-save#restoreContent" } %> + action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %> + <%= form.rich_textarea :body, required: true, placeholder: new_comment_placeholder(card), + data: { local_save_target: "input", action: "actiontext:change->local-save#save turbo:morph-element->local-save#restoreContent" } %> <%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %> Post this comment <% end %> diff --git a/app/views/cards/comments/edit.html.erb b/app/views/cards/comments/edit.html.erb index 342afc590..726693c47 100644 --- a/app/views/cards/comments/edit.html.erb +++ b/app/views/cards/comments/edit.html.erb @@ -5,10 +5,10 @@
-
+
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width", - data: { controller: "form paste", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> - <%= form.markdown_area :body, class: "input comment__input", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %> + data: { controller: "form", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %> + <%= form.rich_textarea :body, required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %>
<%= form.button class: "btn btn--reversed", type: :submit do %> Save changes diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index 4e4c2c9f5..8bbe68550 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -4,23 +4,23 @@

<%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>

-
- <%= sanitize card.description_html %> +
+ <%= card.description %>
<% end %> <% else %> - <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "paste auto-save" } do |form| %> + <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "auto-save" } do |form| %>

<%= form.text_area :title, placeholder: "Name it…", class: "input input--textarea full-width borderless txt-align-start", autofocus: card.title.blank?, data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>

-
- <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", +
+ <%= form.rich_textarea :description, class: "card-field__description", placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "house-md:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> + data: { action: "actiontext:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
<% end %> <% end %> diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index 499563bb4..c02e2a635 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -1,5 +1,5 @@ <%= turbo_frame_tag @card, :edit do %> - <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "form paste" } do |form| %> + <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "form" } do |form| %>

<%= form.label :title, class: "flex flex-column align-center" do %> <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title", @@ -8,10 +8,10 @@ data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> <% end %>

-
- <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", - placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> + + <%= form.rich_textarea :description, class: "card-field__description rich-text-content", + placeholder: "Add some notes, context, pictures, or video about this…", + data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
<%= form.button "Save changes", type: :submit, class: "btn" %> <%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %> diff --git a/app/views/events/event/_comment.html.erb b/app/views/events/event/_comment.html.erb index 22f4596e8..ed0cbb79c 100644 --- a/app/views/events/event/_comment.html.erb +++ b/app/views/events/event/_comment.html.erb @@ -1,5 +1,5 @@ <%= render "events/event/layout", card: event.eventable.card, event: event do %> - <%= strip_tags(event&.eventable.body_html).truncate(200) -%> + <%= format_excerpt(event&.eventable.body, length: 200) -%> <% end %> diff --git a/app/views/layouts/action_text/contents/_content.html.erb b/app/views/layouts/action_text/contents/_content.html.erb new file mode 100644 index 000000000..0dec72775 --- /dev/null +++ b/app/views/layouts/action_text/contents/_content.html.erb @@ -0,0 +1,3 @@ +
+ <%= yield -%> +
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 0e04cbca9..b667bfb47 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -3,7 +3,7 @@ <%= page_title_tag %> - + diff --git a/config/importmap.rb b/config/importmap.rb index 65dc7629c..2abe3a805 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -5,9 +5,11 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js" pin "@hotwired/stimulus", to: "stimulus.min.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin "@rails/request.js", to: "@rails--request.js" # @0.0.11 -pin "house", to: "house.min.js" pin_all_from "app/javascript/controllers", under: "controllers" pin_all_from "app/javascript/helpers", under: "helpers" pin_all_from "app/javascript/initializers", under: "initializers" pin "marked" # @15.0.11 +pin "actiontext-lexical" +pin "@rails/activestorage", to: "activestorage.esm.js" +pin "@rails/actiontext", to: "actiontext.esm.js" diff --git a/config/initializers/markdown.rb b/config/initializers/markdown.rb deleted file mode 100644 index 04d5c8e78..000000000 --- a/config/initializers/markdown.rb +++ /dev/null @@ -1,7 +0,0 @@ -ActiveSupport.on_load :action_text_markdown do - require "markdown_renderer" - require "redcarpet/render_strip" - - ActionText::Markdown.html_renderer = ->(content) { MarkdownRenderer.build.render(content) } - ActionText::Markdown.plain_text_renderer = Redcarpet::Markdown.new(Redcarpet::Render::StripDown) -end diff --git a/db/migrate/20250523084734_create_action_text_tables.action_text.rb b/db/migrate/20250523084734_create_action_text_tables.action_text.rb new file mode 100644 index 000000000..c1c0a9d8c --- /dev/null +++ b/db/migrate/20250523084734_create_action_text_tables.action_text.rb @@ -0,0 +1,26 @@ +# This migration comes from action_text (originally 20180528164100) +class CreateActionTextTables < ActiveRecord::Migration[6.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :action_text_rich_texts, id: primary_key_type do |t| + t.string :name, null: false + t.text :body, size: :long + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + + t.timestamps + + t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [ primary_key_type, foreign_key_type ] + end +end diff --git a/db/schema.rb b/db/schema.rb index 834b2bcc9..098674f3a 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.1].define(version: 2025_05_15_125505) do +ActiveRecord::Schema[8.1].define(version: 2025_05_23_084734) do create_table "accesses", force: :cascade do |t| t.integer "collection_id", null: false t.datetime "created_at", null: false @@ -39,6 +39,16 @@ ActiveRecord::Schema[8.1].define(version: 2025_05_15_125505) do t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record" end + create_table "action_text_rich_texts", force: :cascade do |t| + t.text "body" + t.datetime "created_at", null: false + t.string "name", null: false + t.bigint "record_id", null: false + t.string "record_type", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + create_table "active_storage_attachments", force: :cascade do |t| t.bigint "blob_id", null: false t.datetime "created_at", null: false diff --git a/db/schema_cache.yml b/db/schema_cache.yml index fbaacdc9c..6cd19a67c 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -1,7 +1,7 @@ --- !ruby/object:ActiveRecord::ConnectionAdapters::SchemaCache columns: accesses: - - &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: collection_id cast_type: &1 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer @@ -80,7 +80,7 @@ columns: default_function: collation: comment: - - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &25 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: user_id cast_type: *1 @@ -118,13 +118,13 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: content - cast_type: &14 !ruby/object:ActiveRecord::Type::Text + cast_type: &11 !ruby/object:ActiveRecord::Type::Text true: t false: f precision: scale: limit: - sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: TEXT type: :text limit: @@ -159,17 +159,30 @@ columns: collation: comment: - *9 - active_storage_attachments: + action_text_rich_texts: + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: body + cast_type: *11 + sql_type_metadata: *12 + 'null': true + default: + default_function: + collation: + comment: + - *5 + - *6 + - *10 - &16 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: - name: blob_id - cast_type: &11 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer + name: record_id + cast_type: &14 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer precision: scale: limit: max: 9223372036854775808 min: -9223372036854775808 - sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: bigint type: :integer limit: @@ -180,19 +193,23 @@ columns: default_function: collation: comment: - - *5 - - *6 - - *10 - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - *13 + - *9 + active_storage_attachments: + - &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: - name: record_id - cast_type: *11 - sql_type_metadata: *12 + name: blob_id + cast_type: *14 + sql_type_metadata: *15 'null': false default: default_function: collation: comment: + - *5 + - *6 + - *10 + - *16 - *13 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -208,8 +225,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: byte_size - cast_type: *11 - sql_type_metadata: *12 + cast_type: *14 + sql_type_metadata: *15 'null': false default: default_function: @@ -247,7 +264,7 @@ columns: collation: comment: - *6 - - &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: key cast_type: *7 @@ -260,8 +277,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: metadata - cast_type: *14 - sql_type_metadata: *15 + cast_type: *11 + sql_type_metadata: *12 'null': true default: default_function: @@ -278,7 +295,7 @@ columns: collation: comment: active_storage_variant_records: - - *16 + - *17 - *6 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -292,7 +309,7 @@ columns: comment: ar_internal_metadata: - *5 - - *17 + - *18 - *9 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -305,7 +322,7 @@ columns: collation: comment: assignees_filters: - - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: assignee_id cast_type: *1 @@ -315,7 +332,7 @@ columns: default_function: collation: comment: - - &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: filter_id cast_type: *1 @@ -326,7 +343,7 @@ columns: collation: comment: assigners_filters: - - &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: assigner_id cast_type: *1 @@ -336,11 +353,11 @@ columns: default_function: collation: comment: - - *18 - assignments: - *19 + assignments: - *20 - - &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - *21 + - &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: card_id cast_type: *1 @@ -368,14 +385,14 @@ columns: - *6 - *9 card_goldnesses: - - *21 + - *22 - *5 - *6 - *9 cards: - - *22 + - *23 - *5 - - &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: creator_id cast_type: *1 @@ -428,14 +445,14 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: status - cast_type: *14 - sql_type_metadata: *15 + cast_type: *11 + sql_type_metadata: *12 'null': false default: creating default_function: collation: comment: - - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: title cast_type: *7 @@ -461,7 +478,7 @@ columns: comment: - *9 closures: - - *21 + - *22 - *5 - *6 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column @@ -489,11 +506,11 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: all_access - cast_type: &31 !ruby/object:ActiveModel::Type::Boolean + cast_type: &32 !ruby/object:ActiveModel::Type::Boolean precision: scale: limit: - sql_type_metadata: &32 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &33 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: boolean type: :boolean limit: @@ -507,15 +524,15 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: auto_close_period - cast_type: *11 - sql_type_metadata: *12 + cast_type: *14 + sql_type_metadata: *15 'null': true default: default_function: collation: comment: - *5 - - *23 + - *24 - *6 - *10 - *9 @@ -530,18 +547,18 @@ columns: collation: comment: collections_filters: - - *22 - - *18 + - *23 + - *19 commands: - *5 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: data - cast_type: &25 !ruby/object:ActiveRecord::Type::Json + cast_type: &26 !ruby/object:ActiveRecord::Type::Json precision: scale: limit: - sql_type_metadata: &26 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: json type: :json limit: @@ -556,8 +573,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: line - cast_type: *14 - sql_type_metadata: *15 + cast_type: *11 + sql_type_metadata: *12 'null': true default: default_function: @@ -584,16 +601,16 @@ columns: collation: comment: - *9 - - *24 + - *25 comments: - - *21 + - *22 - *5 - - *23 + - *24 - *6 - *9 creators_filters: - - *23 - - *18 + - *24 + - *19 events: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -605,9 +622,9 @@ columns: default_function: collation: comment: - - *22 - - *5 - *23 + - *5 + - *24 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: eventable_id @@ -632,8 +649,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: particulars - cast_type: *25 - sql_type_metadata: *26 + cast_type: *26 + sql_type_metadata: *27 'null': true default: "{}" default_function: @@ -642,12 +659,12 @@ columns: - *9 filters: - *5 - - *23 + - *24 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: fields - cast_type: *25 - sql_type_metadata: *26 + cast_type: *26 + sql_type_metadata: *27 'null': false default: "{}" default_function: @@ -666,7 +683,7 @@ columns: comment: - *9 filters_stages: - - *18 + - *19 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: stage_id @@ -678,8 +695,8 @@ columns: collation: comment: filters_tags: - - *18 - - &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - *19 + - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: tag_id cast_type: *1 @@ -712,7 +729,7 @@ columns: default_function: collation: comment: - - &27 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: source_id cast_type: *1 @@ -722,7 +739,7 @@ columns: default_function: collation: comment: - - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: source_type cast_type: *7 @@ -756,16 +773,16 @@ columns: default_function: collation: comment: - - *27 - *28 + - *29 - *9 - - *24 + - *25 pins: - - *21 + - *22 - *5 - *6 - *9 - - *24 + - *25 reactions: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -882,24 +899,24 @@ columns: default_function: collation: comment: - - *24 + - *25 taggings: - - *21 + - *22 - *5 - *6 - - *29 + - *30 - *9 tags: - *5 - *6 - - *30 + - *31 - *9 users: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: active - cast_type: *31 - sql_type_metadata: *32 + cast_type: *32 + sql_type_metadata: *33 'null': false default: true default_function: @@ -940,16 +957,16 @@ columns: comment: - *9 watches: - - *21 + - *22 - *5 - *6 - *9 - - *24 + - *25 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: watching - cast_type: *31 - sql_type_metadata: *32 + cast_type: *32 + sql_type_metadata: *33 'null': false default: true default_function: @@ -989,6 +1006,7 @@ primary_keys: accesses: id accounts: id action_text_markdowns: id + action_text_rich_texts: id active_storage_attachments: id active_storage_blobs: id active_storage_variant_records: id @@ -1027,6 +1045,7 @@ data_sources: accesses: true accounts: true action_text_markdowns: true + action_text_rich_texts: true active_storage_attachments: true active_storage_blobs: true active_storage_variant_records: true @@ -1131,6 +1150,25 @@ indexes: nulls_not_distinct: comment: valid: true + action_text_rich_texts: + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: action_text_rich_texts + name: index_action_text_rich_texts_uniqueness + unique: true + columns: + - record_type + - record_id + - name + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true active_storage_attachments: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: active_storage_attachments @@ -2161,4 +2199,4 @@ indexes: comment: valid: true workflows: [] -version: 20250515125505 +version: 20250523084734 diff --git a/lib/markdown_renderer.rb b/lib/markdown_renderer.rb deleted file mode 100644 index e90e86fb4..000000000 --- a/lib/markdown_renderer.rb +++ /dev/null @@ -1,81 +0,0 @@ -require "rouge/plugins/redcarpet" - -class MarkdownRenderer < Redcarpet::Render::HTML - include Rouge::Plugins::Redcarpet - - def self.build - Redcarpet::Markdown.new MarkdownRenderer.new(filter_html: false), - autolink: true, highlight: true, no_intra_emphasis: true, - fenced_code_blocks: true, lax_spacing: true, strikethrough: true, tables: true - end - - def initialize(...) - super - @id_counts = Hash.new 0 - end - - def header(text, header_level) - unique_id(text).then do |id| - <<~HTML.chomp - - #{text} - - HTML - end - end - - def image(url, title, alt_text) - <<~HTML.chomp - - #{alt_text} - - HTML - end - - def code_block(code, language) - block_code(code, language) # call Rouge Redcarpet plugin - end - - def link(url, title, content) - return video_tag(url, title) if video_url?(url) - - attributes = { href: url } - attributes[:title] = title if title - attributes["data-turbo-frame"] = "_top" - - "#{content}" - end - - def autolink(url, link_type) - attributes = { href: url } - attributes["data-turbo-frame"] = "_top" - - "#{url}" - end - - private - attr_reader :id_counts - - def html_attributes(attributes) - attributes.map { |key, value| %Q(#{key}="#{value}") }.join(" ") - end - - 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 - - def video_url?(url) - File.extname(url).downcase.match?(/\.(mp4|webm|ogg|mov|avi|mkv)/) - end - - def video_tag(url, title) - <<~HTML.chomp - - HTML - end -end diff --git a/lib/rails_ext/action_text_has_markdown.rb b/lib/rails_ext/action_text_has_markdown.rb deleted file mode 100644 index b792657d5..000000000 --- a/lib/rails_ext/action_text_has_markdown.rb +++ /dev/null @@ -1,47 +0,0 @@ -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}_html - #{name}.to_html - end - - def #{name}_plain_text - #{name}.to_plain_text - 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 deleted file mode 100644 index cd19fa001..000000000 --- a/lib/rails_ext/action_text_markdown.rb +++ /dev/null @@ -1,44 +0,0 @@ -module ActionText - class Markdown < Record - mattr_accessor :html_renderer - mattr_accessor :plain_text_renderer - - belongs_to :record, polymorphic: true, touch: true - - def to_html - to_unsafe_html.html_safe - end - - def to_unsafe_html - if html_renderer.respond_to? :call - html_renderer.call content - else - html_renderer.render content - end - end - - def to_plain_text - if plain_text_renderer.respond_to? :call - plain_text_renderer.call content - else - plain_text_renderer.render content - end - 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 deleted file mode 100644 index d662da7b2..000000000 --- a/lib/rails_ext/action_text_tag_helper.rb +++ /dev/null @@ -1,36 +0,0 @@ -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: 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 deleted file mode 100644 index d4d4be335..000000000 --- a/lib/rails_ext/active_storage_sluggable.rb +++ /dev/null @@ -1,32 +0,0 @@ -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.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/script/convert_markdowns_to_rich_text.rb b/script/convert_markdowns_to_rich_text.rb new file mode 100755 index 000000000..1a43db239 --- /dev/null +++ b/script/convert_markdowns_to_rich_text.rb @@ -0,0 +1,104 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "redcarpet" +require "nokogiri" + +class ActionText::Markdown < ApplicationRecord + belongs_to :record, polymorphic: true +end + +class MarkdownToActionTextConverter + ATTACHMENT_URL_REGEX = %r{/u/(?[^\/\s\)]+)} + + def initialize(html) + @doc = Nokogiri::HTML::DocumentFragment.parse(html) + @attachments = [] + end + + def convert + process_images + process_links + [ @doc.to_html, @attachments ] + end + + private + def process_images + @doc.css("img").each do |img| + src = img["src"].presence + if src && match = src.match(ATTACHMENT_URL_REGEX) + if (attachment = find_attachment(match[:slug])) + img.replace(build_attachment_node(attachment)) + @attachments << attachment + end + end + end + end + + def process_links + @doc.css("a").each do |link| + href = link["href"].presence + + if href && match = href.match(ATTACHMENT_URL_REGEX) + if (attachment = find_attachment(match[:slug])) + link.replace(build_attachment_node(attachment)) + @attachments << attachment + end + end + end + end + + def build_attachment_node(attachment) + html = ActionText::Attachment.from_attachable(attachment).to_html + fragment = Nokogiri::HTML::DocumentFragment.parse(html) + + node = fragment.at_css("action-text-attachment") + node["url"] = Rails.application.routes.url_helpers.rails_blob_path(attachment.blob, only_path: true) + + fragment + end + + def find_attachment(slug) + ActiveStorage::Attachment.find_by(slug: slug) + end +end + +class RedcarpetRenderer + def self.render(markdown) + renderer = Redcarpet::Render::HTML.new + markdowner = Redcarpet::Markdown.new(renderer, + autolink: true, + tables: true, + fenced_code_blocks: true, + strikethrough: true, + superscript: true, + ) + markdowner.render(markdown.to_s) + end +end + +ApplicationRecord.with_each_tenant do |tenant| + puts "Processing tenant: #{tenant}" + + ActionText::Markdown.find_each do |markdown| + next unless markdown.record + + html = RedcarpetRenderer.render(markdown.content.to_s) + converter = MarkdownToActionTextConverter.new(html) + rich_text_html, attachments = converter.convert + + rich_text = ActionText::RichText.create!( + name: markdown.name, + record: markdown.record, + body: rich_text_html + ) + + attachments.each do |attachment| + attachment.update!(record: rich_text) + end + + puts "✓ Created rich text for #{markdown.record_type}##{markdown.record_id} (#{markdown.name})" + rescue => e + warn "✗ Failed to process markdown ##{markdown.id}: #{e.class} - #{e.message}" + end +end diff --git a/script/migrate-to-multi-tenant b/script/migrate-to-multi-tenant index 8d035f37e..79db5c0e0 100755 --- a/script/migrate-to-multi-tenant +++ b/script/migrate-to-multi-tenant @@ -214,9 +214,6 @@ class EnactTenanting assert_count Tagging, Tagging.count assert_count Watch, Watch.count - # we're only copying the markdown records that are still accessible. - assert_count ActionText::Markdown, Comment.count - assert_count Event, Event.count assert_count Notification, Notification.count @@ -228,22 +225,7 @@ class EnactTenanting end def update_action_text_urls - update_counter["ActionText::Markdown"] = 0 - TENANTS.each do |tenant| - tenanted_domain = sprintf(TENANTED_DOMAIN, tenant: tenant) - - ApplicationRecord.with_tenant(tenant) do - ActionText::Markdown.all.each do |markdown| - content = markdown.content - next unless content =~ %r{#{CURRENT_DOMAIN}/u/} - - content.gsub!(%r{#{CURRENT_DOMAIN}/u/}, "#{tenanted_domain}/u/") - - markdown.update_column :content, content - update_counter["ActionText::Markdown"] += 1 - end - end - end + raise "No more markdown" end def hardlink_active_storage diff --git a/script/reset_cards_ids.rb b/script/reset_cards_ids.rb index 19305b5f2..65c556283 100644 --- a/script/reset_cards_ids.rb +++ b/script/reset_cards_ids.rb @@ -100,9 +100,9 @@ ApplicationRecord.with_each_tenant do |tenant| end # Update action_text_markdowns table (polymorphic relationship) - ActionText::Markdown.where(record_type: "Card").find_each do |markdown| - if id_mapping[markdown.record_id] - markdown.update_column(:record_id, id_mapping[markdown.record_id]) + ActionText::RichText.where(record_type: "Card").find_each do |rich_text| + if id_mapping[rich_text.record_id] + rich_text.update_column(:record_id, id_mapping[rich_text.record_id]) end end diff --git a/script/reset_collections_ids.rb b/script/reset_collections_ids.rb index 142e9e2a9..cf1d84028 100644 --- a/script/reset_collections_ids.rb +++ b/script/reset_collections_ids.rb @@ -73,9 +73,9 @@ ApplicationRecord.with_each_tenant do |tenant| end # Update action_text_markdowns table (polymorphic relationship) - ActionText::Markdown.where(record_type: "Collection").find_each do |markdown| - if id_mapping[markdown.record_id] - markdown.update_column(:record_id, id_mapping[markdown.record_id]) + ActionText::RichText.where(record_type: "Collection").find_each do |rich_text| + if id_mapping[rich_text.record_id] + rich_text.update_column(:record_id, id_mapping[rich_text.record_id]) end end diff --git a/test/controllers/cards/comments_controller_test.rb b/test/controllers/cards/comments_controller_test.rb index 690cf0656..76ff0f6fa 100644 --- a/test/controllers/cards/comments_controller_test.rb +++ b/test/controllers/cards/comments_controller_test.rb @@ -17,11 +17,11 @@ class Cards::CommentsControllerTest < ActionDispatch::IntegrationTest put card_comment_path(cards(:logo), comments(:logo_agreement_kevin)), params: { comment: { body: "I've changed my mind" } } assert_response :success - assert_equal "I've changed my mind", comments(:logo_agreement_kevin).reload.body.content + assert_action_text "I've changed my mind", comments(:logo_agreement_kevin).reload.body end test "update another user's comment" do - assert_no_changes "comments(:logo_agreement_jz).body.content" do + assert_no_changes -> { comments(:logo_agreement_jz).reload.body.to_s } do put card_comment_path(cards(:logo), comments(:logo_agreement_jz)), params: { comment: { body: "I've changed my mind" } } end diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index 5cb3a6582..75ba45147 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -46,7 +46,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest assert_equal "moon.jpg", card.image.filename.to_s assert_equal [ tags(:mobile) ], card.tags - assert_equal "Something more in-depth", card.description_plain_text.strip + assert_equal "Something more in-depth", card.description.to_plain_text.strip end test "users can only see cards in collections they have access to" do diff --git a/test/controllers/uploads_controller_test.rb b/test/controllers/uploads_controller_test.rb deleted file mode 100644 index 3f5f54af5..000000000 --- a/test/controllers/uploads_controller_test.rb +++ /dev/null @@ -1,25 +0,0 @@ -require "test_helper" - -class UploadsControllerTest < ActionDispatch::IntegrationTest - setup do - sign_in_as :kevin - end - - test "create" do - assert_changes -> { ActiveStorage::Attachment.count }, 1 do - post uploads_path(format: "json"), params: { file: fixture_file_upload("moon.jpg", "image/jpeg") }, as: :xhr - end - - assert_response :success - assert_equal ActiveStorage::Attachment.last.slug_url(host: "#{ApplicationRecord.current_tenant}.example.com", port: nil), response.parsed_body["fileUrl"] - assert_equal "image/jpeg", response.parsed_body["mimetype"] - assert_equal "moon.jpg", response.parsed_body["fileName"] - end - - test "show" do - accounts("37s").uploads.attach fixture_file_upload("moon.jpg", "image/jpeg") - get upload_path(slug: accounts("37s").uploads.last.slug) - assert_response :redirect - assert_match /\/rails\/active_storage\/.*\/moon\.jpg/, @response.redirect_url - end -end diff --git a/test/fixtures/action_text/markdowns.yml b/test/fixtures/action_text/rich_texts.yml similarity index 69% rename from test/fixtures/action_text/markdowns.yml rename to test/fixtures/action_text/rich_texts.yml index 519cd408b..9a86c3b30 100644 --- a/test/fixtures/action_text/markdowns.yml +++ b/test/fixtures/action_text/rich_texts.yml @@ -1,14 +1,16 @@ logo_agreement_jz: record: logo_agreement_jz (Comment) name: body - content: I agree. + body: I agree. logo_agreement_kevin: record: logo_agreement_kevin (Comment) name: body - content: Same, let’s do it. + body: Same, let’s do it. layout_overflowing_david: record: layout_overflowing_david (Comment) name: body - content: The text is overflowing the container. + body: The text is overflowing the container. + + diff --git a/test/test_helper.rb b/test/test_helper.rb index 6e27d82fa..4e0a9fcee 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -22,7 +22,7 @@ module ActiveSupport fixtures :all include ActiveJob::TestHelper - include CardTestHelper, ChangeTestHelper, SessionTestHelper + include ActionTextTestHelper, CardTestHelper, ChangeTestHelper, SessionTestHelper end end diff --git a/test/test_helpers/action_text_test_helper.rb b/test/test_helpers/action_text_test_helper.rb new file mode 100644 index 000000000..09a26fceb --- /dev/null +++ b/test/test_helpers/action_text_test_helper.rb @@ -0,0 +1,21 @@ +module ActionTextTestHelper + def assert_action_text(expected_html, content) + assert_equal_html <<~HTML, content.to_s +
#{expected_html} + HTML + end + + def assert_equal_html(expected, actual) + assert_equal normalize_html(expected), normalize_html(actual) + end + + def normalize_html(html) + Nokogiri::HTML.fragment(html).tap do |fragment| + fragment.traverse do |node| + if node.text? + node.content = node.text.squish + end + end + end.to_html.strip + end +end diff --git a/vendor/javascript/house.min.js b/vendor/javascript/house.min.js deleted file mode 100644 index c81e324e5..000000000 --- a/vendor/javascript/house.min.js +++ /dev/null @@ -1 +0,0 @@ -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.#i()&&(this.innerHTML=this.#r())}#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})};#i(){return 0===this.children.length}#r(){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:r,endOffset:o,endContainer:l}=t,a=i(e,r,n);if(t.collapsed)return new s(e,a,a);const c=i(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,i=!1;for(;e.nextNode()&&(!s||!i);){const r=e.currentNode.textContent.length;n+r>=this.start&&!s&&(s=!0,t.setStart(e.currentNode,this.start-n)),n+r>=this.end&&!i&&(i=!0,t.setEnd(e.currentNode,this.end-n)),n+=r}return t}isEqual(t){return this.container===t.container&&this.start===t.start&&this.end===t.end}}function i(t,e,n){return e.nodeType===Node.ELEMENT_NODE?function(t,e,n){const s=e.childNodes[n];let i=0;const r=document.createTreeWalker(t);for(;r.nextNode();){if(r.currentNode===s)return i;r.currentNode.nodeType===Node.TEXT_NODE&&(i+=r.currentNode.textContent.length)}return i}(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 r{#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),i=-1===s?e.length:s+1;return{start:n,end:i,content:e.slice(n,i)}}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 r(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),i=`${s}${t}${this.content.slice(n)}`,r={start:s.length+t.length,end:s.length+t.length};this.#h(),this.#d(i,r)}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:i}=this.currentSelection,r=` ${n}[${t}](${e}) `;this.replaceText(r,s,i),this.selection.select({start:s+r.length,end:s+r.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.#b("> ")}toggleBulletList(){this.selection.isBulletList?this.#f("- "):this.#b("- ")}toggleNumberList(){this.selection.isNumberList?this.#f(/\d+\. /):this.#b(`${this.#L+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)+"",this.contentElement.classList.toggle("house-md-content-empty",this.isEmpty)}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.#w(e,n):this.#E(e,n)}#E(t,e=t){const{start:n,end:s,selection:i}=this.currentSelection,r=`${t}${i}${e}`;this.replaceText(r,n,s),this.selection.select({start:n,end:n+i.length+t.length+e.length})}#w(t,e=t){const{start:n,end:s,selection:i}=this.currentSelection;i.startsWith(t)&&i.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,i=s?"title":n;this.replaceText(`[${i}](url)`,t,e),s?this.selection.select({start:t+1,end:t+i.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.#L+1}. `,this.currentSelection.end+1,this.currentSelection.end+1))}#b(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#L(){const t=this.selection.previousLine().content.match(/^(\d+). /);return t?parseInt(t[1]):0}}class c{#A;constructor(t){this.#A=t}asPlainText(){const t=(new window.DOMParser).parseFromString(this.#A,"text/html");return this.#S(t.body).trim()}#S(t){return t.nodeType===Node.TEXT_NODE?t.nodeValue:Array.from(t.childNodes).map(((e,n)=>{const s=this.#k(e),i=n>0&&this.#k(t.childNodes[n-1]);return(s&&i?"\n\n":"")+this.#S(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 i=t.getTargetRanges()[0],{start:r,end:o}=s.fromDOMRange(i,e);n.call(this,t,r,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)}insertLineBreakHandler(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;static observedAttributes=["initialized"];constructor(){super(),this.internals=this.attachInternals(),this.internals.ariaRole="textbox"}attributeChangedCallback(t,e,n){"initialized"===t&&this.isConnected&&null!=e&&e!==n&&requestAnimationFrame((()=>this.#H()))}connectedCallback(){const t=this.querySelector(".house-md-content")?.textContent??this.textContent;this.document=new a(t,this),this.#y(),this.#N(),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.#_()}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.#D(),this.contentWrapper=document.createElement("div"),this.contentWrapper.classList.add("house-md-content"),this.contentWrapper.setAttribute("contenteditable",!0),this.contentWrapper.setAttribute("role","textbox"),this.contentWrapper.setAttribute("aria-multiline",!0),this.append(this.contentWrapper),this.getAttribute("tabindex")?(this.contentWrapper.setAttribute("tabindex",this.getAttribute("tabindex")),this.removeAttribute("tabindex")):this.contentWrapper.setAttribute("tabindex",0)}#D(){if(this.getAttribute("toolbar"))return document.getElementById(this.getAttribute("toolbar"));{const t=document.createElement("house-md-toolbar");return this.prepend(t),t}}#H(){this.disconnectedCallback(),this.getAttribute("toolbar")||this.toolbar.remove(),this.contentWrapper.remove(),this.connectedCallback()}#N(){this.addEventListener("beforeinput",this.#I),this.addEventListener("keydown",this.#B),this.addEventListener("dragover",this.#F),this.addEventListener("drop",this.#R),this.toolbar.addEventListener("house-md:toolbar-action",this.#M),document.addEventListener("selectionchange",this.#z),this.internals.form.addEventListener("submit",this.validate)}#_(){this.removeEventListener("beforeinput",this.#I),this.removeEventListener("keydown",this.#B),this.removeEventListener("dragover",this.#F),this.removeEventListener("drop",this.#R),this.toolbar.removeEventListener("house-md:toolbar-action",this.#M),document.removeEventListener("selectionchange",this.#z),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=({detail:t})=>{this.actionHandler.handleAction(t.houseMdAction,t)};#z=()=>{this.document.selection.update()}}customElements.define("house-md",g);class v extends HTMLElement{#W;#$;#O;constructor(){super(),this.percentComplete=0,this.status="pending"}connectedCallback(){this.#t(),this.#q(),this.addEventListener("click",this.#e)}disconnectedCallback(){this.removeEventListener("click",this.#e)}async#q(){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.#P,this.xhr.onload=this.#U,this.xhr.onerror=this.#K,this.xhr.onabort=this.#Q,this.xhr.send(t),this.status="uploading",this.#t()}#t(){this.setAttribute("status",this.status),this.innerHTML=`\n ${this.#V()}\n ${this.#X()}\n ${this.#j()}\n ${this.#Z()}\n `}#V(){return"failed"===this.status?"":""}#X(){return`
${this.file.name}
`}#j(){return`${this.percentComplete}%`}#Z(){return this.#O?`
${this.#O}
`:""}#P=t=>{t.lengthComputable&&(this.percentComplete=t.loaded/t.total*100),this.#t()};#U=async()=>{if(this.xhr.status>=400)this.status="failed",this.#O=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.#J()}this.#t()};#K=()=>{this.status="failed",this.#t()};#Q=()=>{this.status="aborted",this.#t()};#e=t=>{t.target.matches("[data-md-action=close]")&&this.remove()};async#J(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,r as Selection,n as Toolbar,v as Upload}; From ee3a9503cd1c5ac12ccfd406047f7502a9a9c6dc Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 15:44:43 +0200 Subject: [PATCH 54/58] Update script to deal with duplicated records We are not enforcing unique keys for markdowns, and we have some dups --- script/convert_markdowns_to_rich_text.rb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/script/convert_markdowns_to_rich_text.rb b/script/convert_markdowns_to_rich_text.rb index 1a43db239..e093730f6 100755 --- a/script/convert_markdowns_to_rich_text.rb +++ b/script/convert_markdowns_to_rich_text.rb @@ -77,10 +77,12 @@ class RedcarpetRenderer end end -ApplicationRecord.with_each_tenant do |tenant| - puts "Processing tenant: #{tenant}" +def process_all(klass, field) + klass.find_each do |record| + markdown = ActionText::Markdown.find_by(record: record, name: field) + next unless markdown - ActionText::Markdown.find_each do |markdown| + puts "markdown.id=#{markdown.id}" next unless markdown.record html = RedcarpetRenderer.render(markdown.content.to_s) @@ -102,3 +104,12 @@ ApplicationRecord.with_each_tenant do |tenant| warn "✗ Failed to process markdown ##{markdown.id}: #{e.class} - #{e.message}" end end + +ApplicationRecord.with_each_tenant do |tenant| + puts "Processing tenant: #{tenant}" + + ActionText::RichText.delete_all + + process_all(Card, :description) + process_all(Comment, :body) +end From f405c364aca42e70cd552abf4969416266105c63 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 16:31:16 +0200 Subject: [PATCH 55/58] update lexical --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0ed3a138e..2c8ea39a1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical - revision: 777b2a4a933db9cb4cd515c4569dfcaf8cae45b5 + revision: 831592e30044aa711ca40386209eb34ed7343371 specs: actiontext-lexical (0.1.0) rails (>= 8.0.2) From 0e5bb87bc6cd555cb82d357366cba5f5a90e228f Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 16:33:04 +0200 Subject: [PATCH 56/58] update lexical --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2c8ea39a1..4df508aa2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical - revision: 831592e30044aa711ca40386209eb34ed7343371 + revision: 6e53a32d373bc73b8b49461338d22727499158b1 specs: actiontext-lexical (0.1.0) rails (>= 8.0.2) From e0accd0fd4c7081976ca79d4578139b4f9d3b410 Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Thu, 29 May 2025 13:37:07 -0500 Subject: [PATCH 57/58] Hide bubbles in trays --- app/assets/stylesheets/trays.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index 947a4094a..2267ea20d 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -162,6 +162,10 @@ --tray-item-margin: 0; --tray-item-scale: 1; } + + .bubble { + display: none; + } } .tray__item--overflow { From 32ae94524af8493ba48e6774de05e38008648151 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 29 May 2025 14:19:48 -0500 Subject: [PATCH 58/58] Safari doesn't support CSS `field-sizing: content` so we can shim it for now --- app/assets/stylesheets/autoresize.css | 27 +++++++++++++++++++ app/assets/stylesheets/cards.css | 6 +++++ .../controllers/autoresize_controller.js | 13 +++++++++ app/views/cards/container/_title.html.erb | 10 +++---- app/views/cards/edit.html.erb | 11 ++++---- 5 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 app/assets/stylesheets/autoresize.css create mode 100644 app/javascript/controllers/autoresize_controller.js diff --git a/app/assets/stylesheets/autoresize.css b/app/assets/stylesheets/autoresize.css new file mode 100644 index 000000000..ac16e04a4 --- /dev/null +++ b/app/assets/stylesheets/autoresize.css @@ -0,0 +1,27 @@ +@layer components { + @supports not (field-sizing: content) { + .autoresize__wrapper { + display: grid !important; + position: relative; + + > *, &::after { + grid-area: 1 / 1 / 2 / 2; + } + + &::after { + content: attr(data-autoresize-clone-value) " "; + font: inherit; + line-height: inherit; + visibility: hidden; + white-space: pre-wrap; + } + } + + .autoresize__textarea { + inset: 0; + overflow: hidden; + position: absolute; + resize: none; + } + } +} diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index d13b60498..53315b8a6 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -174,6 +174,12 @@ line-height: 1.2; text-wrap: balance; + @supports not (field-sizing: content) { + &:has(textarea) { + text-wrap: unset; /* Safari is annoying if you have text-wrap: balance in textareas */ + } + } + .card-field__title { &:is(textarea)::placeholder { color: inherit; diff --git a/app/javascript/controllers/autoresize_controller.js b/app/javascript/controllers/autoresize_controller.js new file mode 100644 index 000000000..02b6cc501 --- /dev/null +++ b/app/javascript/controllers/autoresize_controller.js @@ -0,0 +1,13 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["textarea", "wrapper"] + + connect() { + this.resize() + } + + resize() { + this.wrapperTarget.setAttribute("data-autoresize-clone-value", this.textareaTarget.value) + } +} diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index 8bbe68550..1767fd854 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -10,12 +10,12 @@
<% end %> <% else %> - <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "auto-save" } do |form| %> -

+ <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "autoresize auto-save" } do |form| %> +

<%= form.text_area :title, placeholder: "Name it…", - class: "input input--textarea full-width borderless txt-align-start", - autofocus: card.title.blank?, - data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %> + class: "input input--textarea full-width borderless txt-align-start autoresize__textarea", + autofocus: card.title.blank?, rows: 1, + data: { autoresize_target: "textarea", action: "input->autoresize#resize auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>

<%= form.rich_textarea :description, class: "card-field__description", diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index c02e2a635..6a8d37e13 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -1,11 +1,10 @@ <%= turbo_frame_tag @card, :edit do %> - <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "form" } do |form| %> + <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "autoresize form" } do |form| %>

- <%= form.label :title, class: "flex flex-column align-center" do %> - <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title", - required: true, autofocus: true, placeholder: "Name it…", - rows: 1, - data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> + <%= form.label :title, class: "flex flex-column align-center autoresize__wrapper", data: { autoresize_target: "wrapper", autoresize_clone_value: "" } do %> + <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title autoresize__textarea", + required: true, autofocus: true, placeholder: "Name it…", rows: 1, + data: { autoresize_target: "textarea", action: "input->autoresize#resize keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> <% end %>