Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown Toolbar Design Doc #1125

Open
tomrule007 opened this issue Sep 14, 2021 · 0 comments
Open

Markdown Toolbar Design Doc #1125

tomrule007 opened this issue Sep 14, 2021 · 0 comments
Labels
Design Doc Design Docs

Comments

@tomrule007
Copy link
Contributor

tomrule007 commented Sep 14, 2021

Markdown Toolbar

A collection of markdown styling buttons that applies/removes markdown style to selected text with the associated textarea.

toolbar

This will be included with our main text input box, <MdInput />, with the goal of:

  1. Let users know our input boxes support markdown syntax
  2. Help users learn markdown
  3. Make comments / code reviews more pleasant to read by encouraging more formatting

MdInput componet:
markdown toolbar with MdInput component

Design Considerations

The goal is to have the style buttons behave very similar to the github markdown bar, which covers a lot of edge cases.

  • Removing/Adding style on click
  • Updating selector position to not include newly added style characters
  • Preserving undo/redo functionality of the input
  • Multiline styles vs single line styles
  • block selection vs single line selection
  • new line padding

stretch goals:

  • Cursor word selection expansion (converting a cursor in the middle of a word to a full word selection before applying style
  • Smart link url detection (auto inserting url if selection is a url instead of name)
  • Keyboard bound hotkey support

Basic Logic Rough Breakdown

*User clicks a style button or hotkey*

  1. get Input state (value, selectionStart, selectionEnd)
  2. if styled -> remove style -> update state
  3. if !styled && singleLine -> apply single line style -> update state
  4. if !styled && multiLine -> apply multi line style -> update state
    *Update state must also include new cursor location
    else

*User sees styled text*

Biggest hurdles

Undo/Redo functionality

First Attempt:

using `document.execCommand('insertText',...)` deprecated api in attempt to preserve native undo/redo functionality

This method works and is how github manages there markdown toolbar but it involves a lot of extra imperative code to handle all the various browser implementations of the api and also exposes an interesting bug in chromes version of the api.

  • Chrome:
    With chrome you can insert text using document.execCommand('insertText',...) this command allows the user to modify the currently focused text box by replacing the selected text or cursor position with the provided text. *There is a bug with chrome that will remove the last line feed character if using this command to clear the last line. This can lead to the input getting wiped out and is currently active on githubs implementation.
    documentExecCommandInsertTextBugChrome
  • Firefox
    With firefox document.execCommand('insertText',...) will return false to let you know it doesnt work. Which can be used as a trigger to just use textarea.value = newValue to update the newValue manually as firefox is smart and and automatically save the undo state with manual modifications.
  • Internet Explorer
    I do not have a windows machine and can not yet confirm this but it appears for older version of IE you have to use the document.execCommand('ms-beginUndoUnit',..)
    then manually update the textarea.value and then exec the ms-endUndoUnit to save that undo.

The code to handle these various browser implementations is all contained in a single function insertText(textarea,text) that takes the textarea to update and the new text to insert
*This function does not try to update the currently selected text and expects the cursor to already be in the correct position


Current Solution:
Move full state management into react and not depend on the dom to keep any state (including cursor location, undo and redo history)
*using a 3rd party hook useUndo that has a simple api and exposes undo/redo/set/reset functions but may want to move to impliment custom solution that would allowed a debounced history

This requires:

  • adding hotkey listeners to bind the undo/redo hotkeys and prevent the native event from firing.
  • adding cursor position in state along with text value to allow undo & redo to also restore cursor position

By having the state fully in react it allows for a much simpler markdown style updater api that can reduce code duplication by making style changes small composable functions and not have to worry about different browsers functionality as react abstracts all that away for us.

API

First Attempt:

styleArg object with single applyMarkdown function to handle all styles

Borrowing heavily from githubs implementation and providing styleArg to the applyMarkdown function that handles all the updating logic.

// styleArg
{
    prefix = '',
    suffix = '',
    blockPrefix = '',
    blockSuffix = '',
    multiline = false,
    surroundWithNewLines = false
  }

This implementation works but requires adding more and more properties to handle all the different custom scenarios
ie:

  • smart url detection that updates url or name depending on what is selected
  • ordered list that needs to increment the prefix

All these special cases cause the functions and inputs to become very bloated and hard to follow as new features get add while also having to deal with all the issues using an api in the process of being deprecateddocument.execCommand


Current Implementation:
All style updates are handled with Styler functions that take {left,selection,right} object and return the same object and utility functions to convert from and to the saved TextState

*Proof of concept sandbox

type StyleState = {
left: string
selection: string
right: string
}

type Styler = (input: StyleState) => StyleState

type TextState = {
value: string
selectionStart: number
selectionEnd: number
}


function splitSelection (textState: TextState): StyleState

type Styler = (input: StyleState) => StyleState

function joinSelection (styleState: styleState): TextState

This allows for a much clearer api

  • All markdown style buttons share the same Styler type interface (making them composible)
  • All Stylers should be pure functions making them easier to test independently
  • No extra headache tracking the cursor position separate from the text (all that logic is handled by the split & join utility functions)
  • Also allows easy manipulation of selected text and non selected text at the same time.
    • Want to add text to the left of the selection, append to left: left + "**"
    • Want to add text to the selection, append to selection: "1. " + selection
    • Want to add text to right of selection, append to right: "**" + right

This feature was merged in with PR #1013. Names of the helper functions are different in the the actually implementation but this design doc is still a good reference to the thought process behind the design

@tomrule007 tomrule007 added the Design Doc Design Docs label Oct 20, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Doc Design Docs
Projects
None yet
Development

No branches or pull requests

1 participant