diff --git a/README.md b/README.md index 327588c..49299eb 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ config.use_local_editor = false # or true ## Installation > [!NOTE] -> This guide assumes that you use ferbe on a standard rails 8.1 app that already has stimulus and importmap set up. +> This guide assumes that you use ferbe on a standard rails 8.0/8.1 app that already has stimulus and importmap set up. Add this line to your application's Gemfile: @@ -91,35 +91,13 @@ $ rails generate ferbe:install ### Layout -In your layout, add these in the head section: +Now, in your layout, add these in the head section: ```erb <%= ferbe_styles_tag %> <%= ferbe_javascript_tag %> ``` -Add the `ferbe_editor_tag` and wrap everything in an element with the `ferbe__page` class. - -```erb -
-
- <%= yield %> -
- -<%= ferbe_editor_tag %> -
-``` - -### Add the Stimulus controller - -The final step is to add the provided Stimulus controller to your `javascript/controllers/index.js`: - -```js -import FerbeEditorController from "ferbe/controllers/ferbe_editor_controller"; -// ... -application.register("ferbe-editor", FerbeEditorController); -``` - --- Copyright 2026 by Renuo AG diff --git a/app/assets/javascripts/ferbe/application.js b/app/assets/javascripts/ferbe/application.js index 33219c8..2bd1387 100644 --- a/app/assets/javascripts/ferbe/application.js +++ b/app/assets/javascripts/ferbe/application.js @@ -1,3 +1,4 @@ -import FerbeTemplate from "ferbe/elements/ferbe_template"; +import { application } from "controllers/application"; +import FerbeEditorController from "ferbe/controllers/ferbe_editor_controller"; -customElements.define("ferbe-template", FerbeTemplate); +application.register("ferbe-editor", FerbeEditorController); diff --git a/app/assets/javascripts/ferbe/controllers/ferbe_editor_controller.js b/app/assets/javascripts/ferbe/controllers/ferbe_editor_controller.js index 1030b64..517f76d 100644 --- a/app/assets/javascripts/ferbe/controllers/ferbe_editor_controller.js +++ b/app/assets/javascripts/ferbe/controllers/ferbe_editor_controller.js @@ -1,3 +1,5 @@ +import editorUrl from "ferbe/utils/editor_url"; + import { Controller } from "@hotwired/stimulus"; import { CodeJar } from "codejar"; import hljs from "highlight.js/lib/core"; @@ -6,23 +8,41 @@ import xml from "highlight.js/lib/languages/xml"; import ruby from "highlight.js/lib/languages/ruby"; export default class FerbeEditorController extends Controller { - static targets = ["editor", "form", "input", "errorContainer"]; + static targets = ["editor", "form", "input"]; connect() { this.#setupHighlighting(); this.#highlight(this.editorTarget); this.#preventUnsavedClosing(); - this.#setupErrorHandling(); } disconnect() { this.jar.destroy(); window.onbeforeunload = null; - this.errorContainerTarget.innerHTML = ""; } - close() { - this.element.remove(); + open(event) { + const template = event.detail; + + const currentParams = new URLSearchParams(document.location.search); + const url = currentParams.get("url"); + + const params = new URLSearchParams(); + params.append("url", url); + params.append("template[path]", template.filePath); + template.renderPath.forEach((p) => + params.append("template[render_path][]", p), + ); + + fetch(`/ferbe/template/edit?${params.toString()}`, { + headers: { Accept: "text/vnd.turbo-stream.html" }, + }) + .then((r) => r.text()) + .then((html) => { + Turbo.renderStreamMessage(html); + window.history.pushState({}, "", editorUrl(template)); + }) + .catch((err) => console.error("Failed to open editor:", err)); } save() { @@ -58,24 +78,4 @@ export default class FerbeEditorController extends Controller { return "There are unsaved changes in the ferbe editor."; }; } - - #setupErrorHandling() { - addEventListener("turbo:before-fetch-response", (event) => { - const response = event.detail.fetchResponse; - if (response.statusCode !== 500) return; - - event.preventDefault(); - document.documentElement.removeAttribute("aria-busy"); - this.#displayError({ - message: `${response.statusCode} ${response.response.statusText}`, - url: response.response.url, - }); - }); - } - - #displayError({ message, url }) { - this.errorContainerTarget.innerHTML = `There is an error that was likely caused by your edit: -${message} -Open in new tab`; - } } diff --git a/app/assets/javascripts/ferbe/elements/ferbe_template.js b/app/assets/javascripts/ferbe/elements/ferbe_template.js index 0a957e8..6579848 100644 --- a/app/assets/javascripts/ferbe/elements/ferbe_template.js +++ b/app/assets/javascripts/ferbe/elements/ferbe_template.js @@ -43,19 +43,15 @@ export default class FerbeTemplate extends HTMLElement { } #openInEditor() { - const path = this.getAttribute("path"); - const renderPath = this.#getRenderPath(); - - const params = new URLSearchParams(); - params.append("template[path]", path); - renderPath.forEach((p) => params.append("template[render_path][]", p)); - - fetch(`/ferbe/template/edit?${params.toString()}`, { - headers: { Accept: "text/vnd.turbo-stream.html" }, - }) - .then((r) => r.text()) - .then((html) => Turbo.renderStreamMessage(html)) - .catch((err) => console.error("Failed to open editor:", err)); + window.top.dispatchEvent( + new CustomEvent("ferbe:open-editor", { + detail: { + url: window.location.toString(), + filePath: this.getAttribute("path"), + renderPath: this.#getRenderPath(), + }, + }), + ); } #getRenderPath() { diff --git a/app/assets/javascripts/ferbe/host.js b/app/assets/javascripts/ferbe/host.js new file mode 100644 index 0000000..999115c --- /dev/null +++ b/app/assets/javascripts/ferbe/host.js @@ -0,0 +1,12 @@ +import FerbeTemplate from "ferbe/elements/ferbe_template"; +import editorUrl from "ferbe/utils/editor_url"; + +customElements.define("ferbe-template", FerbeTemplate); + +addEventListener("ferbe:open-editor", (event) => { + const editor = document.getElementById("ferbe-editor"); + if (editor) return; + + const template = event.detail; + window.location.href = editorUrl(template); +}); diff --git a/app/assets/javascripts/ferbe/utils/editor_url.js b/app/assets/javascripts/ferbe/utils/editor_url.js new file mode 100644 index 0000000..609a052 --- /dev/null +++ b/app/assets/javascripts/ferbe/utils/editor_url.js @@ -0,0 +1,11 @@ +export default function editorUrl(template) { + const params = new URLSearchParams(); + + params.append("url", template.url); + params.append("template[path]", template.filePath); + template.renderPath.forEach((p) => + params.append("template[render_path][]", p), + ); + + return `/ferbe/template/edit?${params.toString()}`; +} diff --git a/app/assets/stylesheets/ferbe/application.css b/app/assets/stylesheets/ferbe/application.css index d7ddf6c..08743f8 100644 --- a/app/assets/stylesheets/ferbe/application.css +++ b/app/assets/stylesheets/ferbe/application.css @@ -2,23 +2,22 @@ --ferbe-color-light-grey: #fafafa; --ferbe-color-grey: #dedede; --ferbe-color-blue: #0033b3; - --ferbe-color-red: #f6817f; - --ferbe-color-light-red: #ff9b99; --ferbe-border: 2px solid var(--ferbe-color-grey); } +body { + margin: 0; +} + .ferbe__page { display: flex; - height: 100vh; + min-height: 100vh; - & > main { + iframe { + all: unset; flex-grow: 1; } - .ferbe__editor * { - all: revert; - } - .ferbe__editor > div { width: min(50vw, 60rem); padding: 1rem; @@ -55,6 +54,12 @@ } } + a.ferbe__button { + box-sizing: border-box; + text-decoration: none; + text-align: center; + } + .ferbe__header { display: flex; align-items: baseline; @@ -83,14 +88,5 @@ } } } - - .ferbe__error-container { - border-color: var(--ferbe-color-red); - background-color: var(--ferbe-color-light-red); - - &:empty { - display: none; - } - } } } diff --git a/app/assets/stylesheets/ferbe/host.css b/app/assets/stylesheets/ferbe/host.css new file mode 100644 index 0000000..30b746d --- /dev/null +++ b/app/assets/stylesheets/ferbe/host.css @@ -0,0 +1,3 @@ +ferbe-template { + display: contents; +} diff --git a/app/controllers/ferbe/templates_controller.rb b/app/controllers/ferbe/templates_controller.rb index 59be321..47ea579 100644 --- a/app/controllers/ferbe/templates_controller.rb +++ b/app/controllers/ferbe/templates_controller.rb @@ -1,19 +1,16 @@ module Ferbe class TemplatesController < ApplicationController def edit - template_params = params.require(:template).permit(:path, render_path: []) - return head :bad_request unless valid_path? template_params[:path] + template = edit_params[:template] + return head :bad_request unless valid_path? template[:path] @template = { - content: File.read(template_params[:path]), - path: template_params[:path], - render_path: template_params[:render_path] + content: File.read(template[:path]), + path: template[:path], + render_path: template[:render_path] } - respond_to do |format| - format.turbo_stream - format.html { head :no_content } - end + @url = edit_params[:url] end # :nocov: -> covered by manual system tests @@ -40,6 +37,13 @@ def update private + def edit_params + { + template: params.require(:template).permit(:path, render_path: []), + url: params.require(:url) + } + end + def valid_path?(path) expanded_path = File.expand_path path diff --git a/app/views/ferbe/templates/_editor.html.erb b/app/views/ferbe/templates/_editor.html.erb new file mode 100644 index 0000000..6d268fa --- /dev/null +++ b/app/views/ferbe/templates/_editor.html.erb @@ -0,0 +1,33 @@ +<%# locals: (url:, template:) %> + +
+
+ <%= link_to "❌", url, class: "ferbe__button" %> +

<%= Pathname.new(template[:path]).relative_path_from(Rails.root) %>

+
+
+<% template[:render_path].each_with_index do |path, level| %>
+<%= " " * level %>> <%= link_to(Pathname.new(path).relative_path_from(Rails.root),
+                                edit_template_url(url: ,
+                                                  template: {
+                                                    path: path,
+                                                    render_path: template[:render_path]
+                                                  }),
+                                data: { turbo_stream: true }) %>
+<% end %>
+
+ <%= form_with url: template_url, + method: :patch, + scope: :template, + data: { ferbe_editor_target: "form" } do |form| %> + <%= form.hidden_field :path, value: template[:path] %> + <%= form.hidden_field :content, + value: template[:content], + data: { ferbe_editor_target: "input" } %> +
<%= template[:content] %>
+ <%= form.submit "💾", class: "ferbe__button ferbe__save-button" %> + <% end %> +
diff --git a/app/views/ferbe/templates/edit.html.erb b/app/views/ferbe/templates/edit.html.erb new file mode 100644 index 0000000..adb1b6a --- /dev/null +++ b/app/views/ferbe/templates/edit.html.erb @@ -0,0 +1,11 @@ +
+ + + <%= tag.div class: "ferbe__editor", id: "ferbe-editor", data: { + modifier_key: Ferbe.configuration.modifier_key, + use_local_editor: Ferbe.configuration.use_local_editor, + turbo_permanent: true + } do %> + <%= render partial: "editor", locals: { url: @url, template: @template } %> + <% end %> +
diff --git a/app/views/ferbe/templates/edit.turbo_stream.erb b/app/views/ferbe/templates/edit.turbo_stream.erb index 8a57d47..7072e79 100644 --- a/app/views/ferbe/templates/edit.turbo_stream.erb +++ b/app/views/ferbe/templates/edit.turbo_stream.erb @@ -1,32 +1,3 @@ <%= turbo_stream.update "ferbe-editor" do %> -
-
- -

<%= Pathname.new(@template[:path]).relative_path_from(Rails.root) %>

-
-
-<% @template[:render_path].each_with_index do |path, level| %>
-<%= " " * level %>> <%= link_to( Pathname.new(path).relative_path_from(Rails.root),
-                                 edit_template_url(template: {
-                                   path: path,
-                                   render_path: @template[:render_path]
-                                 }),
-                                 data: { turbo_stream: true }) %>
-<% end %>
-
- <%= form_with url: template_url, - method: :patch, - scope: :template, - data: { ferbe_editor_target: "form" } do |form| %> - <%= form.hidden_field :path, value: @template[:path] %> - <%= form.hidden_field :content, - value: @template[:content], - data: { ferbe_editor_target: "input" } %> -
<%= @template[:content] %>
- <%= form.submit "💾", class: "ferbe__button ferbe__save-button" %> - <% end %> -

-  
+ <%= render partial: "editor", locals: { url: @url, template: @template } %> <% end %> diff --git a/app/views/layouts/ferbe/application.html.erb b/app/views/layouts/ferbe/application.html.erb new file mode 100644 index 0000000..6d862db --- /dev/null +++ b/app/views/layouts/ferbe/application.html.erb @@ -0,0 +1,21 @@ + + + + Ferbe Editor + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%= javascript_importmap_tags %> + <%= javascript_import_module_tag "ferbe/application" %> + + <%= stylesheet_link_tag "ferbe/highlight", media: "all" %> + <%= stylesheet_link_tag "ferbe/application", media: "all" %> + + + +<%= yield %> + + + diff --git a/config/ferbe_importmap.rb b/config/ferbe_importmap.rb index b130484..30e4723 100644 --- a/config/ferbe_importmap.rb +++ b/config/ferbe_importmap.rb @@ -1,8 +1,7 @@ +pin_all_from Ferbe::Engine.root.join("app/assets/javascripts/ferbe"), under: "ferbe" + pin "highlight.js/lib/core", to: "https://ga.jspm.io/npm:highlight.js@11.11.1/es/core.js" pin "highlight.js/lib/languages/erb", to: "https://ga.jspm.io/npm:highlight.js@11.11.1/es/languages/erb.js" pin "highlight.js/lib/languages/xml", to: "https://ga.jspm.io/npm:highlight.js@11.11.1/es/languages/xml.js" pin "highlight.js/lib/languages/ruby", to: "https://ga.jspm.io/npm:highlight.js@11.11.1/es/languages/ruby.js" pin "codejar", to: "https://ga.jspm.io/npm:codejar@4.2.0/dist/codejar.js" - -pin "ferbe", to: "ferbe/application.js" -pin_all_from Ferbe::Engine.root.join("app/assets/javascripts/ferbe"), under: "ferbe" diff --git a/lib/ferbe/helper.rb b/lib/ferbe/helper.rb index ad6771f..13d788a 100644 --- a/lib/ferbe/helper.rb +++ b/lib/ferbe/helper.rb @@ -4,26 +4,13 @@ module Helper def ferbe_styles_tag return unless Ferbe.configuration.enabled - capture do - concat stylesheet_link_tag "ferbe/highlight", media: "all" - concat stylesheet_link_tag "ferbe/application", media: "all" - end + stylesheet_link_tag "ferbe/host", media: "all" end def ferbe_javascript_tag return unless Ferbe.configuration.enabled - javascript_import_module_tag "ferbe" - end - - def ferbe_editor_tag - return unless Ferbe.configuration.enabled - - tag.div class: "ferbe__editor", id: "ferbe-editor", data: { - modifier_key: Ferbe.configuration.modifier_key, - use_local_editor: Ferbe.configuration.use_local_editor, - turbo_permanent: true - } + javascript_import_module_tag "ferbe/host" end #:nocov: end diff --git a/test/controllers/ferbe/templates_controller_test.rb b/test/controllers/ferbe/templates_controller_test.rb index 1a26a86..66a15df 100644 --- a/test/controllers/ferbe/templates_controller_test.rb +++ b/test/controllers/ferbe/templates_controller_test.rb @@ -45,17 +45,24 @@ class TemplatesControllerTest < ActionDispatch::IntegrationTest end end - test "edit as html returns no content" do + test "edit is available as turbo stream and html" do Tempfile.create(["test", ".html.erb"], Rails.root.join("app/views")) do |file| file.write "I am a template!" - get edit_template_url, params: {template: {path: file.path}} - assert_response :no_content + params = {template: {path: file.path, render_path: [file.path]}, url: "http://example.com"} + + get(edit_template_url, params:) + assert_response :success + + get(edit_template_url, params:, as: :turbo_stream) + assert_response :success end end - test "reject editing of invalid template" do - get edit_template_url, params: {template: {path: "some/invalid/file.txt"}} + test "rejection of editor for invalid template" do + invalid_path = "some/invalid/file.txt" + get edit_template_url, + params: {template: {path: invalid_path, render_path: [invalid_path]}, url: "http://example.com"} assert_response :bad_request end end diff --git a/test/dummy/app/assets/stylesheets/application.css b/test/dummy/app/assets/stylesheets/application.css index 1a1bd79..0556e93 100644 --- a/test/dummy/app/assets/stylesheets/application.css +++ b/test/dummy/app/assets/stylesheets/application.css @@ -7,6 +7,10 @@ main { margin-block: 1rem; padding: 1rem; min-height: 3rem; + + &.is-outlined { + border: 0.25rem solid; + } } .red { @@ -24,19 +28,22 @@ main { opacity: 0.5; } -.traffic-light { - border: 0.25rem solid; +.brown { + background-color: var(--primary); + opacity: 0.5; +} - &.is-horizontal { - display: flex; - gap: 1rem; +.is-horizontal { + display: flex; + gap: 1rem; - * { - flex-grow: 1; - } + * { + flex-grow: 1; } } -.tower { +.grid { + display: grid; + grid-template-columns: 1fr 1fr; border: 0.25rem solid; } diff --git a/test/dummy/app/javascript/controllers/index.js b/test/dummy/app/javascript/controllers/index.js index f913136..625b998 100644 --- a/test/dummy/app/javascript/controllers/index.js +++ b/test/dummy/app/javascript/controllers/index.js @@ -1,7 +1,5 @@ // Import and register all your controllers from the importmap via controllers/**/*_controller import { application } from "controllers/application"; import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"; -import FerbeEditorController from "ferbe/controllers/ferbe_editor_controller"; eagerLoadControllersFrom("controllers", application); -application.register("ferbe-editor", FerbeEditorController); diff --git a/test/dummy/app/views/home/index.html.erb b/test/dummy/app/views/home/index.html.erb index 9986507..636f278 100644 --- a/test/dummy/app/views/home/index.html.erb +++ b/test/dummy/app/views/home/index.html.erb @@ -1,5 +1,7 @@

Welcome to the Homepage

+<%= render partial: "shared/grid" %> + <%= render partial: "shared/horizontal_traffic_light" %> <%= render partial: "shared/traffic_light" %> diff --git a/test/dummy/app/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb index 74a52bd..d6ada37 100644 --- a/test/dummy/app/views/layouts/application.html.erb +++ b/test/dummy/app/views/layouts/application.html.erb @@ -20,17 +20,14 @@ <%= stylesheet_link_tag :app %> <%= ferbe_styles_tag %> + <%= javascript_importmap_tags %> <%= ferbe_javascript_tag %> -
<%= yield %>
- - <%= ferbe_editor_tag %> -
diff --git a/test/dummy/app/views/shared/_grid.html.erb b/test/dummy/app/views/shared/_grid.html.erb new file mode 100644 index 0000000..9698a3a --- /dev/null +++ b/test/dummy/app/views/shared/_grid.html.erb @@ -0,0 +1,5 @@ +
+ <%= render partial: "shared/colors/red" %> + <%= render partial: "shared/colors/two_brown" %> + <%= render partial: "shared/colors/green" %> +
diff --git a/test/dummy/app/views/shared/_horizontal_traffic_light.html.erb b/test/dummy/app/views/shared/_horizontal_traffic_light.html.erb index c52a3cc..d25cdb8 100644 --- a/test/dummy/app/views/shared/_horizontal_traffic_light.html.erb +++ b/test/dummy/app/views/shared/_horizontal_traffic_light.html.erb @@ -1,4 +1,4 @@ -
+
<%= render partial: "shared/colors/red" %> <%= render partial: "shared/colors/orange" %> <%= render partial: "shared/colors/green" %> diff --git a/test/dummy/app/views/shared/_tower.html.erb b/test/dummy/app/views/shared/_tower.html.erb index dcfffad..cb7c60b 100644 --- a/test/dummy/app/views/shared/_tower.html.erb +++ b/test/dummy/app/views/shared/_tower.html.erb @@ -1,6 +1,6 @@ <%# locals: (counter:) %> <% counter -= 1 %> -
+
<%= render partial: "shared/tower", locals: { counter: counter } unless counter <= 0 %>
diff --git a/test/dummy/app/views/shared/_traffic_light.html.erb b/test/dummy/app/views/shared/_traffic_light.html.erb index b2122b0..d939fb2 100644 --- a/test/dummy/app/views/shared/_traffic_light.html.erb +++ b/test/dummy/app/views/shared/_traffic_light.html.erb @@ -1,4 +1,4 @@ -
+
<%= render partial: "shared/colors/red" %> <%= render partial: "shared/colors/orange" %> <%= render partial: "shared/colors/green" %> diff --git a/test/dummy/app/views/shared/colors/_two_brown.html.erb b/test/dummy/app/views/shared/colors/_two_brown.html.erb new file mode 100644 index 0000000..5980136 --- /dev/null +++ b/test/dummy/app/views/shared/colors/_two_brown.html.erb @@ -0,0 +1,2 @@ +
+