Skip to content

Commit cd518d8

Browse files
committed
feat: initial implementation
1 parent c592463 commit cd518d8

File tree

6 files changed

+331
-17
lines changed

6 files changed

+331
-17
lines changed

README.md

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,87 @@ npm install svelte-codemirror-editor
1313

1414
To use `svelte-codemirror-editor`, you need to import the package and use it as any `Svelte` component.
1515

16-
```tsx
16+
```svelte
1717
<script lang="ts">
1818
import CodeMirror from "svelte-codemirror-editor";
1919
import { javascript } from "@codemirror/lang-javascript";
2020
2121
let value = "";
2222
</script>
2323
24-
<CodeMirror bind:value lang={javascript} class="editor" />
25-
26-
<style>
27-
:global(.editor .cm-editor) {
28-
height: 40rem;
29-
}
30-
</style>
31-
24+
<CodeMirror bind:value lang={javascript()} />
3225
```
3326

34-
### Properties
27+
## Properties
3528

36-
| Property | Type | Description | Default value |
37-
| --------- | ---------- | ------------------------------------------------------------------------------------------- | ------------- |
38-
| `value` | `string` | The value that will be displayed and edited in the CodeMirror editor | `""` |
39-
| `lang` | `"string"` | The language of the code. Can be one of `javascript`, `typescript`, `html`, `css`, `custom` | `"custom"` |
40-
| `useTab` | `boolean` | Whether to use the `Tab` shortcut to handle indentation | `true` |
41-
| `tabSize` | `number` | The number of space of an indentation level. | `2` |
29+
| Property | Type | Description | Default value |
30+
| --------- | ----------------- | --------------------------------------------------------------------------------------------- | ------------- |
31+
| `value` | `string` | The value that will be displayed and edited in the CodeMirror editor | `""` |
32+
| `basic` | `boolean` | Whether to use CodeMirror `basicSetup` extensions. | `true` |
33+
| `lang` | `LanguageSupport` | The language extension that will parse and highlight the value. | `undefined` |
34+
| `theme` | `Extension` | The theme extension to customize the appearence of the editor. | `undefined` |
35+
| `extensions` | `Extension[]` | Additional extensions to inject in the editor. | `[]` |
36+
| `useTab` | `boolean` | Whether to use the `Tab` shortcut to handle indentation. | `true` |
37+
| `tabSize` | `number` | The number of space of an indentation level. | `2` |
38+
| `editable` | `boolean` | Whether to make the editor editable or not. | `true` |
39+
| `readonly` | `boolean` | Whether to make the editor readonly or not. | `false` |
40+
| `placeholder` | `string` | A placeholder to include when value is empty. | `undefined` |
41+
| `styles` | `ThemeSpec` | In-place theme configuration. *See exemple below*. | `undefined` |
4242

43-
### Events
43+
## Events
4444

4545
| Event | Type | Description |
4646
| -------- | -------- | ----------------------------- |
4747
| `change` | `string` | Trigger when the code changes |
48+
49+
## Exemples
50+
51+
### Basic usage
52+
53+
```svelte
54+
<script lang="ts">
55+
import CodeMirror from "svelte-codemirror-editor";
56+
import { javascript } from "@codemirror/lang-javascript";
57+
58+
let value = "";
59+
</script>
60+
61+
<CodeMirror bind:value lang={javascript()} />
62+
```
63+
64+
### Custom theme
65+
66+
```svelte
67+
<script lang="ts">
68+
import CodeMirror from "svelte-codemirror-editor";
69+
import { javascript } from "@codemirror/lang-javascript";
70+
import { oneDark } from "@codemirror/theme-one-dark";
71+
72+
let value = "";
73+
</script>
74+
75+
<CodeMirror bind:value lang={javascript()} theme={oneDark} />
76+
```
77+
78+
### Custom styles
79+
80+
```svelte
81+
<script lang="ts">
82+
import CodeMirror from "svelte-codemirror-editor";
83+
import { javascript } from "@codemirror/lang-javascript";
84+
85+
let value = "";
86+
</script>
87+
88+
<CodeMirror
89+
bind:value
90+
lang={javascript()}
91+
styles={{
92+
"&": {
93+
width: "500px",
94+
maxWidth: "100%",
95+
height: "50rem"
96+
}
97+
}}
98+
/>
99+
```

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
{
22
"name": "svelte-codemirror-editor",
3+
"description": "A svelte component to create a CodeMirror 6+ editor",
34
"version": "0.0.1",
45
"type": "module",
6+
"author": {
7+
"name": "Touchify",
8+
"email": "[email protected]",
9+
"url": "https://touchify.io"
10+
},
11+
"repository": {
12+
"type": "git",
13+
"url": "https://github.com/touchifyapp/svelte-codemirror-editor"
14+
},
515
"scripts": {
616
"dev": "svelte-kit dev",
717
"build": "svelte-kit build",
@@ -13,10 +23,14 @@
1323
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
1424
"format": "prettier --write --plugin-search-dir=. ."
1525
},
26+
"peerDependencies": {
27+
"codemirror": "^6.0.0"
28+
},
1629
"devDependencies": {
1730
"@sveltejs/kit": "next",
1831
"@typescript-eslint/eslint-plugin": "^5.27.0",
1932
"@typescript-eslint/parser": "^5.27.0",
33+
"codemirror": "^6.0.0",
2034
"eslint": "^8.16.0",
2135
"eslint-config-prettier": "^8.3.0",
2236
"eslint-plugin-svelte3": "^4.0.0",

src/lib/CodeMirror.svelte

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<script lang="ts" context="module">
2+
export type ThemeSpec = Record<string, StyleSpec>;
3+
export type StyleSpec = {
4+
[propOrSelector: string]: string | number | StyleSpec | null;
5+
};
6+
</script>
7+
8+
<script lang="ts">
9+
import { createEventDispatcher, onDestroy, onMount } from "svelte";
10+
import { basicSetup } from "codemirror";
11+
import { EditorView, keymap, placeholder as placeholderExt } from "@codemirror/view";
12+
import { EditorState, StateEffect, type Extension } from "@codemirror/state";
13+
import { indentWithTab } from "@codemirror/commands";
14+
import { indentUnit, type LanguageSupport } from "@codemirror/language";
15+
import { debounce } from "./util";
16+
17+
let classes = "";
18+
export { classes as class };
19+
export let value: string | null | undefined = "";
20+
21+
export let basic = true;
22+
export let lang: LanguageSupport | null | undefined = undefined;
23+
export let theme: Extension | null | undefined = undefined;
24+
export let extensions: Extension[] = [];
25+
26+
export let useTab = true;
27+
export let tabSize = 2;
28+
29+
export let styles: ThemeSpec | null | undefined = undefined;
30+
export let editable = true;
31+
export let readonly = false;
32+
export let placeholder: string | HTMLElement | null | undefined = undefined;
33+
34+
const is_browser = typeof window !== "undefined";
35+
const dispatch = createEventDispatcher<{ change: string }>();
36+
37+
let element: HTMLDivElement;
38+
let view: EditorView;
39+
40+
let update_from_prop = false;
41+
let update_from_state = false;
42+
let first_config = true;
43+
let first_update = true;
44+
45+
$: state_extensions = [
46+
...get_base_extensions(basic, useTab, tabSize, placeholder, editable, readonly, lang),
47+
...get_theme(theme, styles),
48+
...extensions,
49+
];
50+
51+
$: view && update(value);
52+
$: view && state_extensions && reconfigure();
53+
54+
onMount(() => (view = create_editor_view()));
55+
onDestroy(() => view?.destroy());
56+
57+
function create_editor_view(): EditorView {
58+
const on_change = debounce(handle_change, 300);
59+
60+
return new EditorView({
61+
parent: element,
62+
state: create_editor_state(value),
63+
dispatch(transaction) {
64+
view.update([transaction]);
65+
66+
if (!update_from_prop && transaction.docChanged) {
67+
on_change();
68+
}
69+
},
70+
});
71+
}
72+
73+
function reconfigure(): void {
74+
if (first_config) {
75+
first_config = false;
76+
return;
77+
}
78+
79+
view.dispatch({
80+
effects: StateEffect.reconfigure.of(state_extensions),
81+
});
82+
}
83+
84+
function update(value: string | null | undefined): void {
85+
if (first_update) {
86+
first_update = false;
87+
return;
88+
}
89+
90+
if (update_from_state) {
91+
update_from_state = false;
92+
return;
93+
}
94+
95+
update_from_prop = true;
96+
97+
view.setState(create_editor_state(value));
98+
99+
update_from_prop = false;
100+
}
101+
102+
function handle_change(): void {
103+
const new_value = view.state.doc.toString();
104+
if (new_value === value) return;
105+
106+
update_from_state = true;
107+
108+
value = new_value;
109+
dispatch("change", value);
110+
}
111+
112+
function create_editor_state(value: string | null | undefined): EditorState {
113+
return EditorState.create({
114+
doc: value ?? undefined,
115+
extensions: state_extensions,
116+
});
117+
}
118+
119+
function get_base_extensions(
120+
basic: boolean,
121+
useTab: boolean,
122+
tabSize: number,
123+
placeholder: string | HTMLElement | null | undefined,
124+
editable: boolean,
125+
readonly: boolean,
126+
lang: LanguageSupport | null | undefined
127+
): Extension[] {
128+
const extensions: Extension[] = [
129+
indentUnit.of(" ".repeat(tabSize)),
130+
EditorView.editable.of(editable),
131+
EditorState.readOnly.of(readonly),
132+
];
133+
134+
if (basic) extensions.push(basicSetup);
135+
if (useTab) extensions.push(keymap.of([indentWithTab]));
136+
if (placeholder) extensions.push(placeholderExt(placeholder));
137+
if (lang) extensions.push(lang);
138+
139+
return extensions;
140+
}
141+
142+
function get_theme(theme: Extension | null | undefined, styles: ThemeSpec | null | undefined): Extension[] {
143+
const extensions: Extension[] = [];
144+
if (styles) extensions.push(EditorView.theme(styles));
145+
if (theme) extensions.push(theme);
146+
return extensions;
147+
}
148+
</script>
149+
150+
{#if is_browser}
151+
<div class="codemirror-wrapper {classes}" bind:this={element} />
152+
{:else}
153+
<div class="scm-waiting {classes}">
154+
<div class="scm-waiting__loading scm-loading">
155+
<div class="scm-loading__spinner" />
156+
<p class="scm-loading__text">Loading editor...</p>
157+
</div>
158+
159+
<pre class="scm-pre cm-editor">{value}</pre>
160+
</div>
161+
{/if}
162+
163+
<style>
164+
.codemirror-wrapper :global(.cm-focused) {
165+
outline: none;
166+
}
167+
168+
.scm-waiting {
169+
position: relative;
170+
}
171+
.scm-waiting__loading {
172+
position: absolute;
173+
top: 0;
174+
left: 0;
175+
bottom: 0;
176+
right: 0;
177+
background-color: rgba(255, 255, 255, 0.5);
178+
}
179+
180+
.scm-loading {
181+
display: flex;
182+
align-items: center;
183+
justify-content: center;
184+
}
185+
.scm-loading__spinner {
186+
width: 1rem;
187+
height: 1rem;
188+
border-radius: 100%;
189+
border: solid 2px #000;
190+
border-top-color: transparent;
191+
margin-right: 0.75rem;
192+
animation: spin 1s linear infinite;
193+
}
194+
.scm-loading__text {
195+
font-family: sans-serif;
196+
}
197+
.scm-pre {
198+
font-size: 0.85rem;
199+
font-family: monospace;
200+
tab-size: 2;
201+
-moz-tab-size: 2;
202+
resize: none;
203+
pointer-events: none;
204+
user-select: none;
205+
overflow: auto;
206+
}
207+
208+
@keyframes spin {
209+
0% {
210+
transform: rotate(0deg);
211+
}
212+
100% {
213+
transform: rotate(360deg);
214+
}
215+
}
216+
</style>

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as default, type ThemeSpec, type StyleSpec } from "./CodeMirror.svelte";

src/lib/util.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-this-alias */
2+
3+
/**
4+
* Reduce calls to the passed function.
5+
*
6+
* @param func - Function to debounce.
7+
* @param threshold - The delay to avoid recalling the function.
8+
* @param execAsap - If true, the Function is called at the start of the threshold, otherwise the Function is called at the end of the threshold.
9+
*/
10+
export function debounce<T extends (...args: any[]) => any>(func: T, threshold: number, execAsap = false): T {
11+
let timeout: any;
12+
13+
return function debounced(this: any, ...args: any[]): any {
14+
const self = this;
15+
16+
if (timeout) clearTimeout(timeout);
17+
else if (execAsap) func.apply(self, args);
18+
19+
timeout = setTimeout(delayed, threshold || 100);
20+
21+
function delayed(): void {
22+
if (!execAsap) func.apply(self, args);
23+
timeout = null;
24+
}
25+
} as T;
26+
}

svelte.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ const config = {
1212
return filepath.endsWith("package.json") || filepath.endsWith("index.ts");
1313
},
1414
},
15+
vite: {
16+
optimizeDeps: {
17+
exclude: ["codemirror"]
18+
}
19+
}
1520
}
1621
};
1722

0 commit comments

Comments
 (0)