Git Product home page Git Product logo

wagtaildraftail's Introduction

https://travis-ci.org/springload/wagtaildraftail.svg?branch=master https://coveralls.io/repos/github/springload/wagtaildraftail/badge.svg?branch=master

wagtaildraftail ๐Ÿฆ๐Ÿ“๐Ÿธ

Draft.js editor for Wagtail, built upon Draftail and draftjs_exporter.

This is alpha software, use at your own risk. Do not use in production (yet).

Check out Awesome Wagtail for more awesome packages and resources from the Wagtail community.

Installation

Grab the package from pip with pip install wagtaildraftail, then add wagtaildraftail as an app in your Django settings.

Note: this package contains compiled (bundled, minified) JavaScript and CSS, it can only be installed directly from pip.

Usage

There is a basic test site set up in the tests folder for reference.

With Pages

First, add a Draftail field to some of your pages. Here is an example:

from wagtaildraftail.fields import DraftailTextField

class MyPage(Page):
    body = DraftailTextField(blank=True)

    panels = [
        FieldPanel('body')
    ]

When using Wagtail 1.12 and above, DraftailTextField accepts the keyword argument features to define the set of features available in the editor - see Limiting features in a rich text field:

class MyPage(Page):
    body = DraftailTextField(blank=True, features=['h2', 'h3', 'ul', 'ol', 'link', 'document-link'])

Outputting the field directly onto a template will give the HTML representation; there is no need to use the richtext filter.

{% block content %}
    {{ page.body }}
{% endblock %}

With StreamField

Here is an example using the ready-made block:

from wagtaildraftail.blocks import DraftailTextBlock

class MyStructBlock(StructBlock):
    body = DraftailTextBlock()

The features argument is supported when using Wagtail 1.12 and above:

class MyStructBlock(StructBlock):
    body = DraftailTextBlock(features=['bold', 'italic', 'link', 'document-link'])

Configuration

Both DraftailTextField and DraftailTextBlock accept a string as keyword argument editor for a per field customisation.

Wagtail will look for a WAGTAILADMIN_RICH_TEXT_EDITORS constants in the settings, find the requested editor, load the defined widget and pass the options (if defined) to it.

Each editor defined in WAGTAILADMIN_RICH_TEXT_EDITORS is a dictionary with 2 keys:, WIDGET (mandatory) and OPTIONS (optional).

  • WIDGET is a mandatory string set to the widget to use - should always be set to wagtaildraftail.widgets.DraftailTextArea (or a subclass of it) to work with Draft.js content
  • OPTIONS is a dictionary which follows the format of Draftail configuration options. - Draftail options which are JavaScript values are hydrated at runtime in client/wagtaildraftail.js. Alternatively, on Wagtail 1.12 and above, a features list can be passed within the OPTIONS dict in place of a full Draftail configuration.

WARNING: The type key for blockTypes, inlineStyles and entityTypes shouldnโ€™t be changed. It is what defines how content is rendered, and is saved as a JSON blob in the database which would make migrations really painful.

WARNING: All the blocks/styles/entities defined in the editor config should have been configured to render properly in the exporter config.

Here is a sample configuration file. This should live in your Django settings.

For Wagtail 1.12.x and above:

from draftjs_exporter.constants import BLOCK_TYPES, ENTITY_TYPES
from draftjs_exporter.defaults import BLOCK_MAP

WAGTAILADMIN_RICH_TEXT_EDITORS = {
    'default_draftail': {
        'WIDGET': 'wagtaildraftail.widgets.DraftailTextArea',
    },

    'format_and_link': {
        'WIDGET': 'wagtaildraftail.widgets.DraftailTextArea',
        'OPTIONS': {
            'features': ['link', 'bold', 'italic'],
        }
    },

    # Wagtail dependencies
    'default': {
        'WIDGET': 'wagtail.wagtailadmin.rich_text.HalloRichTextArea'
    },

    'custom': {
        'WIDGET': 'wagtail.tests.testapp.rich_text.CustomRichTextArea'
    },
}

DRAFT_EXPORTER_ENTITY_DECORATORS = {
    ENTITY_TYPES.LINK: 'wagtaildraftail.decorators.Link',
    ENTITY_TYPES.DOCUMENT: 'wagtaildraftail.decorators.Document',
    ENTITY_TYPES.IMAGE: 'wagtaildraftail.decorators.Image',
    ENTITY_TYPES.EMBED: 'wagtaildraftail.decorators.Embed',
    ENTITY_TYPES.HORIZONTAL_RULE: 'wagtaildraftail.decorators.HR',
}

DRAFT_EXPORTER_COMPOSITE_DECORATORS = [
    'wagtaildraftail.decorators.BR',
]

DRAFT_EXPORTER_BLOCK_MAP = dict(BLOCK_MAP, **{
    BLOCK_TYPES.UNORDERED_LIST_ITEM: {
        'element': 'li',
        'wrapper': 'ul',
        'wrapper_props': {'class': 'list-styled'},
    },
    BLOCK_TYPES.ORDERED_LIST_ITEM: {
        'element': 'li',
        'wrapper': 'ol',
        'wrapper_props': {'class': 'list-numbered'},
    },
})

For Wagtail 1.11.x and below:

from draftjs_exporter.constants import BLOCK_TYPES, ENTITY_TYPES, INLINE_STYLES
from draftjs_exporter.defaults import BLOCK_MAP

TERMS_BLOCK_ID = 'TERMS_AND_CONDITIONS_TEXT'

DRAFT_BLOCK_TYPE_H3 = {'label': 'H3', 'type': BLOCK_TYPES.HEADER_THREE}
DRAFT_BLOCK_TYPE_H4 = {'label': 'H4', 'type': BLOCK_TYPES.HEADER_FOUR}
DRAFT_BLOCK_TYPE_UL = {'label': 'UL', 'type': BLOCK_TYPES.UNORDERED_LIST_ITEM, 'icon': 'icon-list-ul'}
DRAFT_BLOCK_TYPE_OL = {'label': 'OL', 'type': BLOCK_TYPES.ORDERED_LIST_ITEM, 'icon': 'icon-list-ol'}
DRAFT_BLOCK_TYPE_TERMS = {'label': 'T&Cs', 'type': TERMS_BLOCK_ID, 'element': 'div', 'class': 'legals'}

DRAFT_INLINE_STYLE_BOLD = {'label': 'Bold', 'type': INLINE_STYLES.BOLD, 'icon': 'icon-bold'}
DRAFT_INLINE_STYLE_ITALIC = {'label': 'Italic', 'type': INLINE_STYLES.ITALIC, 'icon': 'icon-italic'}

# It accepts a list of dicts with `label` and `value` keys (e.g. `{'label': 'Full width', 'value': 'fullwidth'}`)
# or a special `__all__` value which will be intercepted and will load all image formats known to Wagtail.
DRAFT_IMAGE_FORMATS = '__all__'

DRAFT_ENTITY_TYPE_IMAGE = {
    'label': 'Image',
    'type': ENTITY_TYPES.IMAGE,
    'icon': 'icon-image',
    'imageFormats': DRAFT_IMAGE_FORMATS,
    'source': 'ImageSource',
    'decorator': 'Image',
}
DRAFT_ENTITY_TYPE_EMBED = {
    'label': 'Embed',
    'type': ENTITY_TYPES.EMBED,
    'icon': 'icon-media',
    'source': 'EmbedSource',
    'decorator': 'Embed',
}
DRAFT_ENTITY_TYPE_LINK = {
    'label': 'Link',
    'type': ENTITY_TYPES.LINK,
    'icon': 'icon-link',
    'source': 'LinkSource',
    'decorator': 'Link',
}
DRAFT_ENTITY_TYPE_DOCUMENT = {
    'label': 'Document',
    'type': ENTITY_TYPES.DOCUMENT,
    'icon': 'icon-doc-full',
    'source': 'DocumentSource',
    'decorator': 'Document',
}

WAGTAILADMIN_RICH_TEXT_EDITORS = {
    'default_draftail': {
        'WIDGET': 'wagtaildraftail.widgets.DraftailTextArea',
        'OPTIONS': {
            'enableHorizontalRule': True,
            'enableLineBreak': False,
            'entityTypes': [
                DRAFT_ENTITY_TYPE_LINK,
                DRAFT_ENTITY_TYPE_DOCUMENT,
            ],
            'blockTypes': [
                DRAFT_BLOCK_TYPE_H3,
                DRAFT_BLOCK_TYPE_UL,
            ],
            'inlineStyles': [
                DRAFT_INLINE_STYLE_BOLD,
                DRAFT_INLINE_STYLE_ITALIC,
            ],
        }
    },

    'format_and_link': {
        'WIDGET': 'wagtaildraftail.widgets.DraftailTextArea',
        'OPTIONS': {
            'entityTypes': [
                DRAFT_ENTITY_TYPE_LINK,
            ],
            'inlineStyles': [
                DRAFT_INLINE_STYLE_BOLD,
                DRAFT_INLINE_STYLE_ITALIC,
            ],
        }
    },

    # Wagtail dependencies
    'default': {
        'WIDGET': 'wagtail.wagtailadmin.rich_text.HalloRichTextArea'
    },

    'custom': {
        'WIDGET': 'wagtail.tests.testapp.rich_text.CustomRichTextArea'
    },
}

DRAFT_EXPORTER_ENTITY_DECORATORS = {
    ENTITY_TYPES.LINK: 'wagtaildraftail.decorators.Link',
    ENTITY_TYPES.DOCUMENT: 'wagtaildraftail.decorators.Document',
    ENTITY_TYPES.IMAGE: 'wagtaildraftail.decorators.Image',
    ENTITY_TYPES.EMBED: 'wagtaildraftail.decorators.Embed',
    ENTITY_TYPES.HORIZONTAL_RULE: 'wagtaildraftail.decorators.HR',
}

DRAFT_EXPORTER_COMPOSITE_DECORATORS = [
    'wagtaildraftail.decorators.BR',
]

DRAFT_EXPORTER_BLOCK_MAP = dict(BLOCK_MAP, **{
    BLOCK_TYPES.UNORDERED_LIST_ITEM: {
        'element': 'li',
        'wrapper': 'ul',
        'wrapper_props': {'class': 'list-styled'},
    },
    BLOCK_TYPES.ORDERED_LIST_ITEM: {
        'element': 'li',
        'wrapper': 'ol',
        'wrapper_props': {'class': 'list-numbered'},
    },
    TERMS_BLOCK_ID: {
        'element': 'p',
        'props': {'class': 'legals'},
    },
})

Creating new content formats

TODO

Creating blocks and inline styles

TODO

Creating entities

An entity basically needs 4 elements:

  • a page decorator.
  • an editor decorator.
  • an editor source.
  • an editor strategy.

Decorators define how the content needs to be displayed on the site's pages, as well as within the editor.

  • For the pages, they are defined in Python with draftjs_exporter. Refer to the dedicated documentation on the draftjs_exporter README.
  • For the editor, they are defined in JS/React with draftail. Refer to the dedicated documentation on the Draftail README.

Sources define the interface (usually a modal) through which the user will select an entity to insert into the editor.

Strategies allow the editor to identify entities when it is loaded. Strategies are optional as the default one works fine in most cases.

Both sources and strategies are defined in JS/React with draftail. Refer to the dedicated documentation on the Draftail README.

To register decorators, sources or strategies to wagtaildraftail, use the corresponding register function:

window.wagtailDraftail.registerDecorators({ LinkDecorator, ButtonDecorator });
window.wagtailDraftail.registerSources({ LinkSource });
window.wagtailDraftail.registerStrategies({ LinkStrategy });

Note: In order for wagtailDraftail and its register functions to be available in the global window namespace, make sure that wagtaildraftail appears before any other app which will try to register an entity in INSTALED_APPS.

Development

Installation

Requirements: virtualenv, pyenv, twine
git clone [email protected]:springload/wagtaildraftail.git
cd wagtaildraftail/
virtualenv .venv
source ./.venv/bin/activate
make init
# Install all tested python versions
pyenv install 2.7.11 && pyenv install 3.3.6 && pyenv install 3.4.4 && pyenv install 3.5.1
pyenv global system 2.7.11 3.3.6 3.4.4 3.5.1

Commands

make help            # See what commands are available.
make init            # Install dependencies and initialise for development.
make start           # Starts the development server and compilation tools.
make lint            # Lint the project.
make load-data       # Prepares the database for usage.
make test            # Test the project.
make test-coverage   # Run the tests while generating test coverage data.
make test-ci         # Continuous integration test suite.
make clean-pyc       # Remove Python file artifacts.
make dist            # Compile the JS and CSS for release.
make publish         # Publishes a new version to pypi.

Debugging

To get up and running,

# Set up the development environment.
make init
# Start the development server.
make start
# If necessary, start the JS compilation watch
npm run start

There are testing and linting tasks available both in the Makefile (Python) and package.json (JS).

Updating test data

Here are useful commands:

# Create new migrations from changes to the project.
python tests/manage.py makemigrations
# "Reset" the database.
rm db.sqlite3
# Generate fixtures from DB data. Remember to clean them up so they do not overlap with data from migrations.
python tests/manage.py dumpdata > tests/fixtures/test_data.json

Releases

  • Make a new branch for the release of the new version.
  • Update the CHANGELOG.
  • Update the version number in wagtaildraftail/__init__.py and package.json, following semver.
  • Make a PR and squash merge it.
  • Back on master with the PR merged, use make publish (confirm, and enter your password).
  • Finally, go to GitHub and create a release and a tag for the new version.
  • Done!

Documentation

See the docs folder

wagtaildraftail's People

Contributors

corymb avatar dnsl48 avatar gasman avatar harrislapiroff avatar katie-day avatar loicteixeira avatar thibaudcolas avatar thijskramer 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

Watchers

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

wagtaildraftail's Issues

Add editor styling aligned with Wagtail's styleguide

For this to be a pleasant editing experience, we need the styling of the editor to be aligned with that of Wagtail. I don't want to tie Draftail to Wagtail too much, so IMHO the styles (and UI configuration) should live in here, while Draftail simply makes the style/UI configurable and themable.

See springload/draftail#63

Fix intruction to publish new package

It looks like git release vx.y.z requires some dependency? I got git: 'release' is not a git command.

Also, make publish fails:

$ make publish
dist
make: dist: No such file or directory
make: *** [publish] Error 1

Make an online demo

Linked from the README, likely on Heroku, ideally without an admin login โ€“โ€“ or with a login, but with a daily auto-reset? What is the best practice?

To investigate for potential auto-reset โ€“ deployments with Travis on push to master + Travis cron https://docs.travis-ci.com/user/cron-jobs/

Edit: could be running the test site's code?

Provide upgrade path to Wagtail 2.0

Now that it is merged into Wagtail 2.0, there's little point in updating this package compatibility. Instead, provide an upgrade path since since are handled a bit differently.

Change block/field default to None

Currently, the default value is an empty dict which is basically an invalid content state. Therefore, the editor should return None which clearly state the lack of content.

  • Update springload/draftail package.
  • Update springload/draftjs_exporter package.
  • Update springload/draftail dependency for this package.
  • Update springload/draftjs_exporter dependency for this package.
  • Update block accordingly
  • Update field accordingly (actually the field doesn't have a default value atm?)
  • Remove the now useless form field and validator?

Test coverage measurement causes some E2E tests to fail

The error is quite cryptic: Failed: cov_15u4dh72x3 is not defined. This is only when code coverage is measured, so I imagine there is some interference between istanbul, the Babel transpilation of async/await, and Electron. Maybe Istanbul doesn't realise what parts of code are actually ran in Electron.

Add default configuration for both editor and exporter

Just like you can use a RichTextField/RichTextBlock in Wagtail (powered by hallo) without the need to configure it, wagtaildraftail should ship with proper defaults (all the default entities, blocks and styles in the editor and the corresponding exporter config).

For the exporter, everything can happen in wagtaildraftail.utils.get_exporter_config.

For the editor, this might be a bit more complex. If a widget doesn't not receive any options upon initialisation, we could add some defaults, but for the widget to be instantiated in the first place, it needs to be present in the WAGTAILADMIN_RICH_TEXT_EDITORS settings (which is used by wagtail.wagtailadmin.rich_text.get_rich_text_editor_widget which both our field and block use to load the widget). I'm not too keen to hijack the settings to inject ours, so maybe we could have a intermediary utility function which first try to load "the Wagtail way", but failing that (catch KeyErrors) would return our default widget.

Django since 1.11 calls `widget.render` with named arguments, not positionals

Django 1.10 (and earlier) makes an invocation of the widget.render method passing arguments using positional propagation: https://github.com/django/django/blob/1.10.7/django/forms/boundfield.py#L101

force_text(widget.render(name, self.value(), attrs=attrs))

Django 1.11 (and later) changes this invocation so that arguments bound to names:
https://github.com/django/django/blob/1.11/django/forms/boundfield.py#L116

html = widget.render(
        name=name,
        value=self.value(),
        attrs=attrs,
        **kwargs
    )

This implies that from now onwards the attribute names are significant.

Also, there was a new attribute introduced: renderer, which becomes mandatory since django 2.1

Move entity decorator and source definition to Python

Following similar developments in our client work,

DRAFT_ENTITY_TYPE_DOCUMENT = {
    'label': 'Document',
    'type': ENTITY_TYPES.DOCUMENT,
    'source': 'DocumentSource',
    'decorator': 'Document',
    'icon': 'icon-doc-full',
}
import LinkSource from './LinkSource';
import ImageSource from './ImageSource';
import DocumentSource from './DocumentSource';
import ModelSource from './ModelSource';
import EmbedSource from './EmbedSource';

/**
 * Mapping object from name to component.
 */
export default {
    LinkSource,
    ImageSource,
    DocumentSource,
    ModelSource,
    EmbedSource,
};

Update Link and Document sources to prevent confusing editing of entities without text selected

See springload/draftail#85. Until there is a new API there, those two sources need to either:

  • Bail out of the entity adding workflow if the selection is collapsed.
  • Insert the text the entity is to be applied upon, and the entity.

I like option 2 better because it's closer to the behavior of other editors, and the text can always be edited / reverted afterwards. For links, we could go with the the page title for pages, and full URL (external or email links). For documents, the document title seems like a good link text.

Figure out path to configurable front-end supporting custom components.

At the moment the package comes with a pre-compiled bundle that can be integrated as a single script tag. This is problematic because some of Draftail still requires JS code to be written for configuration.

Figure out how to expose this properly (npm package of the same name?) so it can be consumed and customised fully.

Finish setup so E2E tests run in CI

This is just a matter of test setup. The problems are:

  • E2E tests fail when code coverage is recorded, so test coverage needs to exclude them
  • E2E tests require the server to be started (make start), but they are also used in git hooks where the server would already be started. We need to branch this off.

I think the likely best move would be to orchestrate this with tox / bash scripts, and only run it with different environments in git hooks / CI.

Investigate Wagtail 1.12 RichText Features

Wagtail 1.12 will introduce Rich Text Features (see wagtail/wagtail#3736), a way to configure the features (format, heading, etc) of a rich-text editor per field. The underlying goal is to the editor config (which is kinda global, i.e. config for the whole Draftail editor) and the features of a given field (i.e. allowing different formatting options for the intro and body blocks). Eventually, features will also be hooked with whitelisting and text conversion logic (the ultimate goal of having all rich text editor saving content in a uniform way to avoid editor lock-in (I can't find the discussion around that anymore sorry)).

I'm not sure whether it's too early to start implementing such thing, but we should at least think of it as it might shuffle quite a few things.

Stop defining boilerplate entity strategy

The two current decorators define the following boilerplate as their strategy:

export function findDocumentEntities(contentBlock, callback) {
  contentBlock.findEntityRanges((character) => {
    const entityKey = character.getEntity();
    return entityKey !== null && Entity.get(entityKey).getType() === ENTITY_TYPE.DOCUMENT;
  }, callback);
}

This is boilerplate-y enough that it should be the default behavior of Draftail โ€“ if no custom strategy is defined, use the following code with the entity's type. Note that this has to be implemented in Draftail first, then used in here. Would also save us from having to write complex tests of this behavior here.

Improve Python unit tests

  • Do they need a DB?
  • Add test-watch command. The one in draftjs_exporter is based on nodemon. Perhaps there is a more sanctified way to do this with Django tests.
  • Prevent fast failure โ€“ย we want to see all the fails in the test suite.
  • Get rid of that big useless stack trace.

Outline path to creating and storing Hallo-compatible HTML-like data

... so it's easier to integrate into existing sites.

Note that we likely won't get to 100% parity with Hallo โ€“ up to us to figure out how close this has to get.

Start defining how to do it, then think of whether to develop it ourselves or not. Two paths:

Should we test against different Django/Wagtail versions?

TL;DR: Should we test against different Django/Wagtail versions in addition to different Python versions?

52176e1 add a fix for Django >= 1.8 so it would make sense for this package to be tested against different versions of Django (and Wagtail eventually).

Looking at what Wagtail does (.travis.yml and tox.ini), they only test for Django 1.8 with Python 2.7 and test with Django 1.10 and 1.11 on all other Python versions. I imagine this is because the those are the version they have branches for in the code (just like our commit above), although it's not clear why they don't test 1.10 with Python 2.7 given it is officially supported by Django.

Regarding Wagtail, the test app has been build for 1.9 and looking at the code, should work with previous versions, although we might want to be careful.

FWIW, I'm experimenting with such config on chore/ci-config.

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.