Coder Social home page Coder Social logo

facebook / lexical Goto Github PK

View Code? Open in Web Editor NEW
17.2K 109.0 1.4K 42.56 MB

Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.

Home Page: https://lexical.dev

License: MIT License

JavaScript 25.87% HTML 0.11% CSS 2.03% Shell 0.02% TypeScript 71.96% MDX 0.01%

lexical's Introduction

GitHub Workflow Status Visit the NPM page Add yourself to our Discord Follow us on Twitter

Lexical is an extensible JavaScript web text-editor framework with an emphasis on reliability, accessibility, and performance. Lexical aims to provide a best-in-class developer experience, so you can easily prototype and build features with confidence. Combined with a highly extensible architecture, Lexical allows developers to create unique text editing experiences that scale in size and functionality.

For documentation and more information about Lexical, be sure to visit the Lexical website.

Here are some examples of what you can do with Lexical:


Overview:


Getting started with React

Note: Lexical is not only limited to React. Lexical can support any underlying DOM based library once bindings for that library have been created.

Install lexical and @lexical/react:

npm install --save lexical @lexical/react

Below is an example of a basic plain text editor using lexical and @lexical/react (try it yourself).

import {$getRoot, $getSelection} from 'lexical';
import {useEffect} from 'react';

import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';

const theme = {
  // Theme styling goes here
  // ...
}

// When the editor changes, you can get notified via the
// LexicalOnChangePlugin!
function onChange(editorState) {
  editorState.read(() => {
    // Read the contents of the EditorState here.
    const root = $getRoot();
    const selection = $getSelection();

    console.log(root, selection);
  });
}

// Lexical React plugins are React components, which makes them
// highly composable. Furthermore, you can lazy load plugins if
// desired, so you don't pay the cost for plugins until you
// actually use them.
function MyCustomAutoFocusPlugin() {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    // Focus the editor when the effect fires!
    editor.focus();
  }, [editor]);

  return null;
}

// Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will
// try to recover gracefully without losing user data.
function onError(error) {
  console.error(error);
}

function Editor() {
  const initialConfig = {
    namespace: 'MyEditor',
    theme,
    onError,
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <PlainTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>Enter some text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <OnChangePlugin onChange={onChange} />
      <HistoryPlugin />
      <MyCustomAutoFocusPlugin />
    </LexicalComposer>
  );
}

Lexical is a framework

The core of Lexical is a dependency-free text editor framework that allows developers to build powerful, simple and complex, editor surfaces. Lexical has a few concepts that are worth exploring:

Editor instances

Editor instances are the core thing that wires everything together. You can attach a contenteditable DOM element to editor instances, and also register listeners and commands. Most importantly, the editor allows for updates to its EditorState. You can create an editor instance using the createEditor() API, however you normally don't have to worry when using framework bindings such as @lexical/react as this is handled for you.

Editor States

An Editor State is the underlying data model that represents what you want to show on the DOM. Editor States contain two parts:

  • a Lexical node tree
  • a Lexical selection object

Editor States are immutable once created, and in order to create one, you must do so via editor.update(() => {...}). However, you can also "hook" into an existing update using node transforms or command handlers – which are invoked as part of an existing update workflow to prevent cascading/waterfalling of updates. You can retrieve the current editor state using editor.getEditorState().

Editor States are also fully serializable to JSON and can easily be serialized back into the editor using editor.parseEditorState().

Reading and Updating Editor State

When you want to read and/or update the Lexical node tree, you must do it via editor.update(() => {...}). You may also do read-only operations with the editor state via editor.getEditorState().read(() => {...}). The closure passed to the update or read call is important, and must be synchronous. It's the only place where you have full "lexical" context of the active editor state, and providing you with access to the Editor State's node tree. We promote using the convention of using $ prefixed functions (such as $getRoot()) to convey that these functions must be called in this context. Attempting to use them outside of a read or update will trigger a runtime error.

For those familiar with React Hooks, you can think of these $functions as having similar functionality:

Feature React Hooks Lexical $functions
Naming Convention useFunction $function
Context Required Can only be called while rendering Can only be called while in an update or read
Can be composed Hooks can call other hooks $functions can call other $functions
Must be synchronous
Other rules ❌ Must be called unconditionally in the same order ✅ None

Node Transforms and Command Listeners are called with an implicit editor.update(() => {...}) context.

It is permitted to do nest updates within reads and updates, but an update may not be nested in a read. For example, editor.update(() => editor.update(() => {...})) is allowed.

All Lexical Nodes are dependent on the associated Editor State. With few exceptions, you should only call methods and access properties of a Lexical Node while in a read or update call (just like $ functions). Methods on Lexical Nodes will first attempt to locate the latest (and possibly a writable) version of the node from the active editor state using the node's unique key. All versions of a logical node have the same key. These keys are managed by the Editor, are only present at runtime (not serialized), and should be considered to be random and opaque (do not write tests that assume hard-coded values for keys).

This is done because the editor state's node tree is recursively frozen after reconciliation to support efficient time travel (undo/redo and similar use cases). Methods that update nodes first call node.getWritable(), which will create a writable clone of a frozen node. This would normally mean that any existing references (such as local variables) would refer to a stale version of the node, but having Lexical Nodes always refer to the editor state allows for a simpler and less error-prone data model.

DOM Reconciler

Lexical has its own DOM reconciler that takes a set of Editor States (always the "current" and the "pending") and applies a "diff" on them. It then uses this diff to update only the parts of the DOM that need changing. You can think of this as a kind-of virtual DOM, except Lexical is able to skip doing much of the diffing work, as it knows what was mutated in a given update. The DOM reconciler adopts performance optimizations that benefit the typical heuristics of a content editable – and is able to ensure consistency for LTR and RTL languages automatically.

Listeners, Node Transforms and Commands

Outside of invoking updates, the bulk of work done with Lexical is via listeners, node transforms and commands. These all stem from the editor and are prefixed with register. Another important feature is that all the register methods return a function to easily unsubscribe them. For example here is how you listen to an update to a Lexical editor:

const unregisterListener = editor.registerUpdateListener(({editorState}) => {
  // An update has occurred!
  console.log(editorState);
});

// Ensure we remove the listener later!
unregisterListener();

Commands are the communication system used to wire everything together in Lexical. Custom commands can be created using createCommand() and dispatched to an editor using editor.dispatchCommand(command, payload). Lexical dispatches commands internally when key presses are triggered and when other important signals occur. Commands can also be handled using editor.registerCommand(handler, priority), and incoming commands are propagated through all handlers by priority until a handler stops the propagation (in a similar way to event propagation in the browser).

Working with Lexical

This section covers how to use Lexical, independently of any framework or library. For those intending to use Lexical in their React applications, it's advisable to check out the source-code for the hooks that are shipped in @lexical/react.

Creating an editor and using it

When you work with Lexical, you normally work with a single editor instance. An editor instance can be thought of as the one responsible for wiring up an EditorState with the DOM. The editor is also the place where you can register custom nodes, add listeners, and transforms.

An editor instance can be created from the lexical package and accepts an optional configuration object that allows for theming and other options:

import {createEditor} from 'lexical';

const config = {
  namespace: 'MyEditor',
  theme: {
    ...
  },
};

const editor = createEditor(config);

Once you have an editor instance, when ready, you can associate the editor instance with a content editable <div> element in your document:

const contentEditableElement = document.getElementById('editor');

editor.setRootElement(contentEditableElement);

If you want to clear the editor instance from the element, you can pass null. Alternatively, you can switch to another element if need be, just pass an alternative element reference to setRootElement().

Working with Editor States

With Lexical, the source of truth is not the DOM, but rather an underlying state model that Lexical maintains and associates with an editor instance. You can get the latest editor state from an editor by calling editor.getEditorState().

Editor states are serializable to JSON, and the editor instance provides a useful method to deserialize stringified editor states.

const stringifiedEditorState = JSON.stringify(editor.getEditorState().toJSON());

const newEditorState = editor.parseEditorState(stringifiedEditorState);

Updating an editor

There are a few ways to update an editor instance:

  • Trigger an update with editor.update()
  • Setting the editor state via editor.setEditorState()
  • Applying a change as part of an existing update via editor.registerNodeTransform()
  • Using a command listener with editor.registerCommand(EXAMPLE_COMMAND, () => {...}, priority)

The most common way to update the editor is to use editor.update(). Calling this function requires a function to be passed in that will provide access to mutate the underlying editor state. When starting a fresh update, the current editor state is cloned and used as the starting point. From a technical perspective, this means that Lexical leverages a technique called double-buffering during updates. There's an editor state to represent what is current on the screen, and another work-in-progress editor state that represents future changes.

Reconciling an update is typically an async process that allows Lexical to batch multiple synchronous updates of the editor state together in a single update to the DOM – improving performance. When Lexical is ready to commit the update to the DOM, the underlying mutations and changes in the update batch will form a new immutable editor state. Calling editor.getEditorState() will then return the latest editor state based on the changes from the update.

Here's an example of how you can update an editor instance:

import {$getRoot, $getSelection, $createParagraphNode} from 'lexical';

// Inside the `editor.update` you can use special $ prefixed helper functions.
// These functions cannot be used outside the closure, and will error if you try.
// (If you're familiar with React, you can imagine these to be a bit like using a hook
// outside of a React function component).
editor.update(() => {
  // Get the RootNode from the EditorState
  const root = $getRoot();

  // Get the selection from the EditorState
  const selection = $getSelection();

  // Create a new ParagraphNode
  const paragraphNode = $createParagraphNode();

  // Create a new TextNode
  const textNode = $createTextNode('Hello world');

  // Append the text node to the paragraph
  paragraphNode.append(textNode);

  // Finally, append the paragraph to the root
  root.append(paragraphNode);
});

If you want to know when the editor updates so you can react to the changes, you can add an update listener to the editor, as shown below:

editor.registerUpdateListener(({editorState}) => {
  // The latest EditorState can be found as `editorState`.
  // To read the contents of the EditorState, use the following API:

  editorState.read(() => {
    // Just like editor.update(), .read() expects a closure where you can use
    // the $ prefixed helper functions.
  });
});

Contributing to Lexical

  1. Clone this repository

  2. Install dependencies

    • npm install
  3. Start local server and run tests

    • npm run start
    • npm run test-e2e-chromium to run only chromium e2e tests
      • The server needs to be running for the e2e tests

npm run start will start both the dev server and collab server. If you don't need collab, use npm run dev to start just the dev server.

Optional but recommended, use VSCode for development

  1. Download and install VSCode

    • Download from here (it’s recommended to use the unmodified version)
  2. Install extensions

    • Flow Language Support
      • Make sure to follow the setup steps in the README
    • Prettier
      • Set prettier as the default formatter in editor.defaultFormatter
      • Optional: set format on save editor.formatOnSave
    • ESlint

Documentation

Browser Support

  • Firefox 52+
  • Chrome 49+
  • Edge 79+ (when Edge switched to Chromium)
  • Safari 11+
  • iOS 11+ (Safari)
  • iPad OS 13+ (Safari)
  • Android Chrome 72+

Note: Lexical does not support Internet Explorer or legacy versions of Edge.

Contributing

  1. Create a new branch
    • git checkout -b my-new-branch
  2. Commit your changes
    • git commit -a -m 'Description of the changes'
      • There are many ways of doing this and this is just a suggestion
  3. Push your branch to GitHub
    • git push origin my-new-branch
  4. Go to the repository page in GitHub and click on "Compare & pull request"
    • The GitHub CLI allows you to skip the web interface for this step (and much more)

Support

If you have any questions about Lexical, would like to discuss a bug report, or have questions about new integrations, feel free to join us at our Discord server.

Lexical engineers are checking this regularly.

Running tests

  • npm run test-unit runs only unit tests.
  • npm run test-e2e-chromium runs only chromium e2e tests.
  • npm run debug-test-e2e-chromium runs only chromium e2e tests in head mode for debugging.
  • npm run test-e2e-firefox runs only firefox e2e tests.
  • npm run debug-test-e2e-firefox runs only firefox e2e tests in head mode for debugging.
  • npm run test-e2e-webkit runs only webkit e2e tests.
  • npm run debug-test-e2e-webkit runs only webkit e2e tests in head mode for debugging.

License

Lexical is MIT licensed.

lexical's People

Contributors

2wheeh avatar acywatson avatar akmarzhan1 avatar birtles avatar btezzxxt avatar chroniclynx avatar dependabot[bot] avatar ebads67 avatar etrepum avatar fantactuka avatar germanjablo avatar harrysiv avatar icrosil avatar im-adithya avatar ivailop7 avatar justinhaaheim avatar karam-qaoud avatar kraisler avatar lucinyan avatar mrkev avatar noi5e avatar prontiol avatar steveluscher avatar stylet avatar thegreatercurve avatar thorn0 avatar trueadm avatar tylerjbainbridge avatar yangshun avatar zurfyx avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

lexical's Issues

selection: moving word backward/forward on the extremities of text that has siblings is glitchy

Steps to repro:

  • type foo bar baz

  • make bar a separate node by making it bold

  • move the caret so that the offset is positionned at the end of foo or the start of baz, spaces included, and try to move forward or backward respectively.

  • observe that the caret goes at the start of bar and not at the end as expected or only at the end when moving backward.

This is because in OutlineSelection:moveWordForward:1070 we do this, which basically does the selection if we looked ahead already.

 if (hasNewLine || lookAhead) {
              node.select(s, s);
              return;
            }

I tried to fix very naively but I broke other things doing so.

Bug: entering space in a hashtag is broken

Hashtags don't seem to function correctly when spaces are inserted anywhere in the hashtag. This includes

  • Add the end of a hashtag
  • In the middle of a hashtag
  • At the start of the hashtag

In fact, entering any text at the start of a hashtag also doesn't seem to work properly. It should be possible to enter text at the start and then delete that text again.

Request: setup Playwright for e2e testing

Playwright seems like a better option for us, especially with its support for more browsers. Plus we can run Playwright in headless and have it work on our existing CI pipeline. We need to hook it up to Jest or some other test runner too and insert a basic typing + text selection test.

I was thinking this can use our Github playground for now, running on localhost:3000, so it will require some fiddling around to setup. In terms of commands, I was thinking:

  • yarn test runs all unit and e2e tests (in headless mode)
  • yarn test-unit runs unit tests
  • yarn test-unit --debug runs unit tests in debug mode (see yarn debug-test)
  • yarn test-e2e runs e2e tests (in headless mode)
  • yarn test-e2e --debug runs e2e tests without headless mode, so we can debug issues. The browser should not close after running the test.

We should aim to support as many of the browsers they support. I believe that gives us WebKit, Chromium, Firefox and possibly Android.

Bug: Emotes do not change to emojii when dictated

OS: Windows 10
Browser: Chrome 88
AT: Dragon Naturally Speaking Professional Group 15.61

Test Steps:

  1. Place focus in input field
  2. Dictate: "Winky Face" "Frowny face", "Smiley face"

Actual Results
Emotes are written but stay emotes

Expected results
Emotes should convert to emojii

dragon.mp4

Blocks break apart unexpectedly when text contains tabs

Repro

  • copy('ABD\tEFG') to your clipboard
  • Paste it into the Outline editor
  • NOTICE: you end up with two blocks (edit: text node) after you paste, instead of one.
  • Add the missing ‘C’
  • NOTICE: the text is now split among three blocks. Bad.
  • Press Fn-Option-Delete to delete forward

Chrome

You now have an empty block.

Screen.Recording.2021-03-01.at.4.12.08.PM.mov

Firefox

You fataled with Expected node _5 to have a parent.

Screen.Recording.2021-03-01.at.4.12.52.PM.mov

Bug: Spellcheck triggers early because of custom insertText logic

By default (Chrome) spellchecker waits for 1-2 seconds before triggering, so if you're typing hous you can still type house without the error underline showing. This works fine when native event handler triggers:

<div texteditable="true" spellcheck="true"></div>

Outline handles the events itself (OutlineEventHandlers):

event.preventDefault(); <--
...
      case 'insertText':
      case 'insertFromComposition': {
        if (data) {
          insertText(selection, data); <-- type it manually
        }
        break;
      }

Repro: https://codepen.io/zurfyx/pen/zYoVdRx?editors=1010

The logic for insertText(selection: Selection, text: string) is quite complex but I believe that ultimately we could compare trees and determine whether the native input would equal the computed input, and if that's the case favor the native over ours.

(Handling the spellchecker API ourselves doesn't look possible in Chrome and we would also have to handle each browser separately.)

cc @trueadm

Emoticons not working on return/enter

OS: Windows 10
Browser: Chrome

Test Steps:

  • Enter emoticon command ie :)
  • press enter

Expected results:
Emoticon turns into emojii

Actual results:
Emoticon stays emote

Bug: Outline doesn't work with Dragon Naturally Speaking

OS: Windows 10
Browser: Chrome 88
AT: Dragon Naturally Speaking Professional Group 15.61

Test Steps:

  1. Place focus in input field
  2. dictate some text

Expected results:
Placeholder text should disappear

Actual results:
dictated text is appended to placeholder text

dragon.mp4

Request: Check for queueMicroTask

We should use this before falling back to Promise.resolve for Outline updates. Arguably, we should maybe only use queueMicroTask if we can.

Bug: Selection across paragraphs selects a ghost character

When the selection boundary crosses over to another line, looks like a ghost character is getting selected before the first/last character gets selected in the paragraph where selection is entering.

Steps (from the screencast below):

  • Place the caret after character B.
  • Use Shift+Arrow Right twice to select two characters.

Expected: 'B' & 'A' to be both selected.
Actual: 'B' & a ghost character are selected.

Screen.Recording.2021-03-11.at.12.15.22.PM.mov

Bug: Ctrl+A/E unable to skip over mentions

Ideally Ctrl+A/E should behave the same as Cmd+ Left/Right and move the cursor Home/End, but when there's a mention in the editor Ctrl+A/E stops at the edge of the mention. Cmd + Left/Right works correctly.

This is due to a browser issue, and other editors like gmail behave the same way. Therefore this is a feature request.

Repro:

  1. Type a mention
  2. Type some text
  3. Press Ctrl+A and notice the cursor moves to the end of the mention, not the beginning of the line.
Screen.Recording.2021-04-01.at.11.53.54.AM.mov

Same is true if you type another mention and move the cursor between the two. Ctrl+A/E are blocked by the mentions on both sides.

[outline-mentions-plugin] Creates paragraph erroneously upon detecting mention

Repro

  1. Fire up the Outline example (yarn install && yarn start)
  2. Type ‘Hello’
  3. Press option-enter for a newline
  4. Type something that triggers the mentions autocomplete (eg. ‘The’)

Observe that the newline gets erroneously converted into a paragraph as soon as a mentionable run of text is detected.

Untitled.mov

Bug: Copy + Paste creates unnecessary empty text nodes

When pasting in content from two blocks in Outline (rich text mode), and pasting that content, we end up with empty text nodes. I feel like this issue is likely to do with the fact we aren't calling block.normalizeTextNodes somewhere.

Steps:

  • Ensure in rich text mode
  • Type some text into the first block
  • Hit enter to create a new block and type some more text
  • Select all text and cut the content
  • Paste content into the editor
  • Notice the empty text nodes
Screen.Recording.2021-03-02.at.15.04.15.mov

Bug: segment error with empty text nodes

Steps to reproduce:

  • Insert '#foo'
  • Move selection to start
  • Type 'a'
  • Press Backspace
  • Manually move selection to end
  • Hold down Backspace

Notice that we have an editor error.

cmd + delete goes too far on chromium browsers

repro:

  • type foobar
  • hit enter
  • type baz
  • cmd+delete to delete line while cursor is at the end of baz

results:

foo

expected:

foobar

Browser tested:
chromium/edge: repro the errors
safari: deleteHardLineBackward not supported error
firefox: does it correctly

Screen.Recording.2021-01-29.at.11.41.20.mov

Selecting a sticker in comment editor causes an automatic post

  1. turn on NVDA (I was using nvda at the time I don't know if that is relevant)
  2. select a post
  3. put cursor inside of comment editor
  4. select sticker

Result:
Comment is posted without selected sticker

Expected result:
sticker should be attached to comment

stickers.mp4

Delete backward/forward and move backward/forward should operate over whole graphemes

Consider these multi codepoint graphemes:

// 👨‍👩‍👧‍👧
const family = '\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67';

// 👏🏽
const clappingHandsWithMediumSkinTone = '\uD83D\uDC4F\uD83C\uDFFD';

Right now you need to press the left/right arrow key four times to skip over the clapping Emoji:

Screen.Recording.2021-01-20.at.1.46.00.PM.mov

…and you need to press delete or fn-delete four times to eliminate it:

Screen.Recording.2021-01-20.at.1.44.39.PM.mov

Bug: deleteWordBackward/deleteWordForward getTargetRanges handling of tab whitespace

I believe this is soemwhat related to #66.

When executing a deleteWordBackward and deleteWordForward on text that contains a tab when using getTargetRanges on Chrome (maybe Safari), it removes the tab as part of the word. Our polyfill doesn't do this, which @steveluscher mentioned was the expected behavior.

What do we do with this? If native beforeinput does this in all browsers, then maybe that is the correct thing to do, and we should update our polyfills?

Code + Bold/Italic formatting buggy

Repro Steps

  1. Type some text, e.g. "Hello World"
  2. Select entire text
  3. Format with Bold
  4. Format with Code
  5. Unformat Bold

Expected Result

Code isn't bold anymore

Actual Result

See that the TextNode in the TreeView.js only has the code flag but the visual result is still bold, because the underlying tag is still <strong>.

image

Supporting Grammarly and Google Translate

DraftJS doesn't work with either because of many reasons that either: conflict with React's rendering, or just don't support Draft's internal model. I feel that we should do better here.

These browser plugins are not only heavily used by a large majority of the Internet, they also provide a much better experience for users that depend on these for accessibility reasons (which includes plugins other than these two).

I feel like we should spent some effort on investigating and possibly implementing support for these browser plugins. We can obviously disable them (you can use DOM attributes and classes to do so) but I feel like we should at least explore this space.

Doing a test

"Every one of us is, the cosmic perspective, precious. The human disagrees with you, let him love. In a hundred billion galaxies, you will not find another."
– Carl Sagan, cosmos

#this is a list:
*item 1
*item 2
*item 3

Actually run unit tests in parallel

Right now we have Circle CI set up to run tests in parallel but we're not actually distributing batches of tests across workers; we're just running all the tests 20 times.

image

Either de-configure this and use one worker, or look up a blog post on the internet that explains how to actually do test splitting with Jest, Circle CI, and jest-junit.

Plan: Set up an E2E test-suite

It occured to me that we should start to write down test cases that we feel help cover many of the weak/complex parts of Outline. It's always best to add regression tests as we add features or fix issues, but I'm well aware that this isn't necessarily do-able right now, as we are lacking testing infra to do so. So maybe we can update this issue with suggested e2e test flows so that we don't forget to add them later.

I've added the various permutations of keyboard controls we'd want to test in brackets (if there's more than one, or if it differs between platforms).

Test Cases

Test Case 1

  • Enter no text into editor. Press (Enter) to create a new paragraph, then press (Space) and type some text into the new paragraph. It should be possible to use keyboard arrows to return to the previous paragraph (All: Left, Right, Up, Down) and then move back to the new paragraph. It should also be possible to select all (Windows: CTRL+A, Mac: CMD+A) and delete (All: Backspace/Delete), removing the second paragraph and clearing out the first paragraph.

Recent regression with textNode.splitText

Since merging #121, we now encounter issues with focus restoration. Here's a repro using the playground:

  • Toggle on character limit
  • Type text until you hit limit
  • Should fatal

Unhandled Rejection (IndexSizeError): Failed to execute 'setBaseAndExtent' on 'Selection': The offset 31 is larger than the node's length (30).

Multiple classes applied when multiple flags are set on Text Node

If a text node has underline and strikethrough, Outline currently applies three classes if specified in the theme - underline, strikethrough, underlineStrikethrough. If these classes are set as below, one could be overriden by another depending on the order in the CSS file and cause text node to not have underlineStrikethrough in case it gets overriden by one of the other two.

# myStyles.css

.underlineStrikethrough {
  text-decoration: 'underline line-through';
}

.underline {
  text-decoration: 'underline';
}

.strikethrough {
  text-decoration: 'line-through';
}

Request: Add support for copy and pasting HTML content

This is a good first feature as it is a self-contained feature that has a fixed scope.

We don't support pasting HTML content into Outline currently. We only support plain-text and content that has come from Outline directly. Given that Draft supports HTML, we should also strive to support it. This will involve parsing HTML content and creating the respective Outline nodes for the content where possible (rich means it's really only for rich text mode). If we find invalid nodes, we should normalize/skip them if needed.

Ensure that all forms of whitespace (en spaces, em spaces, etc) are handled by the backward/forward word delete functions

When you press fn-Option-delete on a Mac keyboard to delete a word, the OS counts leading whitespace as part of the range to be deleted. This must include other kinds of whitespace, other than just U+0020.

Repro:

  1. Consider the string Hello\u2002World.
  2. Place the caret after the word ‘Hello’
  3. Press fn-Option-delete

The en space and the word ‘World’ should be deleted.

Then, consider all types of whitespace other than just \u2002.

Fatal when deleting word forward before preceding tab

Repro

  1. Type Hello\u0009World
  2. Move cursor to end of word ‘Hello’
  3. Press fn-option-delete

Observe fatal.

Outline.js:1723 Uncaught Error: Expected node _4 to have a parent.
    at TextNode.getParentOrThrow (Outline.js:1723)
    at TextNode.getNextSibling (Outline.js:1791)
    at Selection.deleteWordForward (Outline.js:605)
    at onKeyDown (useOutlineInputEvents.js:161)
    at useOutlineEventWrapper.js:27
    at Outline.js:3214
    at enterViewModelScope (Outline.js:2760)
    at updateEditor (Outline.js:3209)
    at OutlineEditor.update (Outline.js:3405)
    at HTMLDivElement.<anonymous> (useOutlineEventWrapper.js:27)

Request: Adding newlines to <code> blocks should make new lines

Currently when you press enter on a code block, it exists the code block and creates a paragraph below. We should instead add a line break. If there are two consecutive line breaks, then we should break out of the code block and create a paragraph. This behavior would then be consistent with how code blocks work with Quip/Draft.

Bug: Text cursor indicator not following cursor

OS: Windows 10
Browser: Chrome 88
AT: NVDA 2020.4, Text Cursor indicator

I have text cursor indicator turned on in windows and I noticed the indicator was not following the cursor when using up and down arrows to navigate between two paragraphs.

To turn on text cursor indicator:

  • Hit Windows Key
  • Type accessibility
  • Open the ease of access center
  • Navigate to "text cursor"
  • select the switch to "Turn on text cursor indicator"
  • adjust settings to make it easily to see, I attached a screenshot of mine

image

Test Steps
Write two paragraphs
Use the up arrow to navigate to the space between both paragraphs

Expected Results
The cursor indicator should follow the cursor

Actual Results
the cursor indicator stays in place and the cursor moves to the space between the paragraphs

Notes

  • hitting the up arrow a second time will rejoin the cursor with the cursor indicator
  • I noticed this is also an issue in github's text editor so it may be browser or OS related.

Screenshot
I couldn't find a way to actually screenshot the cursor so the red line shows where the cursor was when I took the screenshot.
image

Plan: Handling Text Selection/Deletion

This is an umbrella issue to cover all the various use-cases for keyboard text selection and text deletion.

Note: we may change parts of this plan.

Keyboard Text Selection

Moving backward/forward

  • We should listen to left and right arrow events (without the relevant modifier for the OS)
  • We may have to apply some polyfills around moving selection between blocks
  • We may need to apply some custom logic around segmented/immutable nodes for accessibility purposes
  • Otherwise, should use the default native browser behavior

Deleting backward/forward

  • If beforeinput is supported we should attempt to use the boundary from getTargetRanges()
  • If beforeinput is not supported, we should apply a naive (and cheap) boundary implementation
  • We may need to apply some custom logic around the deletion of adjacent segmented/immutable nodes

Moving/Deleting a word backward/forward

  • If Intl.Segmenter is supported, we should always use this to work out the right word boundary
  • If Intl.Segmenter is not supported, we should check if native beforeinput is supported.
  • If beforeinput is supported we should attempt to use the boundary from getTargetRanges()
  • If beforeinput is not supported, we should apply a naive (and cheap) word boundary implementation

Moving/Deleting a line backward/forward

  • If beforeinput is supported we should attempt to use the line boundary from getTargetRanges()
  • If beforeinput is not supported, we should apply a naive approach and move the selection to the start of the current block.

Moving a block backward/forward

  • Should use the default native browser behavior

Move forward command can't escape empty block

Repro

  1. Enter the following text: \nHello
  2. Put your cursor at the beginning of the input
  3. Press the right arrow key

Observe that the cursor does not advance.

Screen.Recording.2021-01-27.at.10.37.07.AM.mov

Build broken

After merging the latest PRs, it seems that the build is broken. After looking into it, it looks related to usage of flatMap, which the CI node version does not support. We should probably upgrade the Node version on CI to a version that doesn support flatMap.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.