Textarea with syntax highlighting powered by solid-js and vscode-oniguruma.
tm-textarea.mp4
- Installation
- Custom Element (
tm-textarea
) - Solid Component (
tm-textarea/solid
) - CDN (
tm-textarea/cdn
) - Themes & Grammars (
tm-textarea/tm
) - Bindings
- Line Numbers
npm i tm tm-textarea
# or
yarn add tm tm-textarea
# or
pnpm add tm tm-textarea
The main export is a custom-element <tm-textarea/>
powered by
@lume/element
Attribute/Property Types
import type { Grammar, Theme } from 'tm-textarea/tm'
interface tmTextareaAttributes extends ComponentProps<'div'> {
grammar?: Grammar
theme?: Theme
value?: string
editable?: boolean
stylesheet?: string | CSSStyleSheet
onInput?: (event: InputEvent & { currentTarget: tmTextareaElement }) => void
}
import 'tm-textarea'
import { setCDN } from 'tm-textarea/cdn'
setCDN('/tm')
export default () => (
<tm-textarea
grammar="tsx"
theme="andromeeda"
value="const sum = (a: string, b: string) => a + b"
editable={true}
style={{
padding: '10px',
'font-size': '16pt',
}}
stylesheet="code, code * { font-style:normal; }"
onInput={e => console.log(e.currentTarget.value)}
/>
)
Some DOM ::part()
are exported.
root
exposes root container.code
exposes thecode
tag.line
exposes the lines.textarea
exposes textarea to maybe change the selection color.
tm-textarea {
min-height: 100%;
min-width: 100%;
padding: 10px;
line-height: 16pt;
}
/* overwrite the theme background-color */
tm-textarea::part(root) {
background: transparent;
/* set a color for meanwhile the theme loads */
color: grey;
}
/* overwrite the selected text background color */
tm-textarea::part(textarea)::selection {
background: deepskyblue;
}
/* add line-numbers */
tm-textarea::part(line)::before {
display: inline-block;
counter-reset: variable calc(var(--line-number) + 1);
min-width: 7ch;
content: counter(variable);
}
tm-textarea::part(textarea) {
margin-left: 7ch;
}
The attribute stylesheet
could be used as a last resort to customize the theme. In the following
example we avoid italics in the rendered coded. The stylesheet is created, cached and possibly
reused on the different tm-textarea
instances.
<tm-textarea
grammar="tsx"
theme="andromeeda"
value="const sum = (a: string, b: string) => a + b"
stylesheet="code, code * { font-style: normal; }"
/>
A solid component of tm-textarea
is available at tm-textarea/solid
Prop Types
import { Grammar, Theme } from 'tm-textarea/tm'
interface tmTextareaProps extends Omit<ComponentProps<'div'>, 'style'> {
grammar: Grammar
theme: Theme
value: string
editable?: boolean
style?: JSX.CSSProperties
onInput?: (event: InputEvent & { currentTarget: HTMLTextAreaElement }) => void
}
import { TmTextarea } from 'tm-textarea/solid'
export default () => (
<TmTextarea
grammar="tsx"
theme="min-light"
value="const sum = (a: string, b: string) => a + b"
editable={true}
style={{
padding: '10px',
'font-size': '16pt',
}}
onInput={e => console.log(e.currentTarget.value)}
/>
)
To ease development we provide a way to set themes/grammars by setting the theme
or grammar
property with a string. Without configuration these are resolved to
tm-themes
and
tm-grammars
hosted on esm.sh
.
To provide a way to customize how these keys are resolved we provide a global function setCDN
,
exported from tm-textarea
. This function accepts as arguments either a base-url or a
callback-function.
When given a base-url, this will be used to fetch
${cdn}/tm-themes/themes/${theme}.json
for thethemes
${cdn}/tm-grammars/grammars/${grammar}.json
for thegrammars
${cdn}/vscode-oniguruma/release/onig.wasm
for theoniguruma
wasm-file
When given a callback, the returned string will be used to fetch instead.
import { setCDN } from 'tm-textarea/cdn'
// Set absolute base-url
setCDN('https://unpkg.com')
// Set relative base-url (for local hosting)
setCDN('/assets/tm')
// Use the callback-form
setCDN((type, id) => (type === 'oniguruma' ? `./oniguruma.wasm` : `./${type}/${id}.json`))
We export a list of textmate grammars and themes that are hosted on
tm-grammars
and tm-themes
.
These are used internally and maintained by shiki
.
import type { Theme, Grammar } from 'tm-textarea/tm'
import { themes, grammars } from 'tm-textarea/tm'
In addition to the core functionality, tm-textarea
provides bindings that enhance the text editing experience by introducing keyboard shortcuts and behaviors that are common in code editors.
The TabIndentation
binding enables tab and shift-tab indentation for a native textarea
, tm-textarea
or <TmTextarea/>
. It allows users to easily increase or decrease the indentation level of lines or selected blocks of text.
Type Definitions for TabIndentation
interface TabIndentation {
/** Adds event listeners to the passed element for handling 'keydown' and 'input' events specific to indentation. */
binding: (element: HTMLTextAreaElement | TmTextareaElement) => () => void;
/** Dispatches `formatIndent` and `formatOutdent` event-types when pressing tab */
onKeyDown: (event: KeyboardEvent & { currentTarget: TmTextareaElement | HTMLTextAreaElement }) => void;
/** Add indentation on `formatIndent` and `formatOutdent` event-type. */
onInput: (event: InputEvent & { currentTarget: TmTextareaElement | HTMLTextAreaElement }) => void;
/** Format leading whitespace of given string according to given tab-size. */
format: (source: string, tabSize: number) => string;
/** Utilities */
getLeadingWhitespace: (source: string) => string;
getLineStart: (value: string, position: number) => number;
getIndentationSegments: (leadingWhitespace: string, tabSize: number) => string[];
}
import { TmTextarea } from 'tm-textarea/solid'
import { TabIndentation } from 'tm-textarea/bindings/tab-indentation'
import source from "./source"
const App = () => {
return (
<TmTextarea
ref={TabIndentation.binding}
value={TabIndentation.format(source, 2)}
grammar="tsx"
theme="andromeeda"
/>
)
}
export default App
To keep the implementation of tm-textarea
as generic as possible, we do not provide specific props/attributes to render line-numbers. Instead css-variables are set to assist with the rendering of css line-numbers:
--tm-line-number
: the line number of a single line. This variable is set ontm-textarea::part(line)
(custom element) and.tm-textarea pre
(solid component).--tm-line-digits
: the amount of digits of the current line-count, useful for preventing overflowing line-numbers.
It can get a bit involved to account for all the possible edge cases, so we do provide the following css-snippets that you can use as a base:
.line-numbers::part(root) {
/* Calculate the offset from the digits of the current line-count and an additional ch for left-padding. */
--offset: calc(var(--tm-line-digits) * 1ch + 1ch);
}
/* Render a pseudo before-element in each line. */
.line-numbers::part(line)::before {
/* Position absolute to not offset the line's content. */
position: absolute;
/* Counter line's offset with the reversed offset. */
transform: translateX(calc(var(--offset) * -1));
/* Sets a counter line-number with the css-variable --tm-line-number + 1. */
counter-reset: line-number calc(var(--tm-line-number) + 1);
/* Adds the counter to the pseudo-element's content. */
content: counter(line-number);
}
/* Offset the textarea and the lines. */
.line-numbers::part(line),
.line-numbers::part(textarea) {
/* Offset should not be done with margin/padding to prevent conflict with inlined tab-calculations. */
transform: translateX(var(--offset));
}
.line-numbers {
/* Calculate the offset from the digits of the current line-count and an additional ch for left-padding. */
--width: calc(var(--tm-line-digits) * 1ch + 1ch);
}
/* Render a pseudo before-element in each line. */
.line-numbers pre::before {
/* Position absolute to not offset the line's content. */
position: absolute;
/* Counter line's offset with the reversed offset. */
transform: translateX(calc(var(--offset) * -1));
/* Sets a counter line-number with the css-variable --tm-line-number + 1. */
counter-reset: line-number calc(var(--tm-line-number) + 1);
/* Adds the counter to the pseudo-element's content. */
content: counter(line-number);
}
/* Offset the textarea and the lines. */
.line-numbers pre,
.line-numbers textarea {
/* Offset should not be done with margin/padding to prevent conflict with inlined tab-calculations. */
transform: translateX(var(--line-number-width));
}