yabwe / medium-editor

Medium.com WYSIWYG editor clone. Uses contenteditable API to implement a rich text solution.
https://yabwe.github.io/medium-editor/
Other
16.03k stars 1.85k forks source link
contenteditable editor javascript medium-editor rich-text-editor wysiwyg

medium-editor needs help!

If you would be interested in helping to maintain one of the most successful WYSIWYG text editors on github, let us know! (See issue #1503)

MediumEditor

This is a clone of medium.com inline editor toolbar.

MediumEditor has been written using vanilla JavaScript, no additional frameworks required.

screenshot

Join the chat at https://gitter.im/yabwe/medium-editor

Browser Support

Saucelabs Build Status

Supported Browsers

NPM info

Travis build status Dependency Status devDependency Status Coverage Status

Basic usage

Demo

demo: http://yabwe.github.io/medium-editor/

Installation

Via npm:

Run in your console: npm install medium-editor

Via bower:

bower install medium-editor

Via an external CDN

Manual installation:

Download the latest release and attach medium editor's stylesheets to your page:

Find the files to below mentioned linking in the dist folder. (./medium-editor/dist/...)

<link rel="stylesheet" href="https://github.com/yabwe/medium-editor/blob/master/css/medium-editor.css"> <!-- Core -->
<link rel="stylesheet" href="https://github.com/yabwe/medium-editor/blob/master/css/themes/default.css"> <!-- or any other theme -->

Usage

The next step is to reference the editor's script

<script src="https://github.com/yabwe/medium-editor/raw/master/js/medium-editor.js"></script>

You can now instantiate a new MediumEditor object:

<script>var editor = new MediumEditor('.editable');</script>

The above code will transform all the elements with the .editable class into HTML5 editable contents and add the medium editor toolbar to them.

You can also pass a list of HTML elements:

var elements = document.querySelectorAll('.editable'),
    editor = new MediumEditor(elements);

MediumEditor also supports textarea. If you provide a textarea element, the script will create a new div with contentEditable=true, hide the textarea and link the textarea value to the div HTML content.

Integrating with various frameworks

People have contributed wrappers around MediumEditor for integrating with different frameworks and tech stacks. Take a look at the list of existing Wrappers and Integrations that have already been written for MediumEditor!

MediumEditor Options

View the MediumEditor Options documentation on all the various options for MediumEditor.

Options to customize medium-editor are passed as the second argument to the MediumEditor constructor. Example:

var editor = new MediumEditor('.editor', {
    // options go here
});

Core options

NOTE:

Using 'fontawesome' as the buttonLabels requires version 4.1.0 of the fontawesome css to be on the page to ensure all icons will be displayed correctly

Toolbar options

The toolbar for MediumEditor is implemented as a built-in extension which automatically displays whenever the user selects some text. The toolbar can hold any set of defined built-in buttons, but can also hold any custom buttons passed in as extensions.

Options for the toolbar are passed as an object that is a member of the outer options object. Example:

var editor = new MediumEditor('.editable', {
    toolbar: {
        /* These are the default options for the toolbar,
           if nothing is passed this is what is used */
        allowMultiParagraphSelection: true,
        buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],
        diffLeft: 0,
        diffTop: -10,
        firstButtonClass: 'medium-editor-button-first',
        lastButtonClass: 'medium-editor-button-last',
        relativeContainer: null,
        standardizeSelectionStart: false,
        static: false,
        /* options which only apply when static is true */
        align: 'center',
        sticky: false,
        updateOnEmptySelection: false
    }
});
Options which only apply when the static option is being used:

To disable the toolbar (which also disables the anchor-preview extension), set the value of the toolbar option to false:

var editor = new MediumEditor('.editable', {
    toolbar: false
});

Button Options

Button behavior can be modified by passing an object into the buttons array instead of a string. This allow for overriding some of the default behavior of buttons. The following options are some of the basic parts of buttons that you may override, but any part of the MediumEditor.Extension.prototype can be overridden via these button options. (Check out the source code for buttons to see what all can be overridden).

Example of overriding buttons (here, the goal is to mimic medium by having H1 and H2 buttons which actually produce <h2> and <h3> tags respectively):

var editor = new MediumEditor('.editable', {
    toolbar: {
        buttons: [
            'bold',
            'italic',
            {
                name: 'h1',
                action: 'append-h2',
                aria: 'header type 1',
                tagNames: ['h2'],
                contentDefault: '<b>H1</b>',
                classList: ['custom-class-h1'],
                attrs: {
                    'data-custom-attr': 'attr-value-h1'
                }
            },
            {
                name: 'h2',
                action: 'append-h3',
                aria: 'header type 2',
                tagNames: ['h3'],
                contentDefault: '<b>H2</b>',
                classList: ['custom-class-h2'],
                attrs: {
                    'data-custom-attr': 'attr-value-h2'
                }
            },
            'justifyCenter',
            'quote',
            'anchor'
        ]
    }
});

Anchor Preview options

The anchor preview is a built-in extension which automatically displays a 'tooltip' when the user is hovering over a link in the editor. The tooltip will display the href of the link, and when clicked, will open the anchor editing form in the toolbar.

Options for the anchor preview 'tooltip' are passed as an object that is a member of the outer options object. Example:

var editor = new MediumEditor('.editable', {
    anchorPreview: {
        /* These are the default options for anchor preview,
           if nothing is passed this is what it used */
        hideDelay: 500,
        previewValueSelector: 'a'
    }
}
});

To disable the anchor preview, set the value of the anchorPreview option to false:

var editor = new MediumEditor('.editable', {
    anchorPreview: false
});
NOTE:

Placeholder Options

The placeholder handler is a built-in extension which displays placeholder text when the editor is empty.

Options for placeholder are passed as an object that is a member of the outer options object. Example:

var editor = new MediumEditor('.editable', {
    placeholder: {
        /* This example includes the default options for placeholder,
           if nothing is passed this is what it used */
        text: 'Type your text',
        hideOnClick: true
    }
});

To disable the placeholder, set the value of the placeholder option to false:

var editor = new MediumEditor('.editable', {
    placeholder: false
});

Anchor Form options

The anchor form is a built-in button extension which allows the user to add/edit/remove links from within the editor. When 'anchor' is passed in as a button in the list of buttons, this extension will be enabled and can be triggered by clicking the corresponding button in the toolbar.

Options for the anchor form are passed as an object that is a member of the outer options object. Example:

var editor = new MediumEditor('.editable', {
    toolbar: {
        buttons: ['bold', 'italic', 'underline', 'anchor']
    },
    anchor: {
        /* These are the default options for anchor form,
           if nothing is passed this is what it used */
        customClassOption: null,
        customClassOptionText: 'Button',
        linkValidation: false,
        placeholderText: 'Paste or type a link',
        targetCheckbox: false,
        targetCheckboxText: 'Open in new window'
    }
}
});

Paste Options

The paste handler is a built-in extension which attempts to filter the content when the user pastes. How the paste handler filters is configurable via specific options.

Options for paste handling are passed as an object that is a member of the outer options object. Example:

var editor = new MediumEditor('.editable', {
    paste: {
        /* This example includes the default options for paste,
           if nothing is passed this is what it used */
        forcePlainText: true,
        cleanPastedHTML: false,
        cleanReplacements: [],
        cleanAttrs: ['class', 'style', 'dir'],
        cleanTags: ['meta'],
        unwrapTags: []
    }
});

KeyboardCommands Options

The keyboard commands handler is a built-in extension for mapping key-combinations to actions to execute in the editor.

Options for KeyboardCommands are passed as an object that is a member of the outer options object. Example:

var editor = new MediumEditor('.editable', {
    keyboardCommands: {
        /* This example includes the default options for keyboardCommands,
           if nothing is passed this is what it used */
        commands: [
            {
                command: 'bold',
                key: 'B',
                meta: true,
                shift: false,
                alt: false
            },
            {
                command: 'italic',
                key: 'I',
                meta: true,
                shift: false,
                alt: false
            },
            {
                command: 'underline',
                key: 'U',
                meta: true,
                shift: false,
                alt: false
            }
        ],
    }
});

To disable the keyboard commands, set the value of the keyboardCommands option to false:

var editor = new MediumEditor('.editable', {
    keyboardCommands: false
});

Auto Link Options

The auto-link handler is a built-in extension which automatically turns URLs entered into the text field into HTML anchor tags (similar to the functionality of Markdown). This feature is OFF by default.

To enable built-in auto-link support, set the value of the autoLink option to true:

var editor = new MediumEditor('.editable', {
    autoLink: true
});

Image Dragging Options

The image dragging handler is a built-in extension for handling dragging & dropping images into the contenteditable. This feature is ON by default.

To disable built-in image dragging, set the value of the imageDragging option to false:

var editor = new MediumEditor('.editable', {
    imageDragging: false
});

Disable File Dragging

To stop preventing drag & drop events and disable file dragging in general, provide a dummy ImageDragging extension.

var editor = new MediumEditor('.editor', {
    extensions: {
        'imageDragging': {}
    }
});

Due to the state of code in 5.0.0, the editor ALWAYS prevented any drag and drop actions. We will have a better way to disable file dragging in 6.*

Options Example:

var editor = new MediumEditor('.editable', {
    delay: 1000,
    targetBlank: true,
    toolbar: {
        buttons: ['bold', 'italic', 'quote'],
        diffLeft: 25,
        diffTop: 10,
    },
    anchor: {
        placeholderText: 'Type a link',
        customClassOption: 'btn',
        customClassOptionText: 'Create Button'
    },
    paste: {
        cleanPastedHTML: true,
        cleanAttrs: ['style', 'dir'],
        cleanTags: ['label', 'meta'],
        unwrapTags: ['sub', 'sup']
    },
    anchorPreview: {
        hideDelay: 300
    },
    placeholder: {
        text: 'Click to edit'
    }
});

Buttons

By default, MediumEditor supports buttons for most of the commands for document.execCommand() that are well-supported across all its supported browsers.

Default buttons.

MediumEditor, by default, will show only the buttons listed here to avoid a huge toolbar:

All buttons.

These are all the built-in buttons supported by MediumEditor.

Themes

Check out the Wiki page for a list of available themes: https://github.com/yabwe/medium-editor/wiki/Themes

API

View the MediumEditor Object API documentation on the Wiki for details on all the methods supported on the MediumEditor object.

Initialization methods

Event Methods

Selection Methods

Editor Action Methods

Helper Methods

Static Methods/Properties

Dynamically add/remove elements to your instance

It is possible to dynamically add new elements to your existing MediumEditor instance:

var editor = new MediumEditor('.editable');
editor.subscribe('editableInput', this._handleEditableInput.bind(this));

// imagine an ajax fetch/any other dynamic functionality which will add new '.editable' elements to the DOM

editor.addElements('.editable');
// OR editor.addElements(document.getElementsByClassName('editable'));
// OR editor.addElements(document.querySelectorAll('.editable'));

Passing an elements or array of elements to addElements(elements) will:

Removing elements dynamically

Straight forward, just call removeElements with the element or array of elements you to want to tear down. Each element itself will remain a contenteditable - it will just remove all event handlers and all references to it so you can safely remove it from DOM.

editor.removeElements(document.querySelector('#myElement'));
// OR editor.removeElements(document.getElementById('myElement'));
// OR editor.removeElements('#myElement');

// in case you have jQuery and don't exactly know when an element was removed, for example after routing state change
var removedElements = [];
editor.elements.forEach(function (element) {
    // check if the element is still available in current DOM
    if (!$(element).parents('body').length) {
        removedElements.push(element);
    }
});

editor.removeElements(removedElements);

Capturing DOM changes

For observing any changes on contentEditable, use the custom 'editableInput' event exposed via the subscribe() method:

var editor = new MediumEditor('.editable');
editor.subscribe('editableInput', function (event, editable) {
    // Do some work
});

This event is supported in all browsers supported by MediumEditor (including IE9+ and Edge)! To help with cases when one instance of MediumEditor is monitoring multiple elements, the 2nd argument passed to the event handler (editable in the example above) will be a reference to the contenteditable element that has actually changed.

This is handy when you need to capture any modifications to the contenteditable element including:

Why is this interesting and why should you use this event instead of just attaching to the input event on the contenteditable element?

So for most modern browsers (Chrome, Firefox, Safari, etc.), the input event works just fine. In fact, editableInput is just a proxy for the input event in those browsers. However, the input event is not supported for contenteditable elements in IE 9-11 and is mostly supported in Microsoft Edge, but not fully.

So, to properly support the editableInput event in Internet Explorer and Microsoft Edge, MediumEditor uses a combination of the selectionchange and keypress events, as well as monitoring calls to document.execCommand.

Extensions & Plugins

Check the documentation in order to learn how to develop extensions for MediumEditor.

A list of existing extensions and plugins, such as Images and Media embeds, Tables and Markdown can be found here.

Development

To run the demo locally:

  1. Clone this repo locally
  2. Run npm install from your console at the root
  3. Run node index.js from the root
  4. Navigate to http://localhost:8088/demo/index.html to view the demo

MediumEditor development tasks are managed by Grunt. To install all the necessary packages, just invoke:

npm install

To run all the test and build the dist files for testing on demo pages, just invoke:

grunt

These are the other available grunt tasks:

The source files are located inside the src directory. Be sure to make changes to these files and not files in the dist directory.

Contributing

Kill some bugs :)

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Test your changes to the best of your ability.
  4. Update the documentation to reflect your changes if they add or changes current functionality.
  5. Commit your changes (git commit -am 'Added some feature') without files from the dist directory.
  6. Push to the branch (git push origin my-new-feature)
  7. Create a new Pull Request

Code Consistency

To help create consistent looking code throughout the project, we use a few tools to help us. They have plugins for most popular editors/IDEs to make coding for our project, but you should use them in your project as well!

JSHint

We use JSHint on each build to find easy-to-catch errors and potential problems in our js. You can find our JSHint settings in the .jshintrc file in the root of the project.

jscs

We use jscs on each build to enforce some code style rules we have for our project. You can find our jscs settings in the .jscsrc file in the root of the project.

EditorConfig

We use EditorConfig to maintain consistent coding styles between various editors and IDEs. You can find our settings in the .editorconfig file in the root of the project.

Easy First Bugs

Looking for something simple for a first contribution? Try fixing an easy first bug!

Contributors (100+ and counting!)

https://github.com/yabwe/medium-editor/graphs/contributors

Is Your Org Using MediumEditor?

Add your org here and we can add you to our landing page!

License

MIT: https://github.com/yabwe/medium-editor/blob/master/LICENSE