Skip to content

Instantly share code, notes, and snippets.

@ifiokjr
Last active January 27, 2021 03:52
Show Gist options
  • Save ifiokjr/8359eb4819592d0302abcb4a773f47d7 to your computer and use it in GitHub Desktop.
Save ifiokjr/8359eb4819592d0302abcb4a773f47d7 to your computer and use it in GitHub Desktop.
Remirror Documentation

Documentation

I find writing documentation difficult. Inline docs while I'm coding is fine, but sitting down and actually writing something that people can use to understand the codebase is very taxing for me.

These are notes to help guide me and hopefully make the process less taxing. Some parts will be copied and pasted directly in the remirror documentation site and other parts are observations and ideas.

Goals

I know what good documentation makes me feel like. It makes me feel safe and loved. It makes me trust the library I'm learning. Sometimes if the documentation is good enough it get's me excited. I may have come to the project to solve a specific problem but the docs are so good that now I want to share all about it.

This is the potential of a docs site.

It should go beyond being a mere repository of knowledge and become a crafted toolkit giving users what they want.

Some of my observations on what makes a documentation site delightful.

How it looks?

The design and first impression can be very important, especially for drive by visitors. There are times when, without even reading a word, I find myself breathless, taken aback by the beauty. It's a wonderful experience.

What does it do?

I hate when I find a beautiful documentation site, and I spend minutes trying to figure out exactly what it does. I'm here to solve my problem. Please, please please, let me know if you can do that. It is very frustrating when that isn't made immediately clear.

For example, with remirror what problems are we solving?

  • It's a text editor.
  • It comes with a shit tone of extensions, for markdown support, mentions, action keys, epic mode, emoji, placeholders.
  • It has prebuilt editors for immediate consumption.
  • Supports i18n for all editors (with language support being added).
  • Support react and react native.
  • Incoming support for angular, svelte, vue.
  • Performant even for large documents
  • Extensible to meet all your needs.
  • A11y first development approach.

How simple is the language?

Documentation sites should never assume I'm smart. Anything for first timers should assume I'm very very not smart. If it's for an advanced topic, then slap an advanced label (not for the faint of mind), or point out that the following is difficult and not essential to using the library.

Feeling stupid is painful. Let me know that this is actually objectively difficult so that I can feel less insecure about my lack of knowledge.

How many examples?

Please, please, have working examples.

Again.

Every single example on a documentation site, should work and there should be lots of them. To enable this, it's a good idea to automatically render examples using a live editor of some sort.

It's also a good idea to have a copy example button.

Conclusion

There's more to add I'm sure and I'll come back to it later. But for now this is a good start.

I want a site that is:

  • Beautiful
  • Forthright about it's functionality.
  • Simple with labels for advanced topics.
  • Full of copyable examples.

Elements

  • Use new logo
  • Use new site design
  • Complete the guide I started with plenty of examples
  • React focused
  • Auto generated documentation from the actual code. Props into prop tables etc...
import {
bool,
CreatePluginReturn,
EditorView,
extensionDecorator,
findPositionOfNodeAfter,
findPositionOfNodeBefore,
Handler,
isUndefined,
pick,
PlainExtension,
ResolvedPos,
throttle,
} from '@remirror/core';
import { dropPoint, insertPoint } from '@remirror/pm/transform';
import { Decoration, DecorationSet } from '@remirror/pm/view';
interface OnInitParameter extends OnDestroyParameter {
extension: DropCursorExtension;
}
interface OnDestroyParameter {
blockElement: HTMLElement;
inlineElement: HTMLElement;
}
export interface DropCursorOptions {
/**
* The main color of the component being rendered.
*
* This can be a named color from the theme such as `background`
*
* @defaultValue `primary`
*/
color?: string;
/**
* The width of the inline drop cursor.
*
* @defaultValue '2px'
*/
inlineWidth?: string | number;
/**
* The horizontal margin around the inline cursor.
*
* @defaultValue '10px'
*/
inlineSpacing?: string | number;
/**
* The width of the block drop cursor.
*
* @defaultValue '100%'
*/
blockWidth?: string | number;
/**
* The height of the block drop cursor.
*/
blockHeight?: string | number;
/**
* The class name added to the block widget
*
* @defaultValue 'remirror-drop-cursor-block'
*/
blockClassName?: string;
/**
* The class name added to the node that appears before the block drop cursor widget.
*
* @defaultValue 'remirror-drop-cursor-before-block'
*/
beforeBlockClassName?: string;
/**
* The class name added to the node that appears after the block drop cursor widget.
*
* @defaultValue 'remirror-drop-cursor-after-block'
*/
afterBlockClassName?: string;
/**
* The class name added to the inline drop cursor widget
*
* @defaultValue 'remirror-drop-cursor-inline'
*/
inlineClassName?: string;
/**
* The class name added to the node that appears before the inline drop cursor widget.
*
* @defaultValue 'remirror-drop-cursor-before-inline'
*/
beforeInlineClassName?: string;
/**
* The class name added to the node that appears after the inline drop cursor widget.
*
* @defaultValue 'remirror-drop-cursor-after-inline'
*/
afterInlineClassName?: string;
/**
* When the plugin is first initialized.
*/
onInit: Handler<(parameter: OnInitParameter) => void>;
/**
* Cleanup when the drop cursor plugin is destroyed. This happens when the
* editor is unmounted.
*
* If you've attached a portal to the element then this is the place to handle
* that.
*/
onDestroy: Handler<(parameter: OnDestroyParameter) => void>;
}
/**
* A drop cursor plugin which adds a decoration at the active drop location. The
* decoration has a class and can be styled however you want.
*/
@extensionDecorator<DropCursorOptions>({
defaultOptions: {
inlineWidth: '2px',
inlineSpacing: '10px',
blockWidth: '100%',
blockHeight: '10px',
color: 'primary',
blockClassName: 'remirror-drop-cursor-block',
beforeBlockClassName: 'remirror-drop-cursor-before-block',
afterBlockClassName: 'remirror-drop-cursor-after-block',
inlineClassName: 'remirror-drop-cursor-inline',
beforeInlineClassName: 'remirror-drop-cursor-before-inline',
afterInlineClassName: 'remirror-drop-cursor-after-inline',
},
handlerKeys: ['onInit', 'onDestroy'],
})
export class DropCursorExtension extends PlainExtension<DropCursorOptions> {
get name() {
return 'dropCursor' as const;
}
createHelpers() {
return {
/**
* Check if the anything is currently being dragged over the editor.
*/
isDragging: () => {
return this.store.getPluginState<DropCursorState>(this.name).isDragging();
},
};
}
/**
* Use the dropCursor plugin with provided options.
*/
createPlugin(): CreatePluginReturn<DropCursorState> {
const dropCursorState = new DropCursorState(this);
return {
view(editorView) {
dropCursorState.init(editorView);
return pick(dropCursorState, ['destroy']);
},
state: {
init: () => dropCursorState,
apply: () => dropCursorState,
},
props: {
decorations: () => dropCursorState.decorationSet,
handleDOMEvents: {
dragover: (_, event) => {
dropCursorState.dragover(event as DragEvent);
return false;
},
dragend: () => {
dropCursorState.dragend();
return false;
},
drop: () => {
dropCursorState.drop();
return false;
},
dragleave: (_, event) => {
dropCursorState.dragleave(event as DragEvent);
return false;
},
},
},
};
}
}
/**
* This indicates whether the current cursor position is within a textblock or
* between two nodes.
*/
export type DropCursorType = 'block' | 'inline';
class DropCursorState {
/**
* The set of all currently active decorations.
*/
decorationSet = DecorationSet.empty;
readonly #extension: DropCursorExtension;
/**
* The editor view.
*/
private view!: EditorView;
/**
* The dom element which holds the block `Decoration.widget`.
*/
private blockElement!: HTMLElement;
/**
* The dom element which holds the inline `Decoration.widget`.
*/
private inlineElement!: HTMLElement;
/**
* The currently active timeout. This is used when removing the drop cursor to prevent any flicker.
*/
#timeout?: any;
/**
* The current derived target position. This is cached to help prevent unnecessary re-rendering.
*/
#target?: number;
constructor(extension: DropCursorExtension) {
this.#extension = extension;
this.dragover = throttle(50, this.dragover);
}
init(view: EditorView) {
const { blockClassName, inlineClassName } = this.#extension.options;
this.view = view;
this.blockElement = document.createElement('div');
this.inlineElement = document.createElement('span');
this.blockElement.classList.add(blockClassName);
this.inlineElement.classList.add(inlineClassName);
this.#extension.options.onInit({
blockElement: this.blockElement,
inlineElement: this.inlineElement,
extension: this.#extension,
});
}
/**
* Called when the view is destroyed
*/
destroy = () => {
this.#extension.options.onDestroy({
blockElement: this.blockElement,
inlineElement: this.inlineElement,
});
};
/**
* Check if the editor is currently being dragged around.
*/
isDragging = () =>
bool(
this.view.dragging ??
(this.decorationSet !== DecorationSet.empty || !isUndefined(this.#target)),
);
/**
* Called on every dragover event.
*
* Captures the current position and whether
*/
dragover = (event: DragEvent) => {
const pos = this.view.posAtCoords({ left: event.clientX, top: event.clientY });
if (pos) {
const {
dragging,
state: { doc, schema },
} = this.view;
const target = dragging?.slice
? dropPoint(doc, pos.pos, dragging.slice) ?? pos.pos
: insertPoint(doc, pos.pos, schema.image) ?? pos.pos;
if (target === this.#target) {
// This line resets the timeout.
this.scheduleRemoval(100);
return;
}
this.#target = target;
this.updateDecorationSet();
this.scheduleRemoval(100);
}
};
/**
* Called when the drag ends.
*
* ? Sometimes this event doesn't fire, is there a way to prevent this.
*/
dragend = () => {
this.scheduleRemoval(100);
};
/**
* Called when the element is dropped.
*
* ? Sometimes this event doesn't fire, is there a way to prevent this.
*/
drop = () => {
this.scheduleRemoval(100);
};
/**
* Called when the drag leaves the boundaries of the prosemirror editor dom node.
*/
dragleave = (event: DragEvent) => {
if (event.target === this.view.dom || !this.view.dom.contains(event.relatedTarget as Node)) {
this.scheduleRemoval(100);
}
};
/**
* Dispatch an empty transaction to trigger a decoration update.
*/
private readonly triggerDecorationSet = () => this.view.dispatch(this.view.state.tr);
/**
* Removes the decoration and (by default) the current target value.
*/
private readonly removeDecorationSet = (ignoreTarget = false) => {
if (!ignoreTarget) {
this.#target = undefined;
}
this.decorationSet = DecorationSet.empty;
this.triggerDecorationSet();
};
/**
* Removes the drop cursor decoration from the view after the set timeout.
*
* Sometimes the drag events don't automatically trigger so it's important to have this cleanup in place.
*/
private scheduleRemoval(timeout: number, ignoreTarget = false) {
if (this.#timeout) {
clearTimeout(this.#timeout);
}
this.#timeout = setTimeout(() => {
this.removeDecorationSet(ignoreTarget);
}, timeout);
}
/**
* Update the decoration set with a new position.
*/
private readonly updateDecorationSet = () => {
if (!this.#target) {
return;
}
const {
state: { doc },
} = this.view;
const $pos = doc.resolve(this.#target);
const cursorIsInline = $pos.parent.inlineContent;
this.decorationSet = DecorationSet.create(
doc,
cursorIsInline ? this.createInlineDecoration($pos) : this.createBlockDecoration($pos),
);
this.triggerDecorationSet();
};
/**
* Create an inline decoration for the document which is rendered when the cursor position
* is within a text block.
*/
private createInlineDecoration($pos: ResolvedPos): Decoration[] {
const dropCursor = Decoration.widget($pos.pos, this.inlineElement, {
key: 'drop-cursor-inline',
});
return [dropCursor];
}
/**
* Create a block decoration for the document which is rendered when the cursor position
* is between two nodes.
*/
private createBlockDecoration($pos: ResolvedPos): Decoration[] {
const { beforeBlockClassName, afterBlockClassName } = this.#extension.options;
const decorations: Decoration[] = [];
const dropCursor = Decoration.widget($pos.pos, this.blockElement, {
key: 'drop-cursor-block',
});
const before = findPositionOfNodeBefore($pos);
const after = findPositionOfNodeAfter($pos);
decorations.push(dropCursor);
if (before) {
const beforeDecoration = Decoration.node(before.pos, before.end, {
class: beforeBlockClassName,
});
decorations.push(beforeDecoration);
}
if (after) {
const afterDecoration = Decoration.node(after.pos, after.end, {
class: afterBlockClassName,
});
decorations.push(afterDecoration);
}
return decorations;
}
}

Controlled Editor

:::caution Advanced Topic The following is considered an advanced topic. If you are struggling to understand some of the concepts don't feel sad. It can be hard to understand initially. :::

There are times when you will want complete control over the content in your editor. For this reason remirror supports controlled editors. Setting up your editor like this is more complicated and for this reason is marked as an advanced topic.

Get started in the usual way.

import React from 'react';
import {RemirrorProvider, useRemirror, createReactManager} from 'remirror/react';
import { BoldExtension } from 'remirror/extension/bold';

const manager = createReactManager([new BoldExtension()]);

const EditorWrapper = () => {

  // This will be changed
  return (
    <RemirrorProvider manager={manager}>
      <Editor />
    </RemirrorProvider>
  );
}

const Editor = () => {
  const { getRootProps } = useRemirror();

  return <div {...getRootProps()} />;
}

The main difference is that you will need to create the state value that is passed into the editor. This value is called the EditorState and is an object that will be familiar to you if you have used ProseMirror in the past. When remirror sees the value it knows to treat the editor as a controlled instance. For things to work correctly you are required to add an onChange handler for the RemirrorProvider.

// Add the `useState` hook to keep track of the state.
import React, { useState } from 'react';
import {RemirrorProvider, useRemirror, createReactManager} from 'remirror/react';
import { BoldExtension } from 'remirror/extension/bold';

// Add the `fromHtml` string handler import so that the initial state can be a html string.
import { fromHtml } from 'remirror/core';

const manager = createReactManager([new BoldExtension()]);

const EditorWrapper = () => {
  // Create the initial value for the manager.
  const initialValue = manager.createState({
    content: '<p>This is the initial value</p>',
    stringHandler: fromHtml,
  });

  const [value, setValue] = useState(initialValue);



  // Add the value and change handler to the editor.
  return (
    <RemirrorProvider manager={manager}
      value={value}
      onChange={(parameter) => {
        // Update the state to the latest value.
        setValue(parameter.state);
      }}
    >
      <Editor />
    </RemirrorProvider>
  );
}

const Editor = () => {
  const { getRootProps } = useRemirror();

  return <div {...getRootProps()} />;
}

The editor now behaves in a similar way to what you'd expect from a non controlled editor. The main thing is that we've been able to intercept the state update and can do some pretty interesting things with this power.

For example, the following change handler now intercepts the state update in order to insert NO!!! into the editor whenever the user types any content.

const EditorWrapper = () => {
  const initialValue = manager.createState({
    content: '<p>This is the initial value</p>',
    stringHandler: fromHtml,
  });

  const [value, setValue] = useState(initialValue);

  return (
    <RemirrorProvider manager={manager}
      value={value}
      onChange={(parameter) => {
        const {state, tr, } = parameter;
        let nextState = state;

        // Check if the document content for the editor changed.
        if (tr?.docChanged) {
          // Insert text into the editor via a new state.
          const nextState = state.applyTransaction(state.tr.insertText('NO!!!'));
        }

        // Update to using a new value
        setValue(nextState);
      }}
    >
      <Editor />
    </RemirrorProvider>
  );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment