Support for embedding HTML5 media links
Use markdown-it-html5-embed plugin so user can embed audio and video using the markdown link syntax []() in the HTML5 way.
This commit is contained in:
parent
258b502cde
commit
b32c844314
14 changed files with 218 additions and 2 deletions
2
Gemfile
2
Gemfile
|
|
@ -122,6 +122,8 @@ source "https://rails-assets.org" do
|
|||
gem "rails-assets-perfect-scrollbar", "0.6.16"
|
||||
end
|
||||
|
||||
gem "markdown-it-html5-embed", "1.0.0"
|
||||
|
||||
# Localization
|
||||
|
||||
gem "http_accept_language", "2.1.1"
|
||||
|
|
|
|||
|
|
@ -368,6 +368,7 @@ GEM
|
|||
systemu (~> 2.6.2)
|
||||
mail (2.6.6)
|
||||
mime-types (>= 1.16, < 4)
|
||||
markdown-it-html5-embed (1.0.0)
|
||||
markerb (1.1.0)
|
||||
memoizable (0.4.2)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
|
|
@ -816,6 +817,7 @@ DEPENDENCIES
|
|||
json-schema-rspec (= 0.0.4)
|
||||
leaflet-rails (= 1.2.0)
|
||||
logging-rails (= 0.6.0)
|
||||
markdown-it-html5-embed (= 1.0.0)
|
||||
markerb (= 1.1.0)
|
||||
mini_magick (= 4.8.0)
|
||||
minitest
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
|
||||
|
||||
(function(){
|
||||
app.helpers.allowedEmbedsMime = function(mimetype) {
|
||||
var v = document.createElement(mimetype[1]);
|
||||
return v.canPlayType && v.canPlayType(mimetype[0]) !== "";
|
||||
};
|
||||
|
||||
app.helpers.textFormatter = function(text, mentions) {
|
||||
mentions = mentions ? mentions : [];
|
||||
|
||||
|
|
@ -83,6 +88,30 @@
|
|||
|
||||
// Bootstrap table markup
|
||||
md.renderer.rules.table_open = function () { return "<table class=\"table table-striped\">\n"; };
|
||||
|
||||
var html5medialPlugin = window.markdownitHTML5Embed;
|
||||
md.use(html5medialPlugin, {html5embed: {
|
||||
inline: false,
|
||||
autoAppend: true,
|
||||
renderFn: function handleBarsRenderFn(parsed, mediaAttributes) {
|
||||
var attributes = mediaAttributes[parsed.mediaType];
|
||||
return HandlebarsTemplates["media-embed_tpl"]({
|
||||
mediaType: parsed.mediaType,
|
||||
attributes: attributes,
|
||||
mimetype: parsed.mimeType,
|
||||
sourceURL: parsed.url,
|
||||
title: parsed.title,
|
||||
fallback: parsed.fallback,
|
||||
needsCover: parsed.mediaType === "video"
|
||||
});
|
||||
},
|
||||
attributes: {
|
||||
"audio": "controls preload=none",
|
||||
"video": "preload=none"
|
||||
},
|
||||
isAllowedMimeType: app.helpers.allowedEmbedsMime
|
||||
}});
|
||||
|
||||
return md.render(text);
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -63,7 +63,29 @@ app.views.Content = app.views.Base.extend({
|
|||
}
|
||||
},
|
||||
|
||||
// This function is called when user clicks cover for HTML5 embedded video
|
||||
onVideoThumbClick: function(evt) {
|
||||
var clickedThumb;
|
||||
if ($(evt.target).hasClass("thumb")) {
|
||||
clickedThumb = $(evt.target);
|
||||
} else {
|
||||
clickedThumb = $(evt.target).parent(".thumb");
|
||||
}
|
||||
clickedThumb.find(".video-overlay").addClass("hidden");
|
||||
clickedThumb.parents(".collapsed").children(".expander").click();
|
||||
var video = clickedThumb.find("video");
|
||||
video.attr("controls", "");
|
||||
video.get(0).load();
|
||||
video.get(0).play();
|
||||
clickedThumb.unbind("click");
|
||||
},
|
||||
|
||||
bindMediaEmbedThumbClickEvent: function() {
|
||||
this.$(".media-embed .thumb").bind("click", this.onVideoThumbClick);
|
||||
},
|
||||
|
||||
postRenderTemplate : function(){
|
||||
this.bindMediaEmbedThumbClickEvent();
|
||||
_.defer(_.bind(this.collapseOversized, this));
|
||||
|
||||
// run collapseOversized again after all contained images are loaded
|
||||
|
|
@ -93,6 +115,8 @@ app.views.StatusMessage = app.views.Content.extend({
|
|||
|
||||
app.views.ExpandedStatusMessage = app.views.StatusMessage.extend({
|
||||
postRenderTemplate : function(){
|
||||
this.bindMediaEmbedThumbClickEvent();
|
||||
|
||||
var photoAttachments = this.$(".photo-attachments");
|
||||
if(photoAttachments.length > 0) {
|
||||
new app.views.Gallery({ el: photoAttachments });
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
//= require markdown-it-sanitizer
|
||||
//= require markdown-it-sub
|
||||
//= require markdown-it-sup
|
||||
//= require markdown-it-html5-embed
|
||||
//= require highlightjs
|
||||
//= require clear-form
|
||||
//= require corejs-typeahead
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@
|
|||
@import 'chat';
|
||||
@import 'markdown-content';
|
||||
@import 'oembed';
|
||||
@import 'media-embed';
|
||||
@import 'post-content';
|
||||
|
||||
// contacts
|
||||
|
|
|
|||
21
app/assets/stylesheets/media-embed.scss
Normal file
21
app/assets/stylesheets/media-embed.scss
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
$stub-bg-color: #ddd;
|
||||
|
||||
.media-embed {
|
||||
margin-top: 5px;
|
||||
|
||||
.thumb {
|
||||
@include video-overlay;
|
||||
|
||||
background-color: $stub-bg-color;
|
||||
|
||||
video {
|
||||
min-height: 60%;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
audio {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
19
app/assets/templates/media-embed_tpl.jst.hbs
Normal file
19
app/assets/templates/media-embed_tpl.jst.hbs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<div class="media-embed">
|
||||
{{#if needsCover}}
|
||||
<div class="thumb">
|
||||
{{/if}}
|
||||
|
||||
<{{mediaType}} {{{attributes}}}>
|
||||
<source type="{{mimetype}}" src="{{sourceURL}}" />
|
||||
{{title}}
|
||||
</{{mediaType}}>
|
||||
|
||||
{{#if needsCover}}
|
||||
<div class="video-overlay">
|
||||
<div class="video-info">
|
||||
<div class="title">{{title}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
@ -421,7 +421,7 @@ en:
|
|||
size_of_images_q: "Can I customize the size of images in posts or comments?"
|
||||
size_of_images_a: "No. Images are resized automatically to fit the stream or single-post view. Markdown does not have a code for specifying the size of an image."
|
||||
embed_multimedia_q: "How do I embed a video, audio, or other multimedia content into a post?"
|
||||
embed_multimedia_a: "You can usually just paste the URL (e.g. http://www.youtube.com/watch?v=nnnnnnnnnnn ) into your post and the video or audio will be embedded automatically. The sites supported include: YouTube, Vimeo, SoundCloud, Flickr and a few more. diaspora* uses oEmbed for this feature. We’re supporting more media sources all the time. Remember to always post simple, full links – no shortened links; no operators after the base URL – and give it a little time before you refresh the page after posting for seeing the preview."
|
||||
embed_multimedia_a: "You can usually just paste the URL (e.g. http://www.youtube.com/watch?v=nnnnnnnnnnn ) into your post and the video or audio will be embedded automatically. The sites supported include: YouTube, Vimeo, SoundCloud, Flickr and a few more. diaspora* uses oEmbed for this feature. If you post a direct link to an audio or video file, diaspora* will embed it using standard HTML5 player. We’re supporting more media sources all the time. Remember to always post simple, full links – no shortened links; no operators after the base URL – and give it a little time before you refresh the page after posting for seeing the preview."
|
||||
post_location_q: "How do I add my location to a post?"
|
||||
post_location_a: "Click the pin icon next to the camera in the publisher. This will insert your location from OpenStreetMap. You can edit your location – you might only want to include the city you’re in rather than the specific street address."
|
||||
post_poll_q: "How do I add a poll to my post?"
|
||||
|
|
|
|||
20
features/desktop/media-embed.feature
Normal file
20
features/desktop/media-embed.feature
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
@javascript
|
||||
Feature: oembed
|
||||
In order to make videos easy accessible
|
||||
As a user
|
||||
I want the media links in my posts be replaced by an embedded player
|
||||
|
||||
Background:
|
||||
Given following user exists:
|
||||
| username | email |
|
||||
| Alice Smith | alice@alice.alice |
|
||||
And I sign in as "alice@alice.alice"
|
||||
|
||||
Scenario: Post a video link
|
||||
When I click the publisher and post "[title](http://example.com/file.ogv)"
|
||||
Then I should see a HTML5 video player
|
||||
|
||||
Scenario: Post an audio link
|
||||
When I click the publisher and post "[title](http://example.com/file.ogg)"
|
||||
Then I should see a HTML5 audio player
|
||||
|
||||
6
features/step_definitions/media_embed_steps.rb
Normal file
6
features/step_definitions/media_embed_steps.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Then /^I should see a HTML5 (video|audio) player$/ do |type|
|
||||
find(".post-content .media-embed")
|
||||
find(".stream-container").should have_css(".post-content .media-embed #{type}")
|
||||
end
|
||||
|
|
@ -26,13 +26,17 @@ module PublishingCukeHelpers
|
|||
submit_publisher
|
||||
end
|
||||
|
||||
def visible_text_from_markdown(text)
|
||||
CGI.unescapeHTML(ActionController::Base.helpers.strip_tags(Diaspora::MessageRenderer.new(text).markdownified.strip))
|
||||
end
|
||||
|
||||
def submit_publisher
|
||||
txt = find("#publisher #status_message_text").value
|
||||
find("#publisher .btn-primary").click
|
||||
# wait for the publisher to be closed
|
||||
expect(find("#publisher")["class"]).to include("closed")
|
||||
# wait for the content to appear
|
||||
expect(find("#main-stream")).to have_content(txt)
|
||||
expect(find("#main-stream")).to have_content(visible_text_from_markdown(txt))
|
||||
end
|
||||
|
||||
def click_and_post(text)
|
||||
|
|
|
|||
|
|
@ -347,6 +347,54 @@ describe("app.helpers.textFormatter", function(){
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
context("media embed", function() {
|
||||
beforeEach(function() {
|
||||
spyOn(app.helpers, "allowedEmbedsMime").and.returnValue(true);
|
||||
});
|
||||
|
||||
it("embeds audio", function() {
|
||||
var html =
|
||||
'<p><a href="https://example.org/file.mp3" target="_blank" rel="noopener noreferrer">title</a></p>\n' +
|
||||
'<div class="media-embed">\n' +
|
||||
"\n" +
|
||||
" <audio controls preload=none>\n" +
|
||||
' <source type="audio/mpeg" src="https://example.org/file.mp3" />\n' +
|
||||
" title\n" +
|
||||
" </audio>\n" +
|
||||
"\n" +
|
||||
"</div>\n";
|
||||
var content = "[title](https://example.org/file.mp3)";
|
||||
var parsed = this.formatter(content);
|
||||
|
||||
expect(parsed).toContain(html);
|
||||
});
|
||||
|
||||
it("embeds video", function() {
|
||||
var html =
|
||||
'<p><a href="https://example.org/file.mp4" target="_blank" rel="noopener noreferrer">title</a></p>\n' +
|
||||
'<div class="media-embed">\n' +
|
||||
' <div class="thumb">\n' +
|
||||
"\n" +
|
||||
" <video preload=none>\n" +
|
||||
' <source type="video/mp4" src="https://example.org/file.mp4" />\n' +
|
||||
" title\n" +
|
||||
" </video>\n" +
|
||||
"\n" +
|
||||
' <div class="video-overlay">\n' +
|
||||
' <div class="video-info">\n' +
|
||||
' <div class="title">title</div>\n' +
|
||||
" </div>\n" +
|
||||
" </div>\n" +
|
||||
" </div>\n" +
|
||||
"</div>\n";
|
||||
|
||||
var content = "[title](https://example.org/file.mp4)";
|
||||
var parsed = this.formatter(content);
|
||||
|
||||
expect(parsed).toContain(html);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context("real world examples", function(){
|
||||
|
|
|
|||
|
|
@ -36,4 +36,43 @@ describe("app.views.Content", function(){
|
|||
expect(this.view.presenter().location).toEqual(factory.location());
|
||||
});
|
||||
});
|
||||
|
||||
// These tests don't work in PhantomJS because it doesn't support HTML5 <video>.
|
||||
if (/PhantomJS/.exec(navigator.userAgent) === null) {
|
||||
describe("onVideoThumbClick", function() {
|
||||
beforeEach(function() {
|
||||
this.post = new app.models.StatusMessage({text: "[title](https://www.w3schools.com/html/mov_bbb.mp4)"});
|
||||
this.view = new app.views.StatusMessage({model: this.post});
|
||||
|
||||
this.view.render();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
this.view.$("video").stop();
|
||||
});
|
||||
|
||||
it("hides video overlay", function() {
|
||||
expect(this.view.$(".video-overlay").length).toBe(1);
|
||||
this.view.$(".media-embed .thumb").click();
|
||||
expect(this.view.$(".video-overlay")).toHaveClass("hidden");
|
||||
});
|
||||
|
||||
it("expands posts on click", function() {
|
||||
this.view.$(".collapsible").height(500);
|
||||
this.view.collapseOversized();
|
||||
|
||||
expect(this.view.$(".collapsed").length).toBe(1);
|
||||
this.view.$(".media-embed .thumb").click();
|
||||
expect(this.view.$(".opened").length).toBe(1);
|
||||
});
|
||||
|
||||
it("plays video", function(done) {
|
||||
this.view.$("video").on("playing", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
this.view.$(".media-embed .thumb").click();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue