Git Product home page Git Product logo

mobiledoc-kit's Introduction

Mobiledoc Kit CI Build Status

Mobiledoc Logo

Mobiledoc Kit is a framework-agnostic library for building WYSIWYG editors supporting rich content via cards.

Libraries

This repository hosts the core Mobiledoc Kit library. If you want to use Mobiledoc Kit to create a WYSIWYG editor you have the following options:

Environment Library
Plain JavaScript mobiledoc-kit (this repo)
Ember ember-mobiledoc-editor
React react-mobiledoc-editor

If you only want to use the Mobiledoc-Kit runtime, for rendering mobiledoc posts only (not editing or creating them), you can use:

Output Format/Environment Library
Plain JavaScript In-Browser (DOM) mobiledoc-dom-renderer
Server-Side Rendering (HTML) see mobiledoc-dom-renderer's Rendering HTML section
Server-Side Rendering (Text-only, e.g. SEO) mobiledoc-text-renderer
In-Browser (DOM) Rendering, with Ember ember-mobiledoc-dom-renderer
React Server and Browser Renderer mobiledoc-react-renderer
🔮 Render Mobiledoc as VDOM by passing React or React-like createElement function mobiledoc-vdom-renderer

Mobiledoc is a deliberately simple and terse format, and you are encouraged to write your own renderer if you have other target output formats (e.g., a PDF renderer, an iOS Native Views Renderer, etc.).

Demo

Try a demo at bustle.github.io/mobiledoc-kit/demo.

API Documentation

API Documentation is available online.

Intro to Mobiledoc Kit

  • Posts are serialized to a JSON format called Mobiledoc instead of to HTML. Mobiledoc can be rendered for the web, mobile web, or in theory on any platform. Mobiledoc is portable and fast.
  • The editor makes limited use of Content Editable, the siren-song of doomed web editor technologies.
  • Mobiledoc is designed for rich content. We call rich sections of an article "cards" and rich inline elements "atoms" and implementing a new one doesn't require an understanding of Mobiledoc editor internals. Adding a new atom or card takes an afternoon, not several days. To learn more, see the docs for Atoms, Cards and Mobiledoc Renderers

To learn more about the ideas behind Mobiledoc and the editor (note that the editor used to be named Content-Kit), see these blog posts:

The Mobiledoc kit saves posts in Mobiledoc format.

Usage

The Mobiledoc.Editor class is invoked with an element to render into and optionally a Mobiledoc to load. For example:

const simpleMobiledoc = {
  version: '0.3.2',
  markups: [],
  atoms: [],
  cards: [],
  sections: [[1, 'p', [[0, [], 0, 'Welcome to Mobiledoc']]]],
}
const element = document.querySelector('#editor')
const options = { mobiledoc: simpleMobiledoc }
const editor = new Mobiledoc.Editor(options)
editor.render(element)

options is an object which may include the following properties:

  • mobiledoc - [object] A mobiledoc object to load and edit.
  • placeholder - [string] default text to show before a user starts typing.
  • spellcheck - [boolean] whether to enable spellcheck. Defaults to true.
  • autofocus - [boolean] When true, focuses on the editor when it is rendered.
  • undoDepth - [number] How many undo levels should be available. Default value is five. Set this to zero to disable undo/redo.
  • cards - [array] The list of cards that the editor may render
  • atoms - [array] The list of atoms that the editor may render
  • cardOptions - [object] Options passed to cards and atoms
  • unknownCardHandler - [function] This will be invoked by the editor-renderer whenever it encounters an unknown card
  • unknownAtomHandler - [function] This will be invoked by the editor-renderer whenever it encounters an unknown atom
  • parserPlugins - [array] See DOM Parsing Hooks
  • tooltipPlugin - [object] Optional plugin for customizing tooltip appearance

The editor leverages unicode characters, so HTML documents must opt in to UTF8. For example this can be done by adding the following to an HTML document's <head>:

<meta charset="utf-8" />

Editor API

  • editor.serialize(version="0.3.2") - serialize the current post for persistence. Returns Mobiledoc.
  • editor.destroy() - teardown the editor event listeners, free memory etc.
  • editor.disableEditing() - stop the user from being able to edit the current post with their cursor. Programmatic edits are still allowed.
  • editor.enableEditing() - allow the user to make edits directly to a post's text.
  • editor.editCard(cardSection) - change the card to its edit mode (will change immediately if the card is already rendered, or will ensure that when the card does get rendered it will be rendered in the "edit" state initially)
  • editor.displayCard(cardSection) - same as editCard except in display mode.
  • editor.range - Read the current Range object for the cursor.

Position API

A Position object represents a location in a document. For example your cursor may be at a position, text may be inserted at a position, and a range has a starting position and an ending position.

Position objects are returned by several APIs, for example deleteRange returns a position. Some methods, like splitSection accept a position as an argument.

A position can be created for any point in a document with section#toPosition(offset).

Position API includes:

  • position.section - The section of this position
  • position.offset - The character offset of this position in the section.
  • position.marker - Based on the section and offset, the marker this position is on. A position may not always have a marker (for example a cursor before or after a card).
  • position.toRange(endPosition) - Create a range based on two positions. Accepts the direction of the range as a second optional argument.
  • position.isEqual(otherPosition) - Is this position the same as another
  • position.move(characterCount) - Move a number of characters to the right (positive number) or left (negative number)
  • position.moveWord(direction) - Move a single word in a given direction.

Range API

Range represents a range of a document. A range has a starting position (head), ending position (tail), and a direction (for example highlighting text left-to-right is a forward direction, highlighting right-to-left is a backward direction).

Ranges are returned by several APIs, but most often you will be interested in the current range selected by the user (be it their cursor or an actual selection). This can be accessed at editor#range. Several post editor APIs expect a range as an argument, for example setRange or deleteRange.

Ranges sport several public APIs for manipulation, each of which returns a new, unique range instance:

  • range.head - The position on the range closer to the start of the document.
  • range.tail - The position on the range closer to the end of the document.
  • range.isCollapsed - A range is collapsed when its head and tail are the same position.
  • range.focusedPosition - If a range has a forward direction, then tail. If it has a backward direction, then head.
  • range.extend(characterCount) - Grow a range one character in whatever its direction is.
  • range.move(direction) - If the range is collapsed, move the range forward one character. If it is not, collapse it in the direction passed.
  • range.expandByMarker(callback) - In both directions attempt grow the range as long as callback returns true. callback is passed each marker as the range is grown.

Editor Lifecycle Hooks

API consumers may want to react to given interaction by the user (or by a programmatic edit of the post). Lifecycle hooks provide notification of change and opportunity to edit the post where appropriate.

Register a lifecycle hook by calling the hook name on the editor with a callback function. For example:

editor.didUpdatePost(postEditor => {
  let { range } = editor
  let cursorSection = range.head.section

  if (cursorSection.text === 'add-section-when-i-type-this') {
    let section = editor.builder.createMarkupSection('p')
    postEditor.insertSectionBefore(section, cursorSection.next)
    postEditor.setRange(new Mobiledoc.Range(section.headPosition))
  }
})

The available lifecycle hooks are:

  • editor.didUpdatePost(postEditor => {}) - An opportunity to use the postEditor and possibly change the post before rendering begins.
  • editor.willRender() - After all post mutation has finished, but before the DOM is updated.
  • editor.didRender() - After the DOM has been updated to match the edited post.
  • editor.willDelete((range, direction, unit)) - Provides range, direction and unit to identify the coming deletion.
  • editor.didDelete((range, direction, unit)) - Provides range, direction and unit to identify the completed deletion.
  • editor.cursorDidChange() - When the cursor (or selection) changes as a result of arrow-key movement or clicking in the document.
  • editor.onTextInput() - When the user adds text to the document (see example)
  • editor.inputModeDidChange() - The active section(s) or markup(s) at the current cursor position or selection have changed. This hook can be used with Editor#activeMarkups, Editor#activeSections, and Editor#activeSectionAttributes to implement a custom toolbar.
  • editor.beforeToggleMarkup(({markup, range, willAdd}) => {...}) - Register a callback that will be called before editor#toggleMarkup is applied. If any callback returns literal false, the toggling of markup will be canceled. (Toggling markup done via the postEditor, e.g. editor.run(postEditor => postEditor.toggleMarkup(...)) will skip this callback.
  • editor.willCopy(({html, text, mobiledoc}) => {...}) - Called before the serialized versions of the selected markup is copied to the system pasteboard. editor.willPaste(({html, text, mobiledoc}) => {...}) - Called before the serialized versions of the system pasteboard is pasted into the mobiledoc.

For more details on the lifecycle hooks, see the Editor documentation.

Programmatic Post Editing

A major goal of the Mobiledoc kit is to allow complete customization of user interfaces using the editing surface. The programmatic editing API allows the creation of completely custom interfaces for buttons, hot-keys, and other interactions.

To change the post in code, use the editor.run API. For example, the following usage would mark currently selected text as "strong":

editor.run(postEditor => {
  postEditor.toggleMarkup('strong')
})

It is important that you make changes to posts, sections, and markers through the run and postEditor API. This API allows the Mobiledoc editor to conserve and better understand changes being made to the post.

editor.run(postEditor => {
  const mention = postEditor.builder.createAtom('mention', 'Jane Doe', { id: 42 })
  // insert at current cursor position:
  // or should the user have to grab the current position from the editor first?
  postEditor.insertMarkers(editor.range.head, [mention])
})

For more details on the API of postEditor, see the API documentation.

For more details on the API for the builder, required to create new sections atoms, and markers, see the builder API.

Configuring hot keys

The Mobiledoc editor allows the configuration of hot keys and text expansions. For instance, the hot-key command-B to make selected text bold, is registered internally as:

const boldKeyCommand = {
  str: 'META+B',
  run(editor) {
    editor.run(postEditor => postEditor.toggleMarkup('strong'))
  },
}
editor.registerKeyCommand(boldKeyCommand)

All key commands must have str and run properties as shown above.

str describes the key combination to use and may be a single key, or modifier(s) and a key separated by +, e.g.: META+K (cmd-K), META+SHIFT+K (cmd-shift-K)

Modifiers can be any of CTRL, META, SHIFT, or ALT.

The key can be any of the alphanumeric characters on the keyboard, or one of the following special keys:

BACKSPACE, TAB, ENTER, ESC, SPACE, PAGEUP, PAGEDOWN, END, HOME, LEFT, UP, RIGHT, DOWN, INS, DEL

Overriding built-in keys

You can override built-in behavior by simply registering a hot key with the same name. For example, to submit a form instead of entering a new line when enter is pressed you could do the following:

const enterKeyCommand = {
  str: 'enter',
  run(editor) {
    // submit the form
  },
}
editor.registerKeyCommand(enterKeyCommand)

To fall-back to the default behavior, return false from run.

Responding to text input

The editor exposes a hook onTextInput that can be used to programmatically react to text that the user enters. Specify a handler object with text or match properties and a run callback function, and the editor will invoke the callback when the text before the cursor ends with text or matches match. The callback is called after the matching text has been inserted. It is passed the editor instance and an array of matches (either the result of match.exec on the matching user-entered text, or an array containing only the text).

editor.onTextInput({
  text: 'X',
  run(editor) {
    // This callback is called after user types 'X'
  },
})

editor.onTextInput({
  match: /\d\dX$/, // Note the "$" end anchor
  run(editor) {
    // This callback is called after user types number-number-X
  },
})

The editor has several default text input handlers that are defined in src/js/editor/text-input-handlers.js.

To remove default text input handlers call the unregister function.

editor.unregisterAllTextInputHandlers()

\n special-case match character

When writing a matching string it is common to use \s at the end of a match regex, thus triggering the handler for a given string when the users presses the space or tab key.

When the enter key is pressed no actual characters are added to a document. Instead a new section is created following the current section. Despite this, you may use \n in a match regex to capture moments when the enter key is pressed. For example if you wanted to process a URL for auto-linking you might want to process the string on both the space key and when the user hits enter.

Since \s is a superset of \n, that makes the following regex a valid match for a hand-typed URL after the user presses space or enter:

/\b(https?:\/\/[^\s]+)\s$/

DOM Parsing hooks

A developer can override the default parsing behavior for leaf DOM nodes in pasted HTML.

For example, when an img tag is pasted it may be appropriate to fetch that image, upload it to an authoritative source, and create a specific kind of image card with the new URL in its payload.

A demonstration of this:

function imageToCardParser(node, builder, { addSection, addMarkerable, nodeFinished }) {
  if (node.nodeType !== 1 || node.tagName !== 'IMG') {
    return
  }
  const payload = { src: node.src }
  const cardSection = builder.createCardSection('my-image', payload)
  addSection(cardSection)
  nodeFinished()
}
const options = {
  parserPlugins: [imageToCardParser],
}
const editor = new Mobiledoc.Editor(options)
const element = document.querySelector('#editor')
editor.render(element)

Parser hooks are called with three arguments:

  • node - The node of DOM being parsed. This may be a text node or an element.
  • builder - The abstract model builder.
  • env - An object containing three callbacks to modify the abstract
    • addSection - Close the current section and add a new one
    • addMarkerable - Add a markerable (marker or atom) to the current section
    • nodeFinished - Bypass all remaining parse steps for this node

Note that you must call nodeFinished to stop a DOM node from being parsed by the next plugin or the default parser.

Tooltip Plugins

Developers can customize the appearance of tooltips (e.g. those shown when a user hovers over a link element) by specificying a tooltip plugin. A tooltip plugin is an object that implements the renderLink method.

The renderLink method is called with three arguments:

  • tooltip - The DOM element of the tooltip UI.
  • link - The DOM element (HTMLAnchorElement) of the link to display a tooltip for.
  • actions - An object containing functions that can be called in response to user interaction.
    • editLink - A function that, when called, prompts the user to edit the href of the link element in question.

The renderLink method is responsible for populating the passed tooltip element with the correct content to display to the user based on the link in question. This allows Mobiledoc users to, for example, provide localized tooltip text via their system of choice.

const MyTooltip = {
  renderLink(tooltip, link, { editLink }) {
    tooltip.innerHTML = `${i18n.translate('URL: ')} ${link.href}`
    const button = document.createElement('button')
    button.innerText = i18n.translate('Edit')
    button.addEventListener('click', editLink)
    tooltip.append(button)
  },
}
const editor = new Mobiledoc.Editor({
  tooltipPlugin: MyTooltip,
})
const element = document.querySelector('#editor')
editor.render(element)

Contributing

Fork the repo, write a test, make a change, open a PR.

Tests, Linting, Formatting

Install dependencies via yarn:

  • Node.js is required
  • Install yarn globally: npm install -g yarn or brew install yarn
  • Install dependencies with yarn: yarn install

Run tests via the built-in broccoli server:

  • yarn start
  • open http://localhost:4200/tests

Or run headless tests via testem:

  • yarn test

Tests in CI are run at Github Actions via Saucelabs (see the test:ci yarn script).

Run linter

  • yarn lint

Run formatting

  • yarn format

Demo

To run the demo site locally:

  • yarn start
  • open http://localhost:4200/demo/

The assets for the demo are in /demo.

Debugging

A debugging environment that prints useful information about the active Mobiledoc editor can be access by:

  • yarn start
  • open http://localhost:4200/demo/debug.html

Getting Help

If you notice a bug or have a feature request please open an issue on github. If you have a question about usage you can post in the slack channel (automatic invites available from our slackin app) or on StackOverflow using the mobiledoc-kit tag.

Releasing (Implementer notes)

  • Use np (yarn install -g np)
  • np <version> (e.g. np 0.12.0)
  • git push <origin> --tags

Deploy the website (demo & docs)

The demo website is hosted at bustle.github.io/mobiledoc-kit/demo.

To publish a new version:

  • yarn run build:website - This builds the website into dist/website
  • yarn run deploy:website - Pushes the website to the gh-pages branch of the origin at github

Development of Mobiledoc and the supporting libraries was generously funded by BDG Media.

mobiledoc-kit's People

Contributors

bantic avatar claytongentry avatar courajs avatar disordinary avatar ef4 avatar eguitarz avatar erisds avatar gitter-badger avatar gpoitch avatar greenkeeperio-bot avatar joshfrench avatar kevinansfield avatar lukemelia avatar maazali avatar martinklepsch avatar mattmcmanus avatar miguelcobain avatar mixonic avatar nikse avatar prayagverma avatar randohinn avatar rlivsey avatar ruiokada avatar stfnio avatar tholman avatar toddself avatar vitosamson avatar yoranbrondsema avatar zeejab avatar zfoster 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

mobiledoc-kit's Issues

editor-dom renderer leaks memory

The editor-dom doesn't recursively remove children of renderNodes that are marked for removal, so the render tree's element map tends to grow over time to contain many elements that have been removed from DOM.

Long-term vision

Hi,

We're HSTRY, an education technology start-up. Up until now, we have been using our own contenteditable rich-text editor but are now running into its limitations. I don't think it's necessary for me to repeat how much of a PITA it is to make contenteditable run consistently across all browsers. As an FYI, our front-end is built with Ember.js.

Until now, only admins in the HSTRY application had rich-text editing capabilities. However, we now want to open it up to all our users. We want to step away from our direct-to-HTML contenteditable editor because it's inconsistent across browsers and not easily extendible with custom "tags" (what you call "cards"). But since 50k+ users will now start adding rich-text information to their content, we want to go for a solution that will work long-term. It will be extremely difficult for us to change the underlying representation once we've pushed one live.

We have been looking into alternatives and it looks like content-kit-editor is the best solution for us. It is open-source, actively developed, and I like that it's been designed so that it's easily extendible with custom "cards". The Mobiledoc format looks clean and well-thought out.

My question now is: what is the long-term vision of the project? I understand that there's a company behind it, Bustle.

  • Is it going to remain open-source?
  • Will it be maintained, I would assume by people from Bustle (at least initially)?

Naturally, if HSTRY chooses to integrate content-kit-editor, we will commit ourselves to push any improvements that we make back upstream.

I'm sorry for the fact that this is not a proper "issue" but it seemed to me like the best place to ask this question.

Thank you,

Yoran

Forward delete behaves the same as backspace

Pressing the delete key on a full keyboard or fn + backspace on a mac keyboard results in the character before being deleted instead of the character afterward.

This behaved as I would expect, deleting forwards in 0.2.1, and changed in 0.2.2

How I reproduced:

  • on master (58ffbc8) using broccoli serve to test the demo
  • place the cursor between h and 2 on the first line and press delete
  • the character h is removed, instead of 2

Initializing with no mobiledoc dies after entering 2 characters

I've initialized the editor without an initial mobiledoc as follows:

let editor = new ContentKit.Editor(element, {});

All looks fine on the first keypress, but as soon as the second one is entered I get:

screenshot 2015-08-11 14 59 14

The element here appears to be the document itself, so it looks like it's walking up out of the editor and not stopping until it hits the root.

It seems to be that collectMarkups is being called with the textNode and rootNode being the same and then immediately calls currentNode = textNode.parentNode and so has now escaped the editor.

Should this check if textNode === rootNode first, or is the problem higher up?

first item in `model` serialization is `null` when 2nd item is a list

To reproduce this, delete all contents from the CK editor pane, then create a list with an item. Use arrow keys to move to the first item, hit enter, then up, then delete so that the cursor is now on a line above the list. The model serialization will display null for this block.

image

cc

selecting using keyboard up arrow generates exception

The Cursor model currently expects the focus/anchor node in the selection to always be a text node, which it uses to lookup the associated Marker.

Using the shift key plus arrow keys can cause the browser to report selected anchorNodes or focusNodes that are not text nodes, which will cause an exception in the Cursor#offsets code.

On Chrome and Safari, assuming two sections, A and B:

  • Place cursor at start of section B, hold shift, press ⬆️
    • left node is a text node in the previous section A (correct)
    • right node is the paragraph tag for section B (incorrect) — should be the last text node in section A
  • Place cursor at start of section A, hold shift, press ⬇️
    • left node is the first text node in section A (correct)
    • right node is the paragraph tag for section B (incorrect) — should be the last text node in section A

Firefox has a different issue — it also reports selected text nodes, but sometimes it reports the text node in the prev/next section instead of the correct one.

typing in link causes console error

On Mac, Chrome:
Select some text, use the toolbar to create a link, and then type text in the link. Causes console error: "Cannot read property 'parentNode' of undefined".

This appears to be due to the fact that, although we reparse after the link command calls document.execCommand('createLink'), we don't rerender the new markers. They end up in the Abstract Tree but their rendered elements are not in the element map, so the next time we type -> reparse -> rerender, we cannot tear down those markers.

Selecting across MarkupSection and ListItem fails

Given this text in the editor:

abcdef

  • 123456

Attempting a selection from the markup section ("abcdef") into the numbers (list item section) fails because some of the marker-walking code does not properly descend from a ListSection into a ListItem

selecting text before and after a card and deleting does not delete the card

If the editor looks like this:

some text in a markup section
[ a card is here ]
markup section number two

And the selecting starts at 'text" and ends at "number", hitting delete will only delete the selected text in the first markup section, leaving:

some
[ a card is here ]
markup section number two

This is because we don't walk non-markerable sections

see #107

bold button incorrectly reflects state

Sometimes, when the text that is highlighted is touching bold text, the button incorrectly reflects that the highlighted text is also bold.

To reproduce:

  • with text "hello world" in a section,
  • select "w" and make it bold
  • select the "o" after the "w"
  • the bold button will be shown as active (blue)

add API to insert a section

There is already a postEditor API of insertSectionBefore, but it leaks some internals (the linked-list structure of the post's sections, e.g.) that make it cumbersome to use. Add an insertSection API that will insert the section at the correct spot.

Events on card elements should not intercepted by the editor

When editing a card within its element (say, typing in an injected textarea or input) the editor’s behavior intercepts and prevents events like backspacing and pasting. The editor’s callbacks don’t need to fire while using in-card UI, and DOM events within the card should not be prevented/intercepted (maybe no DOM events should bubble out of the card element by default?).

Editor hooks

Per the 0.4.0 epic, create public hooks for the lifecycle of abstract changes and rendering.

editor.didUpdatePost((postEditor) => {
  postEditor.doSomething(editor.builder.createMarker());
  postEditor.forEachNode((node) => {
    if (node.type === SECTION) {
      if (node.markers.length === 1 && node.value === '*') {
        let list = editor.builder.createListSection('li');
        postEditor.replaceSection(node.section, list);
      }
    }
  });
});

editor.willRender(() => {
  if (!editor.post.isEmpty) {
    $('.placeholder').remove();
  }
});

editor.didRender(() => {
  if (editor.post.isEmpty) {
    $('.placeholder').append(document.body);
    $('.placeholder').positionAt(editor.element.firstChild);
    $('.placeholder').click(() => {
      editor.cursor.moveToSection(editor.post.sections.head);
    });
  }
});

Card#remove lifecycle hook

There needs to be a way to have a card remove itself from the Post abstract. This should be exposed as a hook that the card can call that will tear it down, remove it from the abstract, and remove its rendered element from DOM.

#remove seems like an appropriate name for this.

cc @dtetto

Applying markups to overlapping markers creates inaccurate AT

To recreate:

  • select two characters, apply "I" markup (via toolbar button)
  • select the second of the two characters and the (plain) character to its right, apply "B" markup (via toolbar)

Expected output: 1 italic, 1 bold+italic, and 1 bold character.
Actual output: 1 italic, 1 bold+italic, 1 italic character (should be bold and not italic)

Handle copy/pasting properly

When pasting text into an editor, we should try to add the pasted content to the existing post abstract, matching as closely as possible.

We can also attempt to serialize sections of mobiledoc when copying by overriding the copy event and either setting a custom mime type like "text/mobiledoc" (works on Safari and Chrome) or hijacking the "text/html" mime type (works on Firefox), and prioritize using that if it is found. This makes pasting easier/reliable when working within the same document (which seems like a common use case), and allows cross-window and cross-browser copy/pasting of mobiledoc.

Here's an example codepen showing overridden copied values in action — http://codepen.io/bantic/pen/dYPWea?editors=101

Placeholder is ignored, editor renders `__BLANK__` when initialised with no mobiledoc

Initialising a new editor with no Mobiledoc option results in the editor showing __BLANK__:

How I reproduced:

  • on master (58ffbc8) using broccoli serve to test the demo
  • remove line 264 from demo/demo.js (or replace with something like placeholder: 'my custom placeholder')
  • visit http://localhost:4200/demo/ and note the __BLANK__ marker shown in the editor rather than a placeholder.

This was introduced in 0.2.2, in 0.2.1 the placeholder appears.

possible to delete a disconnected section below a list item

to reproduce:

  • create a list with two non-empty items
  • add a non-empty markup section below the list
  • add a third empty list item, position the cursor in it
  • select its line by pressing shift-
  • hit backspace
  • result: the unselected markup section below the list section is also removed

leading space in section is eaten by browser

Same issue as #68 but in reverse. If a textNode begins with a leading space when we position the cursor at the start of it, the space gets eaten by the browser.

Example codepen.

This can be recreated making a selection that includes the start of a section up until right before a space character, and hitting <delete>. The start of the section will not show the space, and it won't be there if you type a new character.

Using a no-break space (same fix for #68) fixes this issue as well.

removeMarkupFromRange fails when range includes empty section

removeMarkupFromRange expects to always have a head and tail marker, but that's not true when the final section is blank.

To reproduce:

  • create an empty section below a section with text in it
  • position cursor in the empty section and use shift + to select the line above
  • hit cmd-B to bolden text
  • result: Uncaught TypeError: Cannot read property 'renderNode' of undefined

0.4.0 Epic

  • Pass buttons to content-kit instances for the toolbar
  • Ship programmatic postEditor API (Add/document API on the editor instance that you can replace the current section with a card and payload) #73
  • Mobiledoc could store meta data from the editor, ala "type". For example "listicle" or "listicle-style-one". this means an API on the editor. postEditor.setMetadata(newData) and editor.post.metadata to read punting in favor of encouraging this kind of thing be stored on a proper model.
  • Possibly a validation API, but more likely this can just be a walker API that hits the post or a completed mobiledoc
  • Three levels of interaction. #82
    • Text can be edited and sections created.
    • Sections cannot be created/deleted, but text could be edited.
    • ~~Sections cannot be created/deleted, text cannot be edited ~~
  • Disable the cursor on an editor (disable contenteditable basically) #82
  • Test using two content-kit editors on the same page
  • lists as text (making lists starting with a *) #86 #87
  • styles should be removable or possibly just not present.
  • Placeholder

Paste images from clipboard

I see in the demo that there is provision for uploading images (though it errors out right now). Any plans to allow pasting images directly from the clipboard, like on Imgur, Slack, Google Docs, GitHub etc?

I'm comparing WYSIWYG editors and have been looking for this feature.

handle keystrokes when there is a selection

moved from #46:

handle keystrokes when there is a selection (contenteditable almost does this for us, but it inserts irrelevant spans to try to maintain visual styling — this should be handled semantically as well)

Pressing enter at the start of a line/section has unexpected results

Placing the cursor at the start of a line and pressing enter causes the line to move down in 0.2.1. In 0.2.2 it at first appears to do nothing.

If, after having pressed enter at the beginning of a line, you move the cursor to somewhere else on the same line and also press enter, the content of the line is duplicated before the new line is added.

How I reproduced:

  • on master (58ffbc8) using broccoli serve to test the demo

Test 1:

  • Place the cursor at the beginning of the first line, press enter
  • nothing happens
  • Place the cursor anywhere except the beginning of the second line, press enter
  • the content of the line is duplicated and a new line is added

Test 2:

  • Place the cursor at the beginning of the first line, press enter
  • nothing happens
  • Place the cursor anywhere except the beginning of the second line, press enter
  • a new line is entered and no content is duplicated

Test 3:

  • Place the cursor at the beginning of the first line, press enter multiple times
  • Place the cursor at the beginning of the second line, press enter multiple times
  • Place the cursor anywhere except the beginning of a line, press enter
  • Content is duplicated on both lines!

selecting empty list item causes error

To reproduce:

  • create three list items, where the 1st and 3rd have text and the 2nd is blank
  • put cursor in the 2nd item, use shift+ to select the list item below
  • console reports "Could not find parent section from element node"

This happens because the browser will report (via window.getSelection()) that the focus node is the 3rd <li> itself, rather than the inner textNode, which is what Cursor#fromNode was expecting.

Contributing?

Hello! We're looking at using a library for content editing in our app, and seriously considering content-kit... but it isn't really packaged for consumption as a lib (specifically, we would want bower/es6).

What is your intention with the future growth of content-kit / this repository? Should we just fork the repo, remove the node/aws and maintain it ourselves? I've got approximately 1 man month to spend on my project, the main features not currently supported being word/phrase highlighting/tagging, diff-ing versions, MathML, tables, and full test coverage.

From what I can tell, a large chunk of these will be commands (and additions to the compiler), but some (like highlighting / tagging, which require searching for text) may require larger changes. Full Test Coverage will also likely require changes.

I'm perfectly comfortable forking and moving forward on this on my own if these things don't align with your roadmap/goals... but if possible, I'd like to consolidate effort.

Hit enter to put a section after a card

When a card is the final section in a mobiledoc it is difficult to add a section below it (or remove the card) without a target area below for the cursor to go into.

implement lists

A list section is different from other markup sections because it has sub-elements that are not markers.

A normal markup section has (a linked-list of) markers:
markupSection -> markers

But a list section has list items which have markers:
listSection -> listItems -> markers

To accommodate lists, the Abstract Tree (AT) will need to be able to traverse the leaf nodes (markers) of the tree — for semantic actions like splitting/joining sections and markers — without assuming that the max depth of children is always going to be 1.

  • editor-dom renderer can render list sections and list items
  • mobiledoc parser can parse list sections and list items
  • mobiledoc dom renderer can render list sections and items
  • mobiledoc html renderer can render list sections and items
  • Post parser must be able to reparse list sections
  • Actions on the post editor like splitMarkers must be able to handle markers that have a depth > 1

Creating a list will happen the same way that other section types are created: Click a toolbar button. Eventually (#87) also via text-expansion.
Exiting a list happens by hitting enter when the cursor is in the last item in a list and it is empty (this is how google docs works, too).

For now, only singly-nested lists (this is how medium does it).

Select & backspace removes only the single character preceding the selection start

Using select + backspace has stopped working between 0.2.1 and 0.2.2

How I reproduced:

  • on master (58ffbc8) using broccoli serve to test the demo

Test 1:

  • select the second line and press backspace
  • the newline between the first and second line is deleted, rather than the selected text

Test 2:

  • select both lines and press backspace
  • nothing is removed, the cursor moves to the beginning of the first line

Test 3:

  • select a few characters in the middle of a word and press backspace
  • the single character preceding the start of the selection is deleted

Placing the cursor between two characters and pressing backspace works as expected, deleting the single character preceding the cursor.

trailing space in section is eaten by browser when deleting

In Chrome and Safari, when the cursor is at a character with a space behind it, after hitting delete once the cursor skips backward across the space:

  • text is "abc d", cursor is after the d.
  • Hit delete and the cursor is now after the "c" instead of after the space. The DOM still has the space but you can't use the arrow keys to move the cursor past it.

Firefox leaves the cursor after the space, as expected.

Adding headers does not generate update event

Changing a paragraph into an h2/h3 does not generate an update event on the editor object, causing changes to be missed. The user must hit return to add a paragraph then delete the paragraph to get the changes to be picked up.

0.3.0 Epic

  • Implement a serializer for the post AT to a serialized format
  • Package for distribution a DOM renderer (https://github.com/bustlelabs/mobiledoc-dom-renderer)
  • Package for distribution a HTML renderer (https://github.com/bustlelabs/mobiledoc-html-renderer)
  • Handle section modification, removal, addition en-masse (edits across sections)
  • Don't create a section element during post render for editor
  • Pass serialized payload to the editor for use (06def74)
  • An inserted <span> or other ignorable element in the editor's HTML needs to be ignored properly so that you can delete to join two sections
  • write serializer tests
  • write acceptance tests against serializer output
  • parse the serialized format into the AT for the editor ( https://github.com/bustlelabs/content-kit-compiler/commit/0db07a6058e9af19657808b5f2d6fb9f8dd047aa )
  • Editor#destroy tears down listeners, cleans up after itself
  • Assert correct command and UI behaviors of markup commands: (#17)
    • bold
    • italic
    • link
  • Assert correct command an UI behaviors for sections (these are all new section types)
    • image (port image implementation to a card)
    • embed (including embeds that hit the embed API for HTML) (port embed implementation to a card)
    • display of available cards in ContentKit punting this to the UI-focused work in 0.4.0
    • quote
    • headline
    • subheadline
  • remove remaining memory-leaked event listeners (Tooltip adds a mouseover listener, e.g., and there are a few others) (#18)
  • DOM renderer should know how to render a card
  • HTML renderer should know how to render a card (should use a new bag of hooks called html)

Stretch

  • two-pass parse that avoids the previous-section and isGenerated nonsense
  • make post AT nodes a linked list
  • parse/rerender markers instead of sections
  • serializer should serialize null values as a blank text node
  • IE 10 tests pass and demo works (blocked on lack of input event on contenteditable)

`marker.hasMarkup` does not detect markup properly for bold/italic buttons

As of 6d4983d:

  • select text, click "B"
  • type additional text inside the bold text
  • select the bolded text and notice that "B" is not active (blue). It should be active.

This is because when text is input the post parser reparses that section and we end up re-creating the marker w/ the bold markup in the post parser's markupFromNode method. This skips the builder's marker cache because of two bugs there:

  • attributes is present but empty ([]) — this line should check whether it is empty, not just absent
  • newly-created markers without attributes are (mistakenly) not getting cached

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.