Commit a1f037cc authored by Stan Hu's avatar Stan Hu

Merge branch '332087-render-audio-in-content-editor' into 'master'

Render audio in content editor

See merge request gitlab-org/gitlab!68598
parents a149b49d 933fe8e1
/* eslint-disable class-methods-use-this */
/* eslint-disable @gitlab/require-i18n-strings */
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
import { Node } from 'tiptap';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
/**
* Abstract base class for playable media, like video and audio.
......@@ -32,22 +30,19 @@ export default class Playable extends Node {
const parseDOM = [
{
tag: `.${this.mediaType}-container`,
skip: true,
},
{
tag: `.${this.mediaType}-container p`,
priority: HIGHER_PARSE_RULE_PRIORITY,
ignore: true,
},
{
tag: `${this.mediaType}[src]`,
getAttrs: (el) => ({ src: el.src, alt: el.dataset.title }),
tag: `.media-container`,
getAttrs: (el) => ({
src: el.querySelector('audio,video').src,
alt: el.querySelector('audio,video').dataset.title,
}),
},
];
const toDOM = (node) => [
this.mediaType,
'span',
{ class: 'media-container' },
[
this.options.mediaType,
{
src: node.attrs.src,
controls: true,
......@@ -55,11 +50,14 @@ export default class Playable extends Node {
'data-title': node.attrs.alt,
...this.extraElementAttrs,
},
],
['a', { href: node.attrs.src }, node.attrs.alt],
];
return {
attrs,
group: 'block',
group: 'inline',
inline: true,
draggable: true,
parseDOM,
toDOM,
......@@ -68,6 +66,5 @@ export default class Playable extends Node {
toMarkdown(state, node) {
defaultMarkdownSerializer.nodes.image(state, node);
state.closeBlock(node);
}
}
import Playable from './playable';
export default Playable.extend({
defaultOptions: {
...Playable.options,
mediaType: 'audio',
},
});
import { Node } from '@tiptap/core';
const queryPlayableElement = (element, mediaType) => element.querySelector(mediaType);
export default Node.create({
name: 'playable',
group: 'inline',
inline: true,
draggable: true,
addAttributes() {
return {
src: {
default: null,
parseHTML: (element) => {
const playable = queryPlayableElement(element, this.options.mediaType);
return {
src: playable.src,
};
},
},
canonicalSrc: {
default: null,
parseHTML: (element) => {
const playable = queryPlayableElement(element, this.options.mediaType);
return {
canonicalSrc: playable.dataset.canonicalSrc,
};
},
},
alt: {
default: null,
parseHTML: (element) => {
const playable = queryPlayableElement(element, this.options.mediaType);
return {
alt: playable.dataset.title,
};
},
},
};
},
parseHTML() {
return [
{
tag: '.media-container',
},
];
},
renderHTML({ node }) {
return [
'span',
{ class: 'media-container' },
[
this.options.mediaType,
{
src: node.attrs.src,
controls: true,
'data-setup': '{}',
'data-title': node.attrs.alt,
...this.extraElementAttrs,
},
],
['a', { href: node.attrs.src }, node.attrs.alt],
];
},
});
......@@ -2,6 +2,7 @@ import { Editor } from '@tiptap/vue-2';
import { isFunction } from 'lodash';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import Attachment from '../extensions/attachment';
import Audio from '../extensions/audio';
import Blockquote from '../extensions/blockquote';
import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list';
......@@ -63,6 +64,7 @@ export const createContentEditor = ({
const builtInContentEditorExtensions = [
Attachment.configure({ uploadsPath, renderMarkdown }),
Audio,
Blockquote,
Bold,
BulletList,
......
......@@ -3,6 +3,7 @@ import {
defaultMarkdownSerializer,
} from 'prosemirror-markdown/src/to_markdown';
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
import Audio from '../extensions/audio';
import Blockquote from '../extensions/blockquote';
import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list';
......@@ -40,6 +41,8 @@ import {
openTag,
closeTag,
renderOrderedList,
renderImage,
renderPlayable,
} from './serialization_helpers';
const defaultSerializerConfig = {
......@@ -92,6 +95,7 @@ const defaultSerializerConfig = {
},
nodes: {
[Audio.name]: renderPlayable,
[Blockquote.name]: (state, node) => {
if (node.attrs.multiline) {
state.write('>>>');
......@@ -120,12 +124,7 @@ const defaultSerializerConfig = {
[HardBreak.name]: renderHardBreak,
[Heading.name]: defaultMarkdownSerializer.nodes.heading,
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
[Image.name]: (state, node) => {
const { alt, canonicalSrc, src, title } = node.attrs;
const quotedTitle = title ? ` ${state.quote(title)}` : '';
state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
},
[Image.name]: renderImage,
[ListItem.name]: defaultMarkdownSerializer.nodes.list_item,
[OrderedList.name]: renderOrderedList,
[Paragraph.name]: defaultMarkdownSerializer.nodes.paragraph,
......
......@@ -286,3 +286,14 @@ export function renderHardBreak(state, node, parent, index) {
}
}
}
export function renderImage(state, node) {
const { alt, canonicalSrc, src, title } = node.attrs;
const quotedTitle = title ? ` ${state.quote(title)}` : '';
state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
}
export function renderPlayable(state, node) {
renderImage(state, node);
}
......@@ -41,6 +41,12 @@
}
}
.media-container {
display: inline-flex;
flex-direction: column;
margin-bottom: $gl-spacing-scale-2;
}
img:not(.emoji) {
margin: 0 0 8px;
}
......
......@@ -52,7 +52,7 @@ module Banzai
doc.document.create_element(media_type, media_element_attrs)
end
def download_paragraph(doc, element)
def download_link(doc, element)
link_content = element['title'] || element['alt']
link_element_attrs = {
......@@ -67,19 +67,15 @@ module Banzai
link_element_attrs['data-canonical-src'] = element['data-canonical-src']
end
link = doc.document.create_element('a', link_content, link_element_attrs)
doc.document.create_element('p').tap do |paragraph|
paragraph.children = link
end
doc.document.create_element('a', link_content, link_element_attrs)
end
def media_node(doc, element)
container_element_attrs = { class: "#{media_type}-container" }
container_element_attrs = { class: "media-container #{media_type}-container" }
doc.document.create_element( "div", container_element_attrs).tap do |container|
doc.document.create_element('span', container_element_attrs).tap do |container|
container.add_child(media_element(doc, element))
container.add_child(download_paragraph(doc, element))
container.add_child(download_link(doc, element))
end
end
end
......
......@@ -145,3 +145,15 @@
context: project_wiki
markdown: |-
Hi @gitlab - thank you for reporting this ~bug (#1) we hope to fix it in %1.1 as part of !1
- name: audio
markdown: '![Sample Audio](https://gitlab.com/gitlab.mp3)'
- name: audio_in_lists
markdown: |-
* ![Sample Audio](https://gitlab.com/1.mp3)
* ![Sample Audio](https://gitlab.com/2.mp3)
1. ![Sample Audio](https://gitlab.com/1.mp3)
2. ![Sample Audio](https://gitlab.com/2.mp3)
* [x] ![Sample Audio](https://gitlab.com/1.mp3)
* [x] ![Sample Audio](https://gitlab.com/2.mp3)
......@@ -25,18 +25,14 @@ RSpec.describe Banzai::Filter::AudioLinkFilter do
it 'replaces the image tag with an audio tag' do
container = filter(image).children.first
expect(container.name).to eq 'div'
expect(container['class']).to eq 'audio-container'
expect(container.name).to eq 'span'
expect(container['class']).to eq 'media-container audio-container'
audio, paragraph = container.children
audio, link = container.children
expect(audio.name).to eq 'audio'
expect(audio['src']).to eq src
expect(paragraph.name).to eq 'p'
link = paragraph.children.first
expect(link.name).to eq 'a'
expect(link['href']).to eq src
expect(link['target']).to eq '_blank'
......@@ -105,15 +101,13 @@ RSpec.describe Banzai::Filter::AudioLinkFilter do
image = %(<img src="#{proxy_src}" data-canonical-src="#{canonical_src}"/>)
container = filter(image).children.first
expect(container['class']).to eq 'audio-container'
expect(container['class']).to eq 'media-container audio-container'
audio, paragraph = container.children
audio, link = container.children
expect(audio['src']).to eq proxy_src
expect(audio['data-canonical-src']).to eq canonical_src
link = paragraph.children.first
expect(link['href']).to eq proxy_src
end
end
......
......@@ -25,20 +25,16 @@ RSpec.describe Banzai::Filter::VideoLinkFilter do
it 'replaces the image tag with a video tag' do
container = filter(image).children.first
expect(container.name).to eq 'div'
expect(container['class']).to eq 'video-container'
expect(container.name).to eq 'span'
expect(container['class']).to eq 'media-container video-container'
video, paragraph = container.children
video, link = container.children
expect(video.name).to eq 'video'
expect(video['src']).to eq src
expect(video['width']).to eq "400"
expect(video['preload']).to eq 'metadata'
expect(paragraph.name).to eq 'p'
link = paragraph.children.first
expect(link.name).to eq 'a'
expect(link['href']).to eq src
expect(link['target']).to eq '_blank'
......@@ -107,15 +103,13 @@ RSpec.describe Banzai::Filter::VideoLinkFilter do
image = %(<img src="#{proxy_src}" data-canonical-src="#{canonical_src}"/>)
container = filter(image).children.first
expect(container['class']).to eq 'video-container'
expect(container['class']).to eq 'media-container video-container'
video, paragraph = container.children
video, link = container.children
expect(video['src']).to eq proxy_src
expect(video['data-canonical-src']).to eq canonical_src
link = paragraph.children.first
expect(link['href']).to eq proxy_src
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment