import {Component, type JSX, render, type RefObject, createRef} from "preact"; import {type Value} from "@holllo/webextension-storage"; import {initializeGlobals, log, Link} from "../utilities/exports.js"; import { type MarkdownSnippet, collectMarkdownSnippets, createValueMarkdownSnippet, newMarkdownSnippetId, } from "../storage/exports.js"; import {runMarkdownToolbarFeature} from "../content-scripts/features/markdown-toolbar.js"; import "../scss/index.scss"; import "../scss/markdown-toolbar-editor.scss"; window.addEventListener("load", async () => { initializeGlobals(); render(, document.body); }); type Props = Record; type State = { /** * Snippets are stored as an array of tuples with the {@linkcode Value}-wrapped * {@linkcode MarkdownSnippet} as the first item and a {@linkcode RefObject} to * the {@linkcode SnippetEditor} component. This is done so we can have the * "Save All" button in the main component but have the logic for saving in * the editor components. */ snippets: Array<[Value, RefObject]>; }; class App extends Component { constructor(props: Props) { super(props); this.state = { snippets: [], }; } async componentDidMount() { const snippets = await collectMarkdownSnippets(); this.setState({ snippets: snippets.map((snippet) => [snippet, createRef()]), }); } componentDidUpdate() { // Each time the main component updates we want to re-run the toolbar setup // so the snippets are all updated and in their correct places. runMarkdownToolbarFeature( this.state.snippets .map(([snippet, _ref]) => snippet) .filter((snippet) => snippet.value.enabled), ); } addSnippet = async () => { const id = await newMarkdownSnippetId(); const snippet = await createValueMarkdownSnippet({ enabled: true, id, inDropdown: false, markdown: "", name: `Snippet ${id}`, position: 1, }); await snippet.save(); const {snippets} = this.state; snippets.push([snippet, createRef()]); this.setState({snippets}); }; applyAndReload = async () => { for (const [snippet, ref] of this.state.snippets) { if (ref.current === null) { throw new Error( "SnippetEditor reference is null, this should be unreachable!", ); } const editor = ref.current; if (editor.state.toBeRemoved) { await snippet.remove(); continue; } if (editor.state.hasUnsavedChanges) { await ref.current.save(); } } await this.componentDidMount(); }; render() { const {snippets} = this.state; return ( <>

Toolbar Preview

The Toolbar Preview lets you test out your snippets here directly without having to go to Tildes, with the only difference being that rendering the Markdown isn't possible here.

{/* The key attribute makes it so the mock re-renders on every update. */}

Snippets

Usage Guide

Here you can create your own snippets and customize your toolbar, each snippet has a number of configurable values:

  • Position, the number next to the snippet name determines in what order they will be placed in the toolbar. Snippets with the same position will be sorted alphabetically.
  • Name, the name of the snippet to display in the toolbar.
  • Shortcut, an optional keyboard shortcut that will insert the snippet when the combination of keys is pressed. A shortcut is defined as a list of optional modifier keys and a single final main key separated by plus signs, for example{" "} CTRL+ALT+S. The available modifier keys are ALT,{" "} CTRL and SHIFT. The modifier keys can be in any order however the final key must come at the end.
  • Enable, whether the snippet should be added to the toolbar.
  • Dropdown, with this enabled the snippet will be placed in the "More..." dropdown following the same sorting rules as normal.
  • Snippet (Markdown), the snippet text itself in Markdown.

There are also a few markers that will do special things when used in a snippet:

  • The <cursor> marker indicates where the cursor should be positioned after inserting the snippet. If this marker isn't used the cursor will be placed at the end of the snippet.
  • The <selected-cursor> marker is used for when you have text selected, placing the cursor at this location and inserting the selected text in the snippet at the{" "} <cursor> position. There is currently{" "} {" "} when this marker is placed before the{" "} <cursor> marker, causing the cursor position to be incorrectly placed after inserting the snippet.

To reload the toolbar after you've made changes click the Apply & Reload button. This will save all the snippets and recreate the toolbar with your changes. Any snippets with unsaved changes will have a yellow border and snippets that are going to be removed will have a red border.

To remove a snippet click the Remove button, this will remove it from storage but keep it loaded in the page. You can then click the Apply & Reload button to permanently remove it or click the Save or Undo buttons to get it back into storage.

{snippets.map(([snippet, ref]) => ( ))}
); } } type SnippetEditorProps = { snippet: Value; }; type SnippetEditorState = { enabled: MarkdownSnippet["enabled"]; hasUnsavedChanges: boolean; inDropdown: MarkdownSnippet["inDropdown"]; markdown: MarkdownSnippet["markdown"]; name: MarkdownSnippet["name"]; position: MarkdownSnippet["position"]; shortcut: MarkdownSnippet["shortcut"]; toBeRemoved: boolean; }; class SnippetEditor extends Component { constructor(props: SnippetEditorProps) { super(props); const {enabled, inDropdown, markdown, name, position, shortcut} = props.snippet.value; this.state = { enabled, hasUnsavedChanges: false, inDropdown, markdown, name, position, shortcut, toBeRemoved: false, }; } edit = ( key: K, input: HTMLInputElement | HTMLTextAreaElement, ) => { const unsavedChanges: Partial = { hasUnsavedChanges: true, }; if (key === "position") { this.setState({ ...unsavedChanges, [key]: Number(input.value), }); } else if ( ["enabled", "inDropdown"].includes(key) && input instanceof HTMLInputElement ) { this.setState({ ...unsavedChanges, [key]: input.checked, }); } else { this.setState({ ...unsavedChanges, [key]: input.value, }); } }; save = async () => { let {snippet} = this.props; const { enabled, inDropdown, markdown, name, position, shortcut, toBeRemoved, } = this.state; snippet.value.enabled = enabled; snippet.value.inDropdown = inDropdown; snippet.value.markdown = markdown; snippet.value.name = name; snippet.value.position = position; snippet.value.shortcut = shortcut; const isBuiltin = snippet.value.id < 0; if (isBuiltin || toBeRemoved) { // If the snippet is a builtin one, then remove it from storage and assign // it a new ID indicating it was edited. // If it was marked for removal then we also need to assign a new ID // because it's possible a new snippet was assigned the old ID while this // one was removed. const id = await newMarkdownSnippetId(); if (isBuiltin) { await snippet.remove(); } snippet = await createValueMarkdownSnippet({ ...snippet.value, id, }); } this.props.snippet = snippet; await this.props.snippet.save(); this.setState({hasUnsavedChanges: false, toBeRemoved: false}); }; remove = async () => { const toBeRemoved = !this.state.toBeRemoved; if (toBeRemoved) { await this.props.snippet.remove(); this.setState({toBeRemoved}); } else { await this.save(); } }; render() { const { enabled, hasUnsavedChanges, inDropdown, markdown, name, position, shortcut, toBeRemoved, } = this.state; const onEdit = ( event: Event, key: K, ) => { if ( event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement ) { this.edit(key, event.target); } else { log("Tried to edit field with unknown event target type", true); log(event, true); } }; const editorClasses = [ "snippet-editor", hasUnsavedChanges ? "unsaved-changes" : "", toBeRemoved ? "to-be-removed" : "", ].join(" "); return (
{ onEdit(event, "position"); }} /> { onEdit(event, "name"); }} /> { onEdit(event, "shortcut"); }} />
); } } /** * Create a mocked version of the Markdown ` ); }