Simple Editable
Plain text with undo/redo. The minimal setup.
Hello World!Try typing here.Undo with Cmd+Z.
import type {Context} from "@b9g/crank";import {renderer} from "@b9g/crank/dom";import {CrankEditable, EditableState} from "@b9g/crankeditable";function* SimpleEditable(this: Context) {const state = new EditableState({value: `Hello World!Try typing here.Undo with ${/Win|Linux/.test(navigator.platform) ? "Ctrl" : "Cmd"}+Z.`,});for (const {} of this) {const lines = state.value.split("\n");if (lines[lines.length - 1] === "") lines.pop();let cursor = 0;yield (<CrankEditable state={state} onstatechange={() => this.refresh()}><pre class="editable" contenteditable="true" spellcheck="false">{lines.map((line) => {const key = state.keyer.keyAt(cursor);cursor += line.length + 1;return <div key={key}>{line || <br />}</div>;})}</pre></CrankEditable>);}}renderer.render(<SimpleEditable />, document.body);
Rainbow
Per-character coloring with keyed lines.
Hello
World
Rainbow
Text
import type {Context} from "@b9g/crank";import {renderer} from "@b9g/crank/dom";import {CrankEditable, EditableState} from "@b9g/crankeditable";const COLORS = ["#FF0000", "#FFA500", "#FFDC00","#008000", "#0000FF", "#4B0082", "#800080",];function* RainbowEditable(this: Context) {const state = new EditableState({value: `HelloWorldRainbowText`,});for (const {} of this) {const lines = state.value.split("\n");if (lines[lines.length - 1] === "") lines.pop();let cursor = 0;yield (<CrankEditable state={state} onstatechange={() => this.refresh()}><div class="editable" contenteditable="true" spellcheck="false">{lines.map((line) => {const key = state.keyer.keyAt(cursor);cursor += line.length + 1;const chars = line? [...line].map((char, i) => (<span style={"color: " + COLORS[i % COLORS.length]}>{char}</span>)): <br />;return <div key={key}>{chars}</div>;})}</div></CrankEditable>);}}renderer.render(<RainbowEditable />, document.body);
Social Highlighting
Clickable #hashtags, @mentions, and links.
import type {Context, Element} from "@b9g/crank";import {renderer} from "@b9g/crank/dom";import {CrankEditable, EditableState} from "@b9g/crankeditable";const PATTERN = /(#\w+)|(@\w+)|(https?:\/\/[^\s]+)/g;function highlightSocial(text: string): (Element | string)[] {const result: (Element | string)[] = [];let lastIndex = 0;for (const match of text.matchAll(PATTERN)) {const index = match.index!;if (index > lastIndex) result.push(text.slice(lastIndex, index));const value = match[0];let color: string, href: string;if (match[1]) {color = "#c084fc";href = "https://example.com/tags/" + value.slice(1);} else if (match[2]) {color = "#60a5fa";href = "https://example.com/" + value.slice(1);} else {color = "#34d399";href = value;}result.push(<a href={href} target="_blank" rel="noopener"style={"color: " + color + "; text-decoration: underline"}>{value}</a>);lastIndex = index + value.length;}if (lastIndex < text.length) result.push(text.slice(lastIndex));return result;}function* SocialEditable(this: Context) {const state = new EditableState({value: `Check out #revise by @bikeshavingVisit https://revise.js.org#javascript #editing @everyone`,});for (const {} of this) {const lines = state.value.split("\n");if (lines[lines.length - 1] === "") lines.pop();let cursor = 0;yield (<CrankEditable state={state} onstatechange={() => this.refresh()}><div class="editable" contenteditable="true" spellcheck="false">{lines.map((line) => {const key = state.keyer.keyAt(cursor);cursor += line.length + 1;return (<div key={key}>{line ? highlightSocial(line) : <br />}</div>);})}</div></CrankEditable>);}}renderer.render(<SocialEditable />, document.body);
Twemoji
Emoji replaced with SVG using data-content.
Hello World! ๐
Revise.js is ๐ฅ๐ฅ๐ฅ
Type some emoji: ๐โค๏ธ๐
import type {Context, Element} from "@b9g/crank";import {renderer} from "@b9g/crank/dom";import {CrankEditable, EditableState, ContentAreaElement} from "@b9g/crankeditable";import {parse as parseEmoji} from "@twemoji/parser";if (!customElements.get("content-area")) {customElements.define("content-area", ContentAreaElement);}function renderTwemoji(text: string): (Element | string)[] {const entities = parseEmoji(text);if (!entities.length) return [text];const result: (Element | string)[] = [];let lastIndex = 0;for (const entity of entities) {const [start, end] = entity.indices;if (start > lastIndex) result.push(text.slice(lastIndex, start));result.push(<imgdata-content={entity.text}src={entity.url}alt={entity.text}draggable={false}style="height:1.2em;width:1.2em;vertical-align:middle;display:inline-block"/>);lastIndex = end;}if (lastIndex < text.length) result.push(text.slice(lastIndex));return result;}function* TwemojiEditable(this: Context) {const state = new EditableState({value: `Hello World! ๐Revise.js is ๐ฅ๐ฅ๐ฅType some emoji: ๐โค๏ธ๐`,});for (const {} of this) {const lines = state.value.split("\n");if (lines[lines.length - 1] === "") lines.pop();let cursor = 0;yield (<CrankEditable state={state} onstatechange={() => this.refresh()}><div class="editable" contenteditable="true" spellcheck="false">{lines.map((line) => {const key = state.keyer.keyAt(cursor);cursor += line.length + 1;return (<div key={key}>{line ? renderTwemoji(line) : <br />}</div>);})}</div></CrankEditable>);}}renderer.render(<TwemojiEditable />, document.body);
Code Editor
Keyword highlighting with a simple regex tokenizer.
function greet(name) {return 'Hello, ' + name;}const message = greet('World');console.log(message);
import type {Context, Element} from "@b9g/crank";import {renderer} from "@b9g/crank/dom";import {CrankEditable, EditableState} from "@b9g/crankeditable";const KW = /\b(function|const|let|var|return|if|else|for|while|class|import|export|from|new|typeof)\b/g;function highlight(line: string): (Element | string)[] {const result: (Element | string)[] = [];let lastIndex = 0;for (const match of line.matchAll(KW)) {const index = match.index!;if (index > lastIndex) result.push(line.slice(lastIndex, index));result.push(<span style="color: #c084fc">{match[0]}</span>);lastIndex = index + match[0].length;}if (lastIndex < line.length) result.push(line.slice(lastIndex));return result;}function* CodeEditable(this: Context) {const state = new EditableState({value: `function greet(name) {return 'Hello, ' + name;}const message = greet('World');console.log(message);`,});for (const {} of this) {const lines = state.value.split("\n");if (lines[lines.length - 1] === "") lines.pop();let cursor = 0;yield (<CrankEditable state={state} onstatechange={() => this.refresh()}><pre class="editable" contenteditable="true" spellcheck="false">{lines.map((line) => {const key = state.keyer.keyAt(cursor);cursor += line.length + 1;return (<div key={key}><code>{line ? highlight(line) : null}</code><br /></div>);})}</pre></CrankEditable>);}}renderer.render(<CodeEditable />, document.body);