Skip to content

Commit ba200af

Browse files
committed
Iterate on snippet editor autogrow js and css
1 parent f145657 commit ba200af

File tree

8 files changed

+169
-47
lines changed

8 files changed

+169
-47
lines changed
Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Controller } from '@hotwired/stimulus';
22

3+
import { debounce } from '../../utils/debounce';
34
import debug from '../../utils/debug';
45

56
const console = debug('app:javascript:controllers:snippets:editor');
@@ -11,15 +12,34 @@ export default class extends Controller {
1112
declare sourceTarget: HTMLElement;
1213
declare textareaTarget: HTMLTextAreaElement;
1314

15+
declare onResize: EventListenerOrEventListenerObject;
16+
declare resizeDebounceDelayValue: number;
17+
18+
static values = {
19+
resizeDebounceDelay: {
20+
type: Number,
21+
default: 100,
22+
},
23+
};
24+
1425
connect() {
1526
console.log('Stimulus controller connected');
16-
this.showCode();
17-
this.syncTextarea();
1827
// this.textareaTarget.addEventListener('input', this.autoGrow.bind(this));
1928
// this.textareaTarget.addEventListener(
2029
// 'blur',
2130
// this.handleTextareaBlur.bind(this),
2231
// );
32+
33+
this.element.addEventListener('click', this.toggleEditMode.bind(this));
34+
this.textareaTarget.style.overflow = 'hidden';
35+
const delay: number = this.resizeDebounceDelayValue;
36+
37+
// this.onResize = delay > 0 ? debounce(this.autogrow, delay) : this.autogrow;
38+
39+
this.autogrow();
40+
41+
// this.textareaTarget.addEventListener('input', this.autogrow);
42+
// window.addEventListener('resize', this.onResize);
2343
}
2444

2545
disconnect() {
@@ -30,29 +50,32 @@ export default class extends Controller {
3050
// );
3151
}
3252

33-
preview() {
34-
this.previewButtonTarget.click();
35-
}
53+
// sync() {
54+
// this.sourceTarget.innerHTML = this.textareaTarget.value;
55+
// this.textareaTarget.style.width = `${this.sourceTarget.offsetWidth}px`;
56+
// }
3657

37-
showTextarea() {
38-
this.sourceTarget.style.display = 'none';
39-
this.textareaTarget.style.display = 'block';
40-
this.textareaTarget.focus();
41-
this.autoGrow();
58+
toggleEditMode(event: Event) {
59+
this.textareaTarget.style.visibility = 'visible';
4260
}
4361

44-
syncTextarea() {
45-
this.textareaTarget.value = this.sourceTarget.textContent || '';
46-
this.autoGrow();
47-
}
62+
autogrow = () => {
63+
// Force re-print before calculating the value.
64+
// this.textareaTarget.style.height = 'auto';
65+
// this.textareaTarget.style.width = 'auto';
4866

49-
showCode() {
50-
this.sourceTarget.style.display = 'block';
51-
this.textareaTarget.style.display = 'none';
52-
}
67+
if (this.textareaTarget.parentNode instanceof HTMLElement) {
68+
this.textareaTarget.parentNode.dataset.replicatedValue =
69+
this.textareaTarget.value;
70+
}
71+
72+
// this.sourceTarget.innerHTML = this.textareaTarget.value + ' ';
73+
// this.textareaTarget.style.width = `${this.sourceTarget.scrollWidth}px`;
5374

54-
autoGrow() {
55-
this.textareaTarget.style.height = 'auto';
56-
this.textareaTarget.style.height = this.textareaTarget.scrollHeight + 'px';
75+
// this.textareaTarget.style.height = `${this.textareaTarget.scrollHeight}px`;
76+
};
77+
78+
preview() {
79+
this.previewButtonTarget.click();
5780
}
5881
}

app/javascript/css/components/code.scss

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
@import '../config/variables.scss';
22

33
.code-wrapper {
4+
--code-line-height: 1.7777778;
5+
46
border-radius: var(--space-3xs-2xs);
57
border: 1px solid var(--joy-border-quiet);
68
}
@@ -22,25 +24,29 @@
2224
position: relative;
2325

2426
& pre {
25-
line-height: 1.7777778;
26-
margin-top: 0;
27-
margin-bottom: 0;
2827
border-radius: 0.5rem;
29-
background-color: inherit;
30-
}
31-
32-
& pre,
33-
& textarea {
3428
padding-top: var(--space-m);
3529
padding-inline-end: var(--space-m);
3630
padding-bottom: var(--space-m);
3731
padding-inline-start: var(--space-m);
3832
}
3933

40-
textarea {
41-
width: 100%;
34+
& pre,
35+
& textarea {
36+
background-color: inherit;
37+
line-height: var(--code-line-height);
38+
margin-top: 0;
39+
margin-bottom: 0;
40+
}
41+
42+
/* https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ */
43+
44+
& textarea {
4245
font-family: var(--monospace);
4346
border: none;
47+
color: white;
48+
resize: none;
49+
padding: 0;
4450
}
4551
}
4652

@@ -169,11 +175,64 @@
169175
}
170176

171177
.snippet {
172-
.code-body {
173-
& code {
174-
word-wrap: break-word;
175-
white-space: pre-wrap;
176-
word-break: normal;
177-
}
178+
width: fit-content;
179+
max-width: var(--grid-max-width);
180+
181+
& .code-body {
182+
flex-direction: column;
183+
}
184+
185+
& .code-editor {
186+
padding-top: var(--space-m);
187+
padding-inline-end: var(--space-m);
188+
padding-bottom: var(--space-m);
189+
padding-inline-start: var(--space-m);
178190
}
179191
}
192+
193+
.grid-stack {
194+
display: grid;
195+
196+
& > * {
197+
grid-area: 1 / 1 / 2 / 2;
198+
}
199+
}
200+
201+
/*
202+
.autogrow-wrapper taken from
203+
https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/
204+
*/
205+
.autogrow-wrapper {
206+
/* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */
207+
display: grid;
208+
}
209+
210+
.autogrow-wrapper::after {
211+
/* Note the weird space! Needed to preventy jumpy behavior */
212+
content: attr(data-replicated-value) ' ';
213+
214+
/* Hidden from view, clicks, and screen readers */
215+
visibility: hidden;
216+
217+
line-height: var(--code-line-height);
218+
font-family: var(--monospace);
219+
font-size: var(--step--1);
220+
}
221+
222+
.autogrow-wrapper > textarea {
223+
/* You could leave this, but after a user resizes, then it ruins the auto sizing */
224+
resize: none;
225+
226+
/* Firefox shows scrollbar on growth, you can hide like this. */
227+
overflow: hidden;
228+
}
229+
230+
.autogrow-wrapper > textarea,
231+
.autogrow-wrapper::after {
232+
/* Place on top of each other */
233+
grid-area: 1 / 1 / 2 / 2;
234+
235+
white-space: pre-wrap;
236+
word-wrap: break-word;
237+
max-width: var(--grid-max-width);
238+
}

app/javascript/css/utilities/tailwind.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,3 +994,26 @@ focus\:ring-0:focus {
994994
display: inherit;
995995
}
996996
}
997+
998+
.resize-none {
999+
resize: none;
1000+
}
1001+
.resize-y {
1002+
resize: vertical;
1003+
}
1004+
.resize-x {
1005+
resize: horizontal;
1006+
}
1007+
.resize {
1008+
resize: both;
1009+
}
1010+
1011+
.visible {
1012+
visibility: visible;
1013+
}
1014+
.invisible {
1015+
visibility: hidden;
1016+
}
1017+
.collapse {
1018+
visibility: collapse;
1019+
}

app/javascript/utils/debounce.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
function debounce(callback: Function, delay: number) {
2+
let timeout: number;
3+
4+
return (...args) => {
5+
const context = this;
6+
window.clearTimeout(timeout);
7+
8+
timeout = window.setTimeout(() => callback.apply(context, args), delay);
9+
};
10+
}
11+
12+
export { debounce };

app/views/components/code_block/code.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def code_formatter
77

88
attr_reader :source, :language, :data
99

10-
def initialize(source = nil, language: nil, data: {})
10+
def initialize(source = "", language: nil, data: {})
1111
@source = source
1212
@language = language
1313
@data = data

app/views/components/code_block/snippet.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
class CodeBlock::Snippet < ApplicationComponent
22
include Phlex::DeferredRender
3+
include Phlex::Rails::Helpers::DOMID
34
prepend CodeBlock::AtomAware
45

56
attr_reader :snippet
@@ -9,11 +10,17 @@ def initialize(snippet, **)
910
end
1011

1112
def view_template
12-
render CodeBlock::Container.new(language: language) do
13+
render CodeBlock::Container.new(language: language, class: "snippet", id: dom_id(snippet, :code_block)) do
1314
render CodeBlock::Header.new { title_content }
1415

15-
render CodeBlock::Body.new do
16+
render CodeBlock::Body.new(data: {controller: "snippet-editor"}) do
1617
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 }
23+
end
1724
end
1825
end
1926
end

app/views/snippets/_form.html.erb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<%= form_with(
22
model: snippet,
3-
data: { controller: "snippet-preview", action: "input->snippet-preview#preview" },
43
class: "grid-content"
54
) do |form| %>
65
<% if snippet.errors.any? %>
@@ -36,6 +35,10 @@
3635
</code>
3736
</fieldset>
3837

38+
<%= turbo_frame_tag dom_id(snippet, :code_block), class: "grid-cols-12" do %>
39+
<%= render snippet %>
40+
<% end %>
41+
3942
<fieldset>
4043
<%= form.submit class: "button primary" %>
4144
<%= form.submit "Preview", class: "button secondary hidden",
@@ -50,7 +53,4 @@
5053
<% end %>
5154

5255
<div class="grid-content">
53-
<%= turbo_frame_tag dom_id(snippet, :code_block) do %>
54-
<%= render snippet %>
55-
<% end %>
5656
</div>

app/views/snippets/_snippet.html.erb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
<div id="<%= dom_id(snippet, :code_block) %>">
2-
<%= render CodeBlock::Snippet.new(snippet) %>
3-
</div>
1+
<%= render CodeBlock::Snippet.new(snippet) %>

0 commit comments

Comments
 (0)