Front-End A Document Revisions System in JavaScript

Photo by 777546 on Pixabay

Quill is a rich text editor that supplies deltas of change. So you can build quite a sophisticated system of document revisions within the browser.

Let's first go over how you use Quill.

Quill in JavaScript

<!DOCTYPE html>
<html lang="en">
  <head>

    <!-- Include stylesheet -->
    <link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">

  </head>

  <body>

    <!-- Create the editor container -->
    <div id="editor" style="height: 375px;">
      <p>Hello World!</p>
      <p>Some initial <strong>bold</strong> text</p>
      <p><br></p>
    </div>

    <!-- Include the Quill library -->
    <script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>

    <!-- Initialize Quill editor -->
    <script>
      var quill = new Quill('#editor', {
        theme: 'snow'
      });
  </script>

  <body>
</html>

Lots of customizations of the toolbar are possible.

How to Get The Content

Plain Text

quill.getText()

HTML

quill.root.innerHTML

Delta

Yielding the change as Quill Delta,

quill.getContents()

Events

One can listen to events, and what you usually want is the HTML,

Text Change

  quill.on('text-change', function(delta, oldDelta, source) {     
        if (source == 'api') {
          console.log("An API call triggered this change.");
        } else if (source == 'user') {
          console.log("A user action triggered this change.");
        }
  });

Selection Change

  quill.on('selection-change', function(range, oldRange, source) {
        if (range) {
          if (range.length == 0) {
            console.log('User cursor is on', range.index);
          } else {
            var text = quill.getText(range.index, range.length);
            console.log('User has highlighted', text);
          }
        } else {
          console.log('Cursor not in the editor');
        }
  });

Editor Change

  quill.on('editor-change', function(eventName, ...args) {
    if (eventName === 'text-change') {
      // args[0] will be delta
    } else if (eventName === 'selection-change') {
      // args[0] will be old range
    }
  });

Comparing Document Revisions

If you have stored the content of different document revisions as a delta,

quill.getContents()

Then you can compare the revisions using:

  var diff = oldContent.diff(newContent);

Thereby obtaining a delta again.

To color the difference, use:

  for (var i = 0; i < diff.ops.length; i++) {
    var op = diff.ops[i];
    // if the change was an insertion
    if (op.hasOwnProperty('insert')) {
      // color it green
      op.attributes = {
        background: "#cce8cc",
        color: "#003700"
      };
    }
    // if the change was a deletion 
    if (op.hasOwnProperty('delete')) {
      // keep the text
      op.retain = op.delete;
      delete op.delete;
      // but color it red and struckthrough
      op.attributes = {
        background: "#e8cccc",
        color: "#370000",
        strike: true
      };
    }
  }

Compose the old content with the colored difference

  var adjusted = oldContent.compose(diff);

And display the tracked changes in the editor,

quill_diff.setContents(adjusted);

This code is adapted from a Codepen by Joe Pietruch.

The superstate State Library

superstate is a tiny efficient state library that distinguishes a state and a draft.

First, you define the initial state,

const text = superstate('
    <p>Hello World!</p>
    <p>Some initial <strong>bold</strong> text </p><p><br></p>
')

Then you create a draft with sketch

text.sketch('
    <p>Hello World!</p>
    <p>Some initial <strong>bold</strong> text </p>
    <p>And an <em>italic</em> scene</p><p><br></p>
') 

And you publish it to the state, with publish

text.publish()

Using superstate, together with the above code for comparing document revisions, we can revise a document, track changes, and then save the new revision to the state.

Persistence

When you work in the browser, you must auto-save your work if it is more than a few minutes worth of work.

You can do this with superstate using the localStorage adapter,

import { superstate } from '@superstate/core'
import { ls } from '@superstate/adapters'

const text = superstate(').use([
  ls('doc'), // 'doc' is the localStorage key
]) 

And now, every change to the state is saved to local storage. This does not apply to drafts unless you create the state with:

const count = superstate(0).use([ls('count', { draft: true })]) 

What is Missing for a Full System of Revisions?

We must add a component to accept/reject a highlighted change within the editor.

This design will be part of a whole document management system, with workflows of writing and revision.

Yoram Kornatzky

Yoram Kornatzky