Skip to content

Commit 25c420b

Browse files
committed
Support upload of base64 image data to snippet with active storage
Use Stimulus to capture 'turbo:before-fetch-request' event to modify the request body. This event allows for pausing and resuming the form submit event on the Snippet form asynchronously. This enables us to use the html-to-image library to render the Snippet html as a base64 data uri image and append to the form submit request body. On the backend, we enable the attachment by converting the base64 data to image data and attaching to the Snippet.
1 parent 3c8cc76 commit 25c420b

File tree

12 files changed

+89
-18
lines changed

12 files changed

+89
-18
lines changed

app/controllers/snippets_controller.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def create
4242
def update
4343
@snippet = Snippet.find(params[:id])
4444
if @snippet.update(snippet_params)
45+
@snippet.attach_screenshot_from_base64(params[:screenshot]) if params[:screenshot]
4546
redirect_to @snippet, notice: "Snippet was successfully updated.", status: :see_other
4647
else
4748
render :edit, status: :unprocessable_entity
@@ -59,7 +60,7 @@ def destroy
5960

6061
# Only allow a list of trusted parameters through.
6162
def snippet_params
62-
params.fetch(:snippet, {}).permit(:filename, :source, :url, :language)
63+
params.fetch(:snippet, {}).permit(:filename, :source, :language)
6364
end
6465

6566
def feature_enabled!

app/javascript/controllers/snippets/editor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default class extends Controller {
3535
this.autogrow();
3636

3737
this.element.addEventListener('click', this.enableEditMode);
38+
3839
this.textareaTarget.addEventListener('blur', this.preview);
3940
this.textareaTarget.addEventListener('input', this.autogrow);
4041
window.addEventListener('resize', this.onResize);

app/javascript/controllers/snippets/preview.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
import { Controller } from '@hotwired/stimulus';
2+
import * as htmlToImage from 'html-to-image';
23

34
import debug from '../../utils/debug';
45

56
const console = debug('app:javascript:controllers:snippets:preview');
67

7-
export default class extends Controller {
8+
export default class extends Controller<HTMLFormElement> {
89
// Make timeout type play nice with TypeScript
910
// based on https://donatstudios.com/TypeScriptTimeoutTrouble
1011
private idleTimeout?: ReturnType<typeof setTimeout>;
1112

12-
static targets = ['previewButton'];
13+
static targets = ['previewButton', 'snippet'];
1314

1415
declare readonly hasPreviewButtonTarget: boolean;
1516
declare readonly previewButtonTarget: HTMLInputElement;
1617
declare readonly previewButtonTargets: HTMLInputElement[];
1718

19+
declare readonly hasSnippetTarget: boolean;
20+
declare readonly snippetTarget: HTMLInputElement;
21+
declare readonly snippetTargets: HTMLInputElement[];
22+
1823
connect(): void {
1924
console.log('Connect!');
25+
26+
this.element.addEventListener(
27+
'turbo:before-fetch-request',
28+
this.prepareScreenshot,
29+
);
2030
}
2131

2232
disconnect(): void {
@@ -37,4 +47,34 @@ export default class extends Controller {
3747
console.log('Click preview button!');
3848
this.previewButtonTarget.click();
3949
};
50+
51+
share = async (event: CustomEvent) => {
52+
console.log('Share!');
53+
event.preventDefault();
54+
55+
const data = await htmlToImage.toPng(this.snippetTarget);
56+
57+
const input = document.createElement('input');
58+
input.type = 'hidden';
59+
input.name = 'snippet[screenshot]';
60+
input.value = data;
61+
62+
this.element.requestSubmit();
63+
};
64+
65+
prepareScreenshot = async (event) => {
66+
event.preventDefault();
67+
68+
if (event.detail.fetchOptions.body instanceof URLSearchParams) {
69+
const data = await this.drawScreenshot();
70+
71+
event.detail.fetchOptions.body.append('screenshot', data);
72+
}
73+
74+
event.detail.resume();
75+
};
76+
77+
drawScreenshot = async () => {
78+
return htmlToImage.toPng(this.snippetTarget);
79+
};
4080
}

app/javascript/css/components/article-content.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
padding: 0;
6161
margin: 0;
6262
z-index: 100;
63-
max-width: calc(var(--grid-max-width) / 3);
63+
max-width: var(--grid-max-width);
6464
}
6565

6666
& ul {

app/models/snippet.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
1+
require_relative "../../lib/image_data_uri"
12
class Snippet < ApplicationRecord
23
validates :language, presence: true
34

45
validate :recognized_language
56
before_validation :auto_detect_language, if: :auto_detecting?
67

8+
has_one_attached :screenshot
9+
710
attr_reader :auto_detecting
811

12+
def attach_screenshot_from_base64(data)
13+
data_uri = ImageDataUri.new(data)
14+
image_filename = filename.presence || SecureRandom.uuid
15+
image_filename = [image_filename.parameterize, data_uri.extension].join
16+
17+
screenshot.attach(
18+
io: StringIO.new(data_uri.file_data),
19+
content_type: data_uri.content_type,
20+
filename: image_filename
21+
)
22+
end
23+
924
def language=(value)
1025
if value == "auto"
1126
@auto_detecting = true

app/views/components/code_block/snippet.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ class CodeBlock::Snippet < ApplicationComponent
33
include Phlex::Rails::Helpers::DOMID
44
prepend CodeBlock::AtomAware
55

6-
attr_reader :snippet
6+
attr_reader :snippet, :data
77

8-
def initialize(snippet, editing: false, **)
8+
def initialize(snippet, editing: false, data: {}, **)
99
@snippet = snippet
1010
@editing = editing
11+
@data = data
1112
end
1213

1314
def view_template
14-
div(class: "snippet-background") do
15+
div(class: "snippet-background", data:) do
1516
render CodeBlock::Container.new(language: language, class: "snippet") do
1617
render CodeBlock::Header.new do
1718
if editing?
@@ -22,7 +23,7 @@ def view_template
2223
end
2324
end
2425

25-
render CodeBlock::Body.new(data: controller_data) do
26+
render CodeBlock::Body.new(data: editor_data) do
2627
div(class: "grid-stack") do
2728
render CodeBlock::Code.new(source, language: language, data: {snippet_editor_target: "source"})
2829
if editing?
@@ -57,7 +58,7 @@ def editing? = !!@editing
5758

5859
delegate :language, :filename, to: :snippet
5960

60-
def controller_data
61+
def editor_data
6162
editing? ? {controller: "snippet-editor"} : {}
6263
end
6364
end

app/views/snippets/_form.html.erb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
</div>
2727

2828
<%= turbo_frame_tag dom_id(snippet, :code_block), class: "snippet-frame grid-cols-12" do %>
29-
<%= render CodeBlock::Snippet.new(snippet, editing: true) %>
29+
<%= render CodeBlock::Snippet.new(snippet, editing: true, data: { snippet_preview_target: "snippet" }) %>
3030
<% end %>
3131

3232
<fieldset>
@@ -39,6 +39,8 @@
3939
snippet_preview_target: "previewButton",
4040
turbo_frame: dom_id(snippet, :code_block)
4141
} %>
42+
43+
<%= form.button "Share", class: "button secondary", data: { action: "snippet-preview#share" } %>
4244
</fieldset>
4345
<% end %>
4446

app/views/snippets/index.html.erb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66

77
<% @snippets.each do |snippet| %>
88
<%= link_to snippet, class: "block" do %>
9-
<%= render CodeBlock::Snippet.new(snippet) %>
9+
<% if snippet.screenshot.attached? %>
10+
<%= image_tag snippet.screenshot %>
11+
<% else %>
12+
<%= render CodeBlock::Snippet.new(snippet) %>
13+
<% end %>
1014
<% end %>
1115
<% end %>
1216
</div>

app/views/snippets/show.html.erb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<div class="section-content container py-gap">
33
<%= render CodeBlock::Snippet.new(@snippet) %>
44

5+
<%= image_tag @snippet.screenshot if @snippet.screenshot.attached? %>
6+
57
<div>
68
<%= link_to "Edit this snippet", edit_snippet_path(@snippet) %> |
79
<%= link_to "Back to snippets", snippets_path %>

config/initializers/filter_parameter_logging.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
# Use this to limit dissemination of sensitive information.
55
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
66
Rails.application.config.filter_parameters += [
7-
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
7+
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :screenshot
88
]

0 commit comments

Comments
 (0)