Skip to content

Commit 36452be

Browse files
authored
feat: add examples/templates when adding a new tab (#129)
* feat: add examples/templates when adding a new tab * fix: layout shift when closing tabs while editing * fix: general improvements * fix: spell check in code preview
1 parent 3aedfe1 commit 36452be

File tree

4 files changed

+205
-33
lines changed

4 files changed

+205
-33
lines changed

src/routes/+layout.marko

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ html lang="en"
1414

1515
prefetch-links
1616

17-
body
17+
body spellcheck="false"
1818
div#root
1919
app-header
2020
main
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
export default [
2+
{
3+
name: "counter",
4+
content: `/**
5+
* This component leverages Marko's controllable pattern!
6+
*
7+
* To hoist the value out when using it, you can optionally
8+
* bind a value like this:
9+
* \`\`\`
10+
* <let/myCount=0>
11+
* <counter value:=myCount/>
12+
* \`\`\`
13+
*/
14+
15+
<let/count:=input.value>
16+
<button onClick() { count++ }>
17+
\${count}
18+
</button>
19+
`,
20+
},
21+
{
22+
name: "let-localstorage",
23+
content: `/**
24+
* This can be used just like any other \`<let>\` tag, except
25+
* the value is synced over \`localStorage\` so it stays
26+
* constant after a page reload!
27+
*
28+
* Just add a \`key=\` attribute:
29+
* \`\`\`
30+
* <let-localstorage/count=0 key="my-count"/>
31+
* \`\`\`
32+
*/
33+
34+
<let/internalValue=input.value>
35+
36+
<script>
37+
const existingValue = localStorage.getItem(input.key);
38+
if (existingValue) {
39+
internalValue = JSON.parse(existingValue);
40+
}
41+
window.addEventListener(
42+
"local-storage-update",
43+
({ detail }) => {
44+
if (detail.key === input.key) {
45+
internalValue = JSON.parse(detail.value ?? "null");
46+
}
47+
},
48+
{
49+
signal: $signal,
50+
},
51+
);
52+
</script>
53+
54+
<return=internalValue valueChange(newValue) {
55+
const stringified = JSON.stringify(newValue);
56+
localStorage.setItem(input.key, stringified);
57+
window.dispatchEvent(
58+
new CustomEvent("local-storage-update", {
59+
detail: {
60+
key: input.key,
61+
value: stringified,
62+
},
63+
}),
64+
);
65+
}>
66+
`,
67+
},
68+
{
69+
name: "pointer-down",
70+
content: `/**
71+
* Uses reactive scripts and $signal to wire up document-wide
72+
* event listeners with automatic clean-up
73+
*
74+
* Example usage:
75+
* \`\`\`
76+
* <pointer-down/clicking/>
77+
* <if=clicking>Clicking!</if>
78+
* \`\`\`
79+
*/
80+
81+
<let/down=false>
82+
83+
<script>
84+
document.addEventListener("pointerdown", () => {
85+
down = true;
86+
}, { signal: $signal });
87+
document.addEventListener("pointerup", () => {
88+
down = false;
89+
}, { signal: $signal });
90+
</script>
91+
92+
<return=down>
93+
`,
94+
},
95+
];
Lines changed: 77 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { faPlus } from "@fortawesome/free-solid-svg-icons";
22
import type { File } from "app/util/workspace";
33
import * as styles from "./tabs.style.module.scss";
4+
import EXAMPLE_TEMPLATES from "./examples";
45
export interface Input {
56
files: File[];
67
filesChange: (files: File[]) => void;
@@ -10,53 +11,53 @@ export interface Input {
1011

1112
let/tabs:=input.files
1213
let/selected:=input.selected
14+
let/editingIndex=-1
1315

1416
div class=styles.tabs
1517
div class=styles.files
1618
for|tab, i| of=tabs
1719
const/isSelected=selected === i
1820
const/isEditable=i !== 0
19-
let/editing=false valueChange=(isEditable ? undefined : () => {})
21+
const/isEditing=editingIndex === i
2022

2123
div
2224
,aria-selected=isSelected && "true"
2325
,tabindex=0
2426
,role="button"
2527
,onClick() {
26-
if (isSelected) {
27-
editing = true;
28-
} else {
28+
if (isSelected && !isEditing) {
29+
editingIndex = i;
30+
} else if (!isEditing) {
2931
selected = i;
3032
}
3133
}
32-
,onKeyPress(e, el) {
34+
,onKeyDown(e, el) {
3335
if (e.target === el && (e.key === " " || e.key === "Enter")) {
34-
if (isSelected) {
35-
editing = true;
36-
} else {
36+
if (isSelected && !isEditing) {
37+
editingIndex = i;
38+
} else if (!isEditing) {
3739
selected = i;
3840
}
3941
}
4042
}
4143
,class=styles.tab
42-
if=editing
44+
if=isEditing
4345
let/length=(tab.path.length)
4446
input/$input
4547
,value=tab.path
4648
,size=length
47-
,onInput() {
48-
length = $input().value.length;
49+
,onInput(_, el) {
50+
length = el.value.length;
4951
}
50-
,onBlur() {
51-
tabs = tabs.toSpliced(i, 1, { ...tab, path: $input().value });
52-
editing = false;
52+
,onBlur(_, el) {
53+
tabs = tabs.toSpliced(i, 1, { ...tab, path: el.value });
54+
editingIndex = -1;
5355
}
5456
,onKeyPress(e) {
5557
if (e.key === "Enter") {
56-
$input().blur();
57-
editing = false;
58+
document.querySelector<HTMLElement>(".cm-content")?.focus();
5859
} else if (e.key === "Escape") {
59-
editing = false;
60+
editingIndex = -1;
6061
}
6162
}
6263
script --
@@ -67,6 +68,10 @@ div class=styles.tabs
6768
if=isEditable
6869
button
6970
,title="Delete file"
71+
,onMouseDown(e) {
72+
// prevent button from stealing focus, so it doesn't move out underneath the mouse
73+
e.preventDefault();
74+
}
7075
,onClick(e) {
7176
e.stopPropagation();
7277
if (selected >= i) {
@@ -77,21 +82,63 @@ div class=styles.tabs
7782
,class=styles.close
7883
-- ×
7984

85+
id/popoverId
86+
8087
button
8188
,title="Add file"
82-
,onClick() {
83-
let filename = prompt(
84-
"What filename (extension optional)?",
85-
tabs.some((tab) => tab.path === "Tag.marko")
86-
? `Tag${tabs.length}`
87-
: "Tag",
88-
);
89+
,popovertarget=popoverId
90+
,class=styles.add
91+
,onClick(_, btn) {
92+
const { top, right } = btn.getBoundingClientRect();
93+
$popover().style.top = `${top}px`;
94+
$popover().style.right = `${innerWidth - right}px`;
95+
}
96+
fa-icon=faPlus
8997

90-
if (filename) {
91-
if (!filename.includes(".")) filename += ".marko";
92-
selected = tabs.length;
93-
tabs = [...tabs, { path: filename, content: "" }];
98+
div/$popover
99+
,id=popoverId
100+
,class=styles.popover
101+
,popover
102+
,role="menu"
103+
,onFocusOut(e) {
104+
if (!$popover().contains(e.relatedTarget as Node)) {
105+
$popover().hidePopover();
94106
}
95107
}
96-
,class=styles.add
97-
fa-icon=faPlus
108+
109+
button
110+
,role="menuitem"
111+
,autofocus
112+
,onClick() {
113+
const newIndex = tabs.length;
114+
let path = "Tag.marko";
115+
for (let i = 1; tabs.some((tab) => tab.path === path); i++) {
116+
path = "Tag" + i + ".marko";
117+
}
118+
tabs = [
119+
...tabs,
120+
{
121+
path,
122+
content: "",
123+
},
124+
];
125+
selected = newIndex;
126+
editingIndex = newIndex;
127+
}
128+
,class=styles.templateButton
129+
-- New Blank File
130+
131+
for|{ name, content }| of=EXAMPLE_TEMPLATES
132+
button
133+
,role="menuitem"
134+
,onClick() {
135+
selected = tabs.length;
136+
let path = name + ".marko";
137+
for (let i = 1; tabs.some((tab) => tab.path === path); i++) {
138+
path = name + "-" + i + ".marko";
139+
}
140+
tabs = [...tabs, { path, content }];
141+
document.querySelector<HTMLElement>(".cm-content")?.focus();
142+
}
143+
,class=styles.templateButton
144+
-- ${`<${name}>`}

src/routes/playground/tags/playground/tags/editor/tags/tabs/tabs.style.module.scss

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,9 @@
4343
color: var(--color-blue);
4444
background-color: var(--color-gray-dim);
4545
align-items: center;
46-
border: 1px solid transparent;
46+
4747
&:hover {
4848
background-color: var(--color-gray-alt-dim);
49-
// color: var(--color-gray-dim);
5049
}
5150

5251
svg {
@@ -56,3 +55,34 @@
5655
}
5756
}
5857
}
58+
59+
.popover {
60+
inset: unset;
61+
flex-direction: column;
62+
max-width: 95vw;
63+
background-color: var(--color-gray-alt-dim);
64+
border: 1px solid var(--color-gray-alt);
65+
border-radius: 0.25rem;
66+
&:popover-open {
67+
display: flex;
68+
}
69+
}
70+
71+
.templateButton {
72+
padding: 0.6rem 0.75rem;
73+
background-color: transparent;
74+
border: none;
75+
text-align: right;
76+
77+
&:hover,
78+
&:focus-visible {
79+
background-color: var(--color-gray-dim);
80+
&:first-child {
81+
background-color: var(--color-blue-dim);
82+
}
83+
}
84+
85+
&:first-child {
86+
font-weight: bold;
87+
}
88+
}

0 commit comments

Comments
 (0)