-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow media files to be embedded in rich text content
- Update tests for rich text chooser - Address inconsistent tab/spacing issues - Address more CI issues - Properly sort imports - For wagtail versions <2.5, use MediaEmbedHandler instead of EmbedHandler
- Loading branch information
juan0tron
committed
Feb 29, 2020
1 parent
8b11344
commit 5f16dc4
Showing
13 changed files
with
482 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
from draftjs_exporter.dom import DOM | ||
from wagtail.admin.rich_text.converters.contentstate_models import Entity | ||
from wagtail.admin.rich_text.converters.html_to_contentstate import AtomicBlockEntityElementHandler | ||
|
||
from wagtailmedia.models import get_media_model | ||
|
||
|
||
def media_entity(props): | ||
""" | ||
Helper to construct elements of the form | ||
<embed embedtype="custommedia" id="1"/> | ||
when converting from contentstate data | ||
""" | ||
return DOM.create_element('embed', { | ||
'embedtype': 'wagtailmedia', | ||
'id': props.get('id'), | ||
'title': props.get('title'), | ||
'type': props.get('type'), | ||
|
||
'thumbnail': props.get('thumbnail'), | ||
'file': props.get('file'), | ||
|
||
'autoplay': props.get('autoplay'), | ||
'mute': props.get('mute'), | ||
'loop': props.get('loop'), | ||
}) | ||
|
||
|
||
class MediaElementHandler(AtomicBlockEntityElementHandler): | ||
""" | ||
Rule for building a media entity when converting from | ||
database representation to contentstate | ||
""" | ||
def create_entity(self, name, attrs, state, contentstate): | ||
Media = get_media_model() | ||
try: | ||
media = Media.objects.get(id=attrs['id']) | ||
except Media.DoesNotExist: | ||
media = None | ||
|
||
return Entity('MEDIA', 'IMMUTABLE', { | ||
'id': attrs['id'], | ||
'title': media.title, | ||
'type': media.type, | ||
|
||
'thumbnail': media.thumbnail.url if media.thumbnail else '', | ||
'file': media.file.url if media.file else '', | ||
|
||
'autoplay': True if attrs.get('autoplay') == 'true' else False, | ||
'loop': True if attrs.get('loop') == 'true' else False, | ||
'mute': True if attrs.get('mute') == 'true' else False | ||
}) | ||
|
||
|
||
ContentstateMediaConversionRule = { | ||
'from_database_format': { | ||
'embed[embedtype="wagtailmedia"]': MediaElementHandler(), | ||
}, | ||
'to_database_format': { | ||
'entity_decorators': { | ||
'MEDIA': media_entity | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
from django.template.loader import render_to_string | ||
|
||
from wagtail import VERSION as WAGTAIL_VERSION | ||
|
||
from wagtailmedia.models import get_media_model | ||
|
||
if WAGTAIL_VERSION < (2, 5): | ||
from wagtail.embeds.rich_text import MediaEmbedHandler as EmbedHandler | ||
else: | ||
from wagtail.core.rich_text import EmbedHandler | ||
|
||
|
||
class MediaEmbedHandler(EmbedHandler): | ||
identifier = 'wagtailmedia' | ||
|
||
@staticmethod | ||
def get_model(): | ||
return get_media_model() | ||
|
||
@staticmethod | ||
def expand_db_attributes(attrs): | ||
""" | ||
Given a dict of attributes from the <embed> tag, return the real HTML | ||
representation for use on the front-end. | ||
""" | ||
|
||
if(attrs['type'] == 'video'): | ||
template = 'wagtailmedia/embeds/video_embed.html' | ||
elif(attrs['type'] == 'audio'): | ||
template = 'wagtailmedia/embeds/audio_embed.html' | ||
|
||
return render_to_string(template, { | ||
'title': attrs['title'], | ||
|
||
'thumbnail': attrs['thumbnail'], | ||
'file': attrs['file'], | ||
|
||
'autoplay': True if attrs['autoplay'] == 'true' else False, | ||
'loop': True if attrs['loop'] == 'true' else False, | ||
'mute': True if attrs['mute'] == 'true' else False | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
206 changes: 206 additions & 0 deletions
206
wagtailmedia/static/wagtailmedia/js/WagtailMediaBlock.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
const React = window.React; | ||
const ReactDOM = window.ReactDOM; | ||
const Modifier = window.DraftJS.Modifier; | ||
const EditorState = window.DraftJS.EditorState; | ||
const AtomicBlockUtils = window.DraftJS.AtomicBlockUtils; | ||
|
||
/** | ||
* Choose a media file in this modal | ||
*/ | ||
class WagtailMediaChooser extends window.draftail.ModalWorkflowSource { | ||
componentDidMount() { | ||
const { onClose, entityType, entity, editorState } = this.props; | ||
|
||
$(document.body).on('hidden.bs.modal', this.onClose); | ||
|
||
this.workflow = global.ModalWorkflow({ | ||
url: `${window.chooserUrls.mediaChooser}?select_format=true`, | ||
onload: MEDIA_CHOOSER_MODAL_ONLOAD_HANDLERS, | ||
urlParams: {}, | ||
responses: { | ||
mediaChosen: (data) => this.onChosen(data) | ||
}, | ||
onError: (err) => { | ||
console.error("WagtailMediaChooser Error", err); | ||
onClose(); | ||
}, | ||
}); | ||
} | ||
|
||
onChosen(data) { | ||
const { editorState, entityType, onComplete } = this.props; | ||
|
||
const content = editorState.getCurrentContent(); | ||
const selection = editorState.getSelection(); | ||
|
||
const entityData = data; | ||
const mutability = 'IMMUTABLE'; | ||
|
||
const contentWithEntity = content.createEntity(entityType.type, mutability, entityData); | ||
const entityKey = contentWithEntity.getLastCreatedEntityKey(); | ||
const nextState = AtomicBlockUtils.insertAtomicBlock(editorState, entityKey, ' '); | ||
|
||
this.workflow.close(); | ||
|
||
onComplete(nextState); | ||
} | ||
} | ||
|
||
// Constraints the maximum size of the tooltip. | ||
const OPTIONS_MAX_WIDTH = 300; | ||
const OPTIONS_SPACING = 70; | ||
const TOOLTIP_MAX_WIDTH = OPTIONS_MAX_WIDTH + OPTIONS_SPACING; | ||
|
||
/** | ||
* Places media thumbnail HTML in the Rich Text Editor | ||
*/ | ||
class WagtailMediaBlock extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
|
||
this.state = { | ||
showTooltipAt: null, | ||
}; | ||
|
||
this.setState = this.setState.bind(this); | ||
this.openTooltip = this.openTooltip.bind(this); | ||
this.closeTooltip = this.closeTooltip.bind(this); | ||
this.renderTooltip = this.renderTooltip.bind(this); | ||
} | ||
|
||
componentDidMount() { | ||
document.addEventListener('mouseup', this.closeTooltip); | ||
document.addEventListener('keyup', this.closeTooltip); | ||
window.addEventListener('resize', this.closeTooltip); | ||
} | ||
|
||
openTooltip(e) { | ||
const { blockProps } = this.props; | ||
const { entity, onRemoveEntity } = blockProps; | ||
const data = entity.getData(); | ||
|
||
const trigger = e.target.closest('[data-draftail-trigger]'); | ||
|
||
if (!trigger) return; // Click is within the tooltip | ||
|
||
const container = trigger.closest('[data-draftail-editor-wrapper]'); | ||
|
||
if (container.children.length > 1) return; // Tooltip already exists | ||
|
||
const containerRect = container.getBoundingClientRect(); | ||
const rect = trigger.getBoundingClientRect(); | ||
const maxWidth = trigger.parentNode.offsetWidth - rect.width; | ||
const direction = maxWidth >= TOOLTIP_MAX_WIDTH ? 'left' : 'top-left'; // Determines position of the arrow on the tooltip | ||
|
||
let top = 0; | ||
let left = 0; | ||
|
||
if(direction == 'left'){ | ||
left = rect.width + 50; | ||
top = rect.top - containerRect.top + (rect.height / 2); | ||
} | ||
else if (direction == 'top-left'){ | ||
top = rect.top - containerRect.top + rect.height; | ||
} | ||
|
||
this.setState({ | ||
showTooltipAt: { | ||
container: container, | ||
top: top, | ||
left: left, | ||
width: rect.width, | ||
height: rect.height, | ||
direction: direction, | ||
} | ||
}); | ||
} | ||
|
||
closeTooltip(e) { | ||
if(e.target.classList){ | ||
if(e.target.classList.contains("Tooltip__button")){ | ||
return; // Don't setState if the "Delete" button was clicked | ||
} | ||
} | ||
this.setState({ showTooltipAt: null }); | ||
} | ||
|
||
/** | ||
* Returns either a tooltip "portal" element or null | ||
*/ | ||
renderTooltip(data) { | ||
const { showTooltipAt } = this.state; | ||
const { blockProps } = this.props; | ||
const { entity, onRemoveEntity } = blockProps; | ||
|
||
// No tooltip coords exist, don't show one | ||
if(!showTooltipAt) return null; | ||
|
||
let options = [] | ||
if(data.autoplay) options.push("Autoplay"); | ||
if(data.mute) options.push("Mute"); | ||
if(data.loop) options.push("Loop"); | ||
const options_str = options.length ? options.join(", ") : ""; | ||
|
||
return ReactDOM.createPortal(React.createElement('div', null, | ||
React.createElement('div', | ||
{ | ||
style: { | ||
top: showTooltipAt.top, | ||
left: showTooltipAt.left | ||
}, | ||
class: "Tooltip Tooltip--"+showTooltipAt.direction, | ||
role: "tooltip" | ||
}, | ||
React.createElement('div', { style: { maxWidth: showTooltipAt.width } }, [ | ||
React.createElement('p', { | ||
class: "ImageBlock__alt" | ||
}, data.type.toUpperCase()+": "+data.title), | ||
React.createElement('p', { class: "ImageBlock__alt" }, options_str), | ||
React.createElement('button', { | ||
class: "button button-secondary no Tooltip__button", | ||
onClick: onRemoveEntity | ||
}, "Delete") | ||
]) | ||
) | ||
), showTooltipAt.container); | ||
} | ||
|
||
render() { | ||
const { blockProps } = this.props; | ||
const { entity } = blockProps; | ||
const data = entity.getData(); | ||
|
||
let icon; | ||
if(data.type == 'video'){ | ||
icon = React.createElement('span', { class:"icon icon-fa-video-camera", 'aria-hidden':"true" }); | ||
} | ||
else if(data.type == 'audio'){ | ||
icon = React.createElement('span', { class:"icon icon-fa-music", 'aria-hidden':"true" }); | ||
} | ||
|
||
return React.createElement('button', | ||
{ | ||
class: 'MediaBlock WagtailMediaBlock '+data.type, | ||
type: 'button', | ||
tabindex: '-1', | ||
'data-draftail-trigger': "true", | ||
onClick: this.openTooltip, | ||
style: { 'min-width': '100px', 'min-height': '100px'} | ||
}, | ||
[ | ||
React.createElement('span', | ||
{ class:"MediaBlock__icon-wrapper", 'aria-hidden': "true" }, | ||
React.createElement('span', {}, icon) | ||
), | ||
React.createElement('img', { src: data.thumbnail }), | ||
this.renderTooltip(data) | ||
] | ||
); | ||
} | ||
} | ||
|
||
window.draftail.registerPlugin({ | ||
type: 'MEDIA', | ||
source: WagtailMediaChooser, | ||
block: WagtailMediaBlock | ||
}); |
31 changes: 31 additions & 0 deletions
31
wagtailmedia/templates/wagtailmedia/chooser/select_format.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
{% load wagtailimages_tags %} | ||
{% load i18n %} | ||
{% trans "Media Player Options" as choose_str %} | ||
{% include "wagtailadmin/shared/header.html" with title=choose_str %} | ||
|
||
<div class="row row-flush nice-padding"> | ||
<div class="col5"> | ||
{% if media.thumbnail %} | ||
<img src="{{media.thumbnail.url}}" alt="{{media.title}}"> | ||
{% elif media.type == 'video' %} | ||
<div class="media-thumbnail-placeholder video"> | ||
<i class="icon icon-fa-video-camera"></i> | ||
</div> | ||
{% elif media.type == 'audio' %} | ||
<div class="media-thumbnail-placeholder audio"> | ||
<i class="icon icon-fa-music"></i> | ||
</div> | ||
{% endif %} | ||
</div> | ||
<div class="col7"> | ||
<form action="{% url 'wagtailmedia:chooser_select_format' media.id %}" class="media-player-settings" method="POST" novalidate> | ||
{% csrf_token %} | ||
<ul class="fields"> | ||
{% for field in form %} | ||
{% include "wagtailadmin/shared/field_as_li.html" with field=field %} | ||
{% endfor %} | ||
<li><input type="submit" value="{% trans 'Insert media' %}" class="button" /></li> | ||
</ul> | ||
</form> | ||
</div> | ||
</div> |
10 changes: 10 additions & 0 deletions
10
wagtailmedia/templates/wagtailmedia/embeds/audio_embed.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{% block audio %} | ||
<audio | ||
class="wagtailmedia-audio" | ||
src="{{file}}" | ||
{% if mute %}muted{% endif %} | ||
{% if autoplay %}autoplay{% endif %} | ||
{% if loop %}loop{% endif %} | ||
controls> | ||
</audio> | ||
{% endblock %} |
10 changes: 10 additions & 0 deletions
10
wagtailmedia/templates/wagtailmedia/embeds/video_embed.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{% block video %} | ||
<video class="wagtailmedia-video" | ||
controls | ||
{% if mute %}muted{% endif %} | ||
{% if autoplay %}autoplay{% endif %} | ||
{% if loop %}loop{% endif %} | ||
{% if thumbnail %}poster="{{thumbnail}}"{% endif %}> | ||
<source src="{{file}}" data-title={{title}}> | ||
</video> | ||
{% endblock %} |
Oops, something went wrong.