Skip to content

Commit fe2f603

Browse files
committed
Ability to preview snippet while editing
1 parent ba200af commit fe2f603

File tree

8 files changed

+122
-76
lines changed

8 files changed

+122
-76
lines changed

app/controllers/snippets_controller.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ def show
1313

1414
# GET /snippets/new
1515
def new
16-
@snippet = Snippet.new(snippet_params)
16+
default_params = {
17+
filename: "app/models/user.rb",
18+
source: "class User < ApplicationRecord\n\s\shas_many :posts\nend",
19+
language: "ruby"
20+
}
21+
@snippet = Snippet.new(snippet_params.presence || default_params)
1722
end
1823

1924
# GET /snippets/1/edit

app/javascript/controllers/snippets/editor.ts

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,58 +24,47 @@ export default class extends Controller {
2424

2525
connect() {
2626
console.log('Stimulus controller connected');
27-
// this.textareaTarget.addEventListener('input', this.autoGrow.bind(this));
28-
// this.textareaTarget.addEventListener(
29-
// 'blur',
30-
// this.handleTextareaBlur.bind(this),
31-
// );
3227

33-
this.element.addEventListener('click', this.toggleEditMode.bind(this));
28+
this.disableEditMode();
29+
3430
this.textareaTarget.style.overflow = 'hidden';
3531
const delay: number = this.resizeDebounceDelayValue;
3632

37-
// this.onResize = delay > 0 ? debounce(this.autogrow, delay) : this.autogrow;
33+
this.onResize = delay > 0 ? debounce(this.autogrow, delay) : this.autogrow;
3834

3935
this.autogrow();
4036

41-
// this.textareaTarget.addEventListener('input', this.autogrow);
42-
// window.addEventListener('resize', this.onResize);
37+
this.element.addEventListener('click', this.enableEditMode);
38+
this.textareaTarget.addEventListener('blur', this.preview);
39+
this.textareaTarget.addEventListener('input', this.autogrow);
40+
window.addEventListener('resize', this.onResize);
4341
}
4442

4543
disconnect() {
46-
// this.textareaTarget.removeEventListener('input', this.autoGrow.bind(this));
47-
// this.textareaTarget.removeEventListener(
48-
// 'blur',
49-
// this.handleTextareaBlur.bind(this),
50-
// );
44+
window.removeEventListener('resize', this.onResize);
5145
}
5246

53-
// sync() {
54-
// this.sourceTarget.innerHTML = this.textareaTarget.value;
55-
// this.textareaTarget.style.width = `${this.sourceTarget.offsetWidth}px`;
56-
// }
57-
58-
toggleEditMode(event: Event) {
47+
enableEditMode = () => {
48+
this.textareaTarget.disabled = false;
5949
this.textareaTarget.style.visibility = 'visible';
60-
}
50+
this.sourceTarget.style.visibility = 'hidden';
51+
};
6152

62-
autogrow = () => {
63-
// Force re-print before calculating the value.
64-
// this.textareaTarget.style.height = 'auto';
65-
// this.textareaTarget.style.width = 'auto';
53+
disableEditMode = () => {
54+
this.textareaTarget.style.visibility = 'hidden';
55+
this.sourceTarget.style.visibility = 'visible';
56+
};
6657

58+
autogrow = () => {
6759
if (this.textareaTarget.parentNode instanceof HTMLElement) {
6860
this.textareaTarget.parentNode.dataset.replicatedValue =
6961
this.textareaTarget.value;
7062
}
71-
72-
// this.sourceTarget.innerHTML = this.textareaTarget.value + ' ';
73-
// this.textareaTarget.style.width = `${this.sourceTarget.scrollWidth}px`;
74-
75-
// this.textareaTarget.style.height = `${this.textareaTarget.scrollHeight}px`;
7663
};
7764

78-
preview() {
79-
this.previewButtonTarget.click();
80-
}
65+
preview = () => {
66+
this.dispatch('changed', {
67+
detail: { content: this.textareaTarget.value },
68+
});
69+
};
8170
}

app/javascript/controllers/snippets/preview.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@ export default class extends Controller {
99
// based on https://donatstudios.com/TypeScriptTimeoutTrouble
1010
private idleTimeout?: ReturnType<typeof setTimeout>;
1111

12-
static targets = ['source', 'previewButton'];
13-
14-
declare readonly hasSourceTarget: boolean;
15-
declare readonly sourceTarget: HTMLInputElement;
16-
declare readonly sourceTargets: HTMLInputElement[];
12+
static targets = ['previewButton'];
1713

1814
declare readonly hasPreviewButtonTarget: boolean;
1915
declare readonly previewButtonTarget: HTMLInputElement;

app/javascript/css/components/code.scss

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55

66
border-radius: var(--space-3xs-2xs);
77
border: 1px solid var(--joy-border-quiet);
8+
9+
& input,
10+
& textarea {
11+
border: none;
12+
color: white;
13+
resize: none;
14+
padding: 0;
15+
background-color: inherit;
16+
}
817
}
918

1019
@media screen and (min-width: $screen-md) {
@@ -33,29 +42,25 @@
3342

3443
& pre,
3544
& textarea {
36-
background-color: inherit;
3745
line-height: var(--code-line-height);
3846
margin-top: 0;
3947
margin-bottom: 0;
4048
}
4149

42-
/* https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ */
43-
4450
& textarea {
4551
font-family: var(--monospace);
46-
border: none;
47-
color: white;
48-
resize: none;
49-
padding: 0;
5052
}
5153
}
5254

5355
.code-header {
56+
--code-header-font-size: 0.875rem;
57+
--code-header-line-height: 1.5715;
58+
5459
line-height: 1;
5560
display: none;
5661
padding: 1.5rem 1.5rem 0.5rem;
57-
font-size: 0.875rem;
58-
line-height: 1.5715;
62+
font-size: var(--code-header-font-size);
63+
line-height: var(--code-header-line-height);
5964
/* color: rgb(156 163 175 / var(--tw-text-opacity)); */
6065

6166
& .code-title {
@@ -64,6 +69,12 @@
6469
text-align: center;
6570
line-height: 1;
6671

72+
& input {
73+
font-size: var(--code-header-font-size);
74+
line-height: var(--code-header-line-height);
75+
text-align: right;
76+
}
77+
6778
& a {
6879
font-family: var(--sans-serif);
6980
gap: 0.25rem;
@@ -220,6 +231,8 @@
220231
}
221232

222233
.autogrow-wrapper > textarea {
234+
min-height: auto;
235+
223236
/* You could leave this, but after a user resizes, then it ruins the auto sizing */
224237
resize: none;
225238

app/views/components/code_block/code.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ def initialize(source = "", language: nil, data: {})
1414
end
1515

1616
def view_template(&block)
17-
pre do
18-
code data: data do
17+
pre data: data do
18+
code do
1919
unsafe_raw self.class.code_formatter.format(lexer.lex(source))
2020
end
2121
end

app/views/components/code_block/snippet.rb

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,22 @@ def initialize(snippet, **)
1010
end
1111

1212
def view_template
13-
render CodeBlock::Container.new(language: language, class: "snippet", id: dom_id(snippet, :code_block)) do
14-
render CodeBlock::Header.new { title_content }
13+
render CodeBlock::Container.new(language: language, class: "snippet") do
14+
render CodeBlock::Header.new do
15+
label(class: "sr-only", for: "snippet[filename]") { "Filename" }
16+
input(type: "text", name: "snippet[filename]", value: filename)
17+
end
1518

1619
render CodeBlock::Body.new(data: {controller: "snippet-editor"}) do
17-
render CodeBlock::Code.new(source, language: language)
18-
div(class: "code-editor autogrow-wrapper") do
19-
textarea(
20-
name: "snippet[source]",
21-
data: {snippet_editor_target: "textarea", action: "input->snippet-editor#autogrow"}
22-
) { source }
20+
div(class: "grid-stack") do
21+
render CodeBlock::Code.new(source, language: language, data: {snippet_editor_target: "source"})
22+
label(class: "sr-only", for: "snippet[source]") { "Source" }
23+
div(class: "code-editor autogrow-wrapper") do
24+
textarea(
25+
name: "snippet[source]",
26+
data: {snippet_editor_target: "textarea"}
27+
) { source }
28+
end
2329
end
2430
end
2531
end

app/views/snippets/_form.html.erb

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<%= form_with(
22
model: snippet,
3-
class: "grid-content"
3+
class: "grid-content",
4+
data: { controller: "snippet-preview", action: "snippet-editor:changed->snippet-preview#preview" }
45
) do |form| %>
56
<% if snippet.errors.any? %>
67
<div style="color: red">
@@ -15,27 +16,16 @@
1516
<% end %>
1617

1718
<div class="flex items-start flex-col space-col-4 md:items-center md:flex-row md:space-row-4">
18-
<fieldset class="flex-grow">
19-
<%= form.label :filename, class: "block" %>
20-
<%= form.text_field :filename, class: "flex-1 rounded bg-white/5 focus-ring focus:ring-0 ring-1 ring-inset ring-white/10 w-full lg:min-w-[36ch] " %>
21-
</fieldset>
22-
2319
<fieldset>
24-
<%= form.label :language, class: "block" %>
25-
<%= form.select :language, [["Auto", "auto"]] + Rouge::Lexer.all.map { |lexer| [lexer.title, lexer.tag] }, class: "flex-1 rounded bg-white/5 focus-ring focus:ring-0 ring-1 ring-inset ring-white/10 w-full lg:min-w-[36ch]" %>
20+
<%= form.label :language, class: "sr-only" %>
21+
<%= form.select :language, [["Auto", "auto"]] + Rouge::Lexer.all.map { |lexer| [lexer.title, lexer.tag] },
22+
{},
23+
class: "flex-1 rounded bg-white/5 focus-ring focus:ring-0 ring-1 ring-inset ring-white/10 w-full lg:min-w-[36ch]",
24+
data: { action: "change->snippet-preview#preview" } %>
2625
</fieldset>
2726
</div>
2827

29-
<fieldset>
30-
<%= form.label :source, class: "block" %>
31-
<code>
32-
<%= form.text_area :source,
33-
# data: { action: "input->snippet-preview#handleChange" },
34-
class: "flex-1 rounded bg-white/5 focus-ring focus:ring-0 ring-1 ring-inset ring-white/10 w-full lg:min-w-[36ch]" %>
35-
</code>
36-
</fieldset>
37-
38-
<%= turbo_frame_tag dom_id(snippet, :code_block), class: "grid-cols-12" do %>
28+
<%= turbo_frame_tag dom_id(snippet, :code_block), class: "snippet-frame grid-cols-12" do %>
3929
<%= render snippet %>
4030
<% end %>
4131

spec/system/snippets_spec.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require "rails_helper"
2+
3+
RSpec.describe "Snippets", type: :system do
4+
before do
5+
Flipper.enable(:snippets)
6+
end
7+
8+
it "can creat and edit a snippet" do
9+
login_as_user
10+
11+
visit snippets_path
12+
13+
click_link "New snippet"
14+
15+
fill_in "snippet[filename]", with: "app/models/user.rb"
16+
select "Ruby", from: "Language"
17+
18+
find("[data-controller=snippet-editor]").click
19+
20+
fill_in "snippet[source]", with: "class User\nend"
21+
22+
click_button "Create Snippet"
23+
24+
expect(page).to have_content("Snippet was successfully created.")
25+
26+
snippet = Snippet.last
27+
expect(snippet.source).to eq("class User\nend")
28+
expect(snippet.language).to eq("ruby")
29+
expect(snippet.filename).to eq("app/models/user.rb")
30+
31+
click_link "Edit"
32+
33+
fill_in "snippet[filename]", with: "app/models/admin_user.rb"
34+
35+
find("[data-controller=snippet-editor]").click
36+
fill_in "snippet[source]", with: "class User\n has_many :posts\nend"
37+
38+
click_button "Update Snippet"
39+
40+
expect(page).to have_content("Snippet was successfully updated.")
41+
42+
snippet.reload
43+
expect(snippet.source).to eq("class User\n has_many :posts\nend")
44+
expect(snippet.language).to eq("ruby")
45+
expect(snippet.filename).to eq("app/models/admin_user.rb")
46+
end
47+
end

0 commit comments

Comments
 (0)