From 10b2aad6478708431000bcfb4ab495875688f150 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 23 May 2025 11:41:08 +0200 Subject: [PATCH 01/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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 ecf82d8703b09f2d940cdd916d42e7309b1f8232 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 29 May 2025 12:37:16 +0200 Subject: [PATCH 41/50] 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 42/50] 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 43/50] 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 44/50] 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 45/50] 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 46/50] 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 47/50] 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 48/50] 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 49/50] 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 50/50] 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)