Giter Club home page Giter Club logo

ed's Introduction

npm start

ed

Build Status

Using ProseMirror with data from the Grid API

Demo: the-grid.github.io/ed/, with fixture

The demo shows translating from ProseMirror to the the Grid API JSON and back.

purpose

ProseMirror provides a high-level schema-based interface for interacting with contenteditable, taking care of that pain. Ed is focused on:

  • Schema to translate between the Grid API data and ProseMirror doc type
  • Coordinating widgets (block editors specialized by type) (example)

use

Using as a React ⚛ component

Ed exposes a React component by default.

import Ed from '@the-grid/ed'

export default class PostEditor extends React.Component {
  render() {
    return (
      <Ed key='item-uuid' initialContent={...} onChange={...} ... />
    )
  }
}

Using as a stand-alone library in iframe or similar

Including dist/build.js in your page exposes window.TheGridEd

<script src='dist/build.js'></script>

There are {mountApp, unmountApp} helper methods available to use like this:

  var container = document.querySelector('#ed')
  window.TheGridEd.mountApp(container, {
    // REQUIRED -- Content array from post
    initialContent: [],
    // OPTIONAL (default true) enable or disable the default menu
    menuBar: true,
    // REQUIRED -- Hit on every change
    onChange: function () {
      /* App can show "unsaved changes" in UI */
    },
    // REQUIRED
    onShareFile: function (index) {
      /* App triggers native file picker */
      /* App calls ed.insertPlaceholders(index, count) and gets array of ids back */
      /* App uploads files and sets status on placeholder blocks with ed.updateProgress */
      /* On upload / measurement finishing, app replaces placeholder blocks with ed.setContent */
    },
    // REQUIRED
    onRequestCoverUpload: function (id) {
      /* Similar to onShareFile, but hit with block id instead of index */
      /* App uploads files and sets status on blocks with ed.updateProgress */
      /* Once upload is complete, app hits ed.setCoverSrc */
    },
    // REQUIRED
    onShareUrl: function ({block, url}) {
      /* Ed made the placeholder with block id */
      /* App shares url with given block id */
      /* App updates status on placeholder blocks with ed.updateProgress */
      /* On share / measurement finishing, app replaces placeholder blocks with ed.setContent */
    },
    // REQUIRED
    onPlaceholderCancel: function (id) {
      /* Ed removed the placeholder if you call ed.getContent() now */
      /* App should cancel the share or upload */
    },
    // OPTIONAL
    onRequestLink: function (value) {
      /*
        If defined, Ed will _not_ show prompt for link
        If selection is url-like, value will be the selected string
        App can then call `ed.execCommand('link:toggle', {href, title})`
          Note: If that is called while command 'link:toggle' is 'active', it will remove the link, not replace it
      */
    },
    // OPTIONAL
    onDropFiles: function (index, files) {
      /* App calls ed.insertPlaceholders(index, files.length) and gets array of ids back */
      /* App uploads files and sets status on placeholder blocks with ed.updateProgress */
      /* On upload / measurement finishing, app replaces placeholder blocks with ed.setContent */
    },
    // OPTIONAL
    onDropFileOnBlock: function (id, file) {
      /* App uploads files and sets status on block with ed.updateProgress */
      /* Once upload is complete, app hits ed.setCoverSrc */
    },
    // OPTIONAL
    onMount: function (mounted) {
      /* Called once PM and widgets are mounted */
      window.ed = mounted
    },
    // OPTIONAL
    onCommandsChanged: function (commands) {
      /* Object with commandName keys and one of inactive, active, disabled */
    },
    // OPTIONAL -- imgflo image proxy config
    imgfloConfig: {
      server: 'https://imgflo.herokuapp.com/',
      key: 'key',
      secret: 'secret'
    },
    // OPTIONAL -- where iframe widgets live relative to app (or absolute)
    widgetPath: './node_modules/',
    // OPTIONAL -- site-wide settings to allow cover filter, crop, overlay; default true
    coverPrefs: {
      filter: false,
      crop: true,
      overlay: true
    },
    // OPTIONAL -- site or user flags to reduce functionality
    featureFlags: {
      edCta: false,
      edEmbed: false
    }
  })
  
  // Returns array of inserted placeholder ids
  ed.insertPlaceholders(index, count)
  
  // Update placeholder metadata
  // {status (string), progress (number 0-100), failed (boolean)}
  // metadata argument with {progress: null} will remove the progress bar
  ed.updateProgress(id, metadata)
  
  // Once block cover upload completes
  // `cover` is object with {src, width, height}
  ed.setCover(id, cover)

  // For placeholder or media block with uploading cover
  // `src` should be blob: or data: url of a
  // sized preview of the local image
  ed.setCoverPreview(id, src)

  // Returns content array
  // Expensive, so best to debounce and not call this on every change
  // Above the fold block is index 0, and starred
  ed.getContent()
  
  // Only inserts/updates placeholder blocks and converts placeholder blocks to media
  ed.setContent(contentArray)
  
  // Returns true if command applies successfully with current selection
  ed.execCommand(commandName)

Demo: ./demo/demo.js

commands

With onCommandsChanged prop, app will get an object containing these commandName keys. Values will be one of these strings: inactive, active, disabled, flagged.

Apps can apply formatting / editing commands with ed.execCommand(commandName)

Special case: ed.execCommand('link:toggle', {href, title}) (title optional) to set link of current selection.

Supported commandName keys:

strong:toggle
em:toggle
link:toggle
paragraph:make
heading:make1
heading:make2
heading:make3
bullet_list:wrap
ordered_list:wrap
horizontal_rule:insert
lift
undo
redo
ed_upload_image
ed_add_code
ed_add_location
ed_add_userhtml
ed_add_cta
ed_add_quote

dev

server

npm start and open http://localhost:8080/

In development mode, webpack builds and serves the targets in memory from /webpack/

Changes will trigger a browser refresh.

plugins

Plugins are ES2015 classes with 2 required methods:

  • constructor (ed) {} gets a reference to the main ed, where you can
    • listen to PM events: ed.pm.on('draw', ...)
    • and set up UI: ed.pluginContainer.appendChild(...)
  • teardown () {} where all listeners and UI should be removed

widgets

Widgets are mini-editors built to edit specific media types

iframe

Run in iframe and communicate via postMessage

Example: ced - widget for code editing

native

Example: WIP

styling

  1. Primary: Rebass defaults and rebass-theme for global overrides
  2. Secondary: inlined JS style objects (example)
  3. Deprecating: require('./component-name.css') style includes, but needed for some responsive hacks and ProseMirror overrides

code style

Feross standard checked by ESLint with npm test or npm run lint

  • no unneeded semicolons
  • no trailing spaces
  • single quotes

To automatically fix easy stuff like trailing whitespace: npm run lintfix

test

npm test

Karma is set up to run tests in local Chrome and Firefox.

Tests will also run in mobile platforms via BrowserStack, if you have these environment variables set up:

BROWSERSTACK_USERNAME
BROWSERSTACK_ACCESSKEY

build

npm run build

Outputs minified dist/ed.js and copies widgets defined in package.json.

deploying

npm version patch - style tweaks, hot bug fixes

npm version minor - adding features, backwards-compatible changes

npm version major - removing features, non-backwards-compatible changes

These shortcuts will run tests, tag, change package version, and push changes and tags to GH.

Travis will then publish new tags to npm and build the demo to publish to gh-pages.

ed's People

Contributors

d4tocchini avatar forresto avatar greenkeeperio-bot avatar marsch avatar narrowdesign avatar netlemur avatar nickvelloff 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

Watchers

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

ed's Issues

Detect pasted URLs

When user adds a paragraph that only contains a URL, we should make a /share API call on that.

Possible inconsistency when deleting placeholder and closing Ed

Given the following scenario:

  1. User pastes URL, initiates share
  2. User deletes placeholder block
  3. App PUTs item without placeholder
  4. Share job finishes
  5. App fetches item, does ed.setContent

API might append the share block to the end of the content array if it doesn't find the placeholder. But Ed would just ignore that block, as the merging strategy only replaces placeholders.

As long as the App does a final PUT without placeholder or shared block, things are okay though.

setContent with article throws error

Using ed 0.0.35 and this flow:

  1. Hit enter to create new block
  2. Paste URL, hit enter
  3. App gets url share event, creates placeholder block, does setContent (placeholder is displayed correctly)
  4. App does PUT /item/<id> to save placeholder block to API
  5. App does POST /share into the placeholder block
  6. As soon as job-completed GCM comes in, app reloads item and does setContent

This is the item (I do ed.setContent(item.content)): https://gist.github.com/hannesstruss/819ab983c52cca26251c

This is the error (the index in Failed to resolve path %d is the index of the block about to be replaced +1, when counting 0-based):
developer_tools_-_file____android_asset_posteditor_editor_html

Not sure how related they are and can't reproduce, but I also encountered these errors (might have happened during just editing):
developer_tools_-_file____android_asset_posteditor_editor_html

developer_tools_-_file____android_asset_posteditor_editor_html

(Sorry for screenshots, but I can't seem to copy/paste stacktraces from Chrome devtools)

Should we remove excess empty blocks after tapping in the blank space?

I like that you can tap in the blank space now to create a new block!

There's a slight inconvenience on Android though. When long pressing on an empty block that is not the last empty block, the in-place context menu for pasting doesn't pop up. Instead, an action mode overlay shows from which people can paste.

Definitely an Android bug, but we might be able to work around that by not leaving empty blocks at the end of the content array. What do you think?

db1ap5h3i2

Separation of concerns: Placeholder creation spread across App and Ed

Currently, when sharing URLs, Ed creates the placeholder. For sharing images, there is ed.insertBlocks(index, blocks), which requires the app to create the placeholders itself.

I think for consistency it should only be one component's responsibility to actually construct placeholder blocks. Currently, the knowledge about placeholder construction, the placeholder block's internal structure (metadata.status etc.) is spread across app and Ed. Since Ed has to interpret placeholder blocks anyway, it should be the single source of construction as well. I'd encapsulate that behind a more narrowly scoped API like (a less verbose version of) ed.createPlaceholdersForImages(originalPlaceholderBlockId, numberOfPlaceholders) which could return an array of placeholder block IDs to share into.

Let's discuss that in the meeting suggested in #editor!

cc @nickvelloff

Mobile-friendly toolbar option

Right now the popover toolbar clashes with the native selection bars on mobile platforms. It would be better to use a position: fixed toolbar on the bottom of the screen for those.

the fold

We need to build "the fold" into Ed.

  • [title text field, if exists]
  • [instance of ProseMirror with schema limited to p, list, quote, inline formatting]
  • [buttons to add single title (plain text), single (for now) image OR url share]
  • [hr equivalent visually denotes fold]
  • [full-schema ProseMirror]

Ed should have initial option and method to toggle collapsed (only show above the fold) state.

Initial content should infer fold from starred blocks.

ed.getContent should build content array with blocks "above the fold" starred.


This makes Ed not-insignificantly more complex, but has advantage of working with current API.

typing while setContent hit

Uncaught TypeError: Cannot read property 'value' of undefined
Uncaught TypeError: Cannot read property 'node' of undefined
Uncaught Error: Position 4:1 is not valid in current document

Something gets thrown off 💀 if typing while setContent is hit (like new share progress in demo).

Text block reverts to old value after fetching an updated item (gists included)

  1. Initial load of item data: https://gist.github.com/nickvelloff/6e8bc69ba717a3c36939
  2. visual state of content:
    img_5617
  3. Edit some textual content, type "The name is mr ed"
  4. PUT succeeds: https://gist.github.com/nickvelloff/d78cd3a558ef36821f4d
  5. visual state of content:
    img_5618
  6. Receive push notification that the item update is complete
  7. GET the updated item: https://gist.github.com/nickvelloff/e59162feed3fe82ab541
  8. "window.ed.setContent(\(contentString)); the new item data
  9. visual state of content (text reverts to old value):
    img_5619

set up travis to build and deploy to npm

  • use css-loader to inline styles
  • setup build to make standalone Ed
  • setup build to copy widgets #12
  • make minimal mobile.html
  • setup travis to deploy build to npm

& keep deploying the demo to gh-pages

``` inputRule → Media code block

  • Move plugins/code-embedder input rule to widget-code
  • replace PM's ``` input rule
  • make it inject a media div with grid-type="code", widget sys will do rest

build and copy editing widgets

Needed for gh-pages preview and packaged versions for Grid apps to consume. Hopefully easy with a dash of webpack magic.

Handling share failures

We need a way to handle failures when sharing. Currently, sharing works like this:

  1. User triggers share
  2. Ed creates placeholder, notifies app with {blockId, url} (in case of URL sharing)
    • Update: When sharing images, App creates placeholders and does ed.setContent
  3. App does ed.getContent(), PUT /item/<id> and POST /share
  4. When share is done (App receives GCM/APN), App does GET /item/<id>, then ed.setContent(blocks)

Possible fail cases (ignoring programmer errors):

  1. When sharing images: User closes image picker without picking images -- not an issue b/c App inserts placeholders after files are selected - fo
  2. PUT /item/<id> fails
  3. POST /share fails
    1. Special case when sharing images: only some POST /share calls succeed
  4. POST /share succeeds, but the app isn't notified, or the share job takes a long time api-side
  5. GET /item/<id> fails

Questions:

  1. We need UX for failures. I think V1 shouldn't include retrying. I'd go for something like "make placeholder block red, add X button to remove the failed block"
  2. We need an API in Ed, so App can notify Ed of failures. Since Ed already creates the placeholders itself, I think it should also modify/remove them. Off the top of my head: ed.shareFailed(placeholderBlockId).
  3. Should we timeout when the Share job doesn't complete within X minutes? It could result in false positives, where the user thinks the share failed, does something else/closes the editor, and the shared content eventually ends up in the item unexpectedly.
  4. It's probably not a big problem if some but not all image shares fail. Some placeholders become red, some get replaced with images.
  5. When sharing images, Ideally we only create the placeholder blocks after the user has already selected n images, so we don't have to remove them again.

cc @forresto @nickvelloff @JohnRandom

onShareUrl failing to fire

  1. If I add a url, I see this:
  2. screenshot 2016-02-23 22 45 22
  3. onShareUrl fires properly
  4. Then I delete that url.
  5. Then I repeat.
  6. onShareUrl does not fire subsequently (and there are no js errors).
  7. 😨

Additionally:
If an placeholder block exists when the item is loaded, the it will fail after deleting it as well.

Construct with configuration

Can we construct the editor object with a configuration?
3 come to mind:

  1. Do or don't use toolbar
  2. Auto save enabled
  3. Auto save frequency

Should onShareUrl be triggered multiple times?

Steps to reproduce:

  1. Hit enter to create new block
  2. Paste URL, hit enter
  3. onShareUrl callback is called (we don't do anything with it for now)
  4. Move caret to end of pasted URL
  5. Hit enter
  6. onShareUrl is called again
  7. GOTO 4

I'm not sure this is a real-world-problem. But what if I want a block that just contains a plain-text url without sharing it?

nested blockquotes

PM makes nested blockquotes when pressing ( “ ) multiple times. We need to either

  1. make the style work for that
  2. change button behavior to toggle & disallow nested bq

ed-bq-nested

path for arbitrary search widgets (read: giphy)

Here's a path that could work without API changes.

  1. type /gif on a new line in the editor
  2. prosemirror injects a placeholder with grid-type="placeholder" grid-widget="gif" (this placeholder block could get saved to API without making anybody 😢)
  3. widget plugin makes overlay with interface to search eg GiphyAPI
  4. widget builds a block type image (or video) that API can then do normal Caliper stuff with
  5. widget replaces self with this block

skip jsx?

I started AttributionEditor with it, but I am not terribly attached to xml.

using pros:

  • react documentation
  • react potential api changes sometimes swallowed by jsx
  • I think you have to export default React.createFactory(X) to use X without jsx

cons:

  • subjectively, it makes code look weird
  • 1 code style / linting setup to keep in mind

  • I usually end up doing const props = { /* js stuff here */ } then <div {...props} ></div> ... could just be el('div', props)
  • new things in ES6 > XMLish things mixed in

dive into tooltipMenu

  • option for menu bar for mobile with native tooltip
  • insert image on newline - trigger file event for grid-chrome
  • block controls also show up in inline selection (default is block controls only show on newlines and esc block selection)
  • media details event to trigger modal

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.