Fixes #2713 - Knowledge Base does't support animated GIFs and embedded Video (Youtube/Vimeo) content.
This commit is contained in:
parent
695d05689b
commit
0a70711695
24 changed files with 474 additions and 72 deletions
|
@ -5,10 +5,13 @@ class App.UiElement.richtext.additions.RichTextToolButton
|
|||
@klass: ->
|
||||
# Needs implementation. Return constructor of RichTextToolPopup subclass.
|
||||
|
||||
@pickExisting: (sel, textEditor) ->
|
||||
# needs implementation
|
||||
|
||||
@initializeAttributes: {}
|
||||
|
||||
@instantiateContent: (event, selection, delegate) ->
|
||||
attrs = @initializeAttributes
|
||||
attrs = $.extend(true, {}, @initializeAttributes)
|
||||
|
||||
attrs['event'] = event
|
||||
attrs['selection'] = selection
|
||||
|
@ -35,46 +38,6 @@ class App.UiElement.richtext.additions.RichTextToolButton
|
|||
|
||||
hash
|
||||
|
||||
@pickLinkInSingleContainer: (elem, containerToLookUpTo) ->
|
||||
if elem.nodeName == 'A'
|
||||
elem
|
||||
else if innerLink = $(elem).find('a')[0]
|
||||
innerLink
|
||||
else if containerToLookUpTo and closestLink = $(elem).closest('a', containerToLookUpTo)[0]
|
||||
closestLink
|
||||
else
|
||||
null
|
||||
|
||||
@pickLinkAt: (elem, container, direction, boundary = null) ->
|
||||
for parent in App.UiElement.richtext.buildParentsListWithSelf(elem, container)
|
||||
if parent.nodeName is 'A'
|
||||
return parent
|
||||
|
||||
for elem in App.UiElement.richtext.allDirectionalSiblings(parent, direction, boundary)
|
||||
if link = @pickLinkInSingleContainer(elem)
|
||||
return link
|
||||
|
||||
null
|
||||
|
||||
@pickLink: (sel, textEditor) ->
|
||||
range = sel.getRangeAt(0)
|
||||
|
||||
if range.startContainer == range.endContainer
|
||||
return @pickLinkInSingleContainer(range.startContainer, textEditor)
|
||||
|
||||
if link = @pickLinkAt(range.startContainer, range.commonAncestorContainer, 1, range.endContainer)
|
||||
return link
|
||||
|
||||
if startParent = App.UiElement.richtext.buildParentsList(range.startContainer, range.commonAncestorContainer).pop()
|
||||
for elem in App.UiElement.richtext.allDirectionalSiblings(startParent, 1, range.endContainer)
|
||||
if link = @pickLinkInSingleContainer(elem)
|
||||
return link
|
||||
|
||||
if link = @pickLinkAt(range.endContainer, range.commonAncestorContainer, -1)
|
||||
return link
|
||||
|
||||
return null
|
||||
|
||||
# close other buttons' popovers
|
||||
@closeOtherPopovers: (event) ->
|
||||
$(event.currentTarget)
|
||||
|
@ -88,15 +51,10 @@ class App.UiElement.richtext.additions.RichTextToolButton
|
|||
@selectionSnapshot: (sel) ->
|
||||
textEditor = $(event.currentTarget).closest('.richtext.form-control').find('[contenteditable]')
|
||||
|
||||
if sel.isCollapsed and selectedLink = $(sel.anchorNode).closest('a')[0]
|
||||
if selected = @pickExisting(sel, textEditor)
|
||||
{
|
||||
type: 'existing'
|
||||
dom: $(selectedLink)
|
||||
}
|
||||
else if !sel.isCollapsed and selectedLink = @pickLink(sel, textEditor)
|
||||
{
|
||||
type: 'existing'
|
||||
dom: $(selectedLink)
|
||||
dom: $(selected)
|
||||
}
|
||||
else if sel.type is 'Range' and $(sel.anchorNode).closest('[contenteditable]', textEditor)[0]
|
||||
range = sel.getRangeAt(0)
|
||||
|
@ -117,6 +75,7 @@ class App.UiElement.richtext.additions.RichTextToolButton
|
|||
dom: textEditor
|
||||
}
|
||||
|
||||
# on clicking button above rich text area
|
||||
@onClick: (event, delegate) ->
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
class App.UiElement.richtext.additions.RichTextToolButtonLink extends App.UiElement.richtext.additions.RichTextToolButton
|
||||
@pickLinkInSingleContainer: (elem, containerToLookUpTo) ->
|
||||
if elem.nodeName == 'A'
|
||||
elem
|
||||
else if innerLink = $(elem).find('a')[0]
|
||||
innerLink
|
||||
else if containerToLookUpTo and closestLink = $(elem).closest('a', containerToLookUpTo)[0]
|
||||
closestLink
|
||||
else
|
||||
null
|
||||
|
||||
@pickLinkAt: (elem, container, direction, boundary = null) ->
|
||||
for parent in App.UiElement.richtext.buildParentsListWithSelf(elem, container)
|
||||
if parent.nodeName is 'A'
|
||||
return parent
|
||||
|
||||
for elem in App.UiElement.richtext.allDirectionalSiblings(parent, direction, boundary)
|
||||
if link = @pickLinkInSingleContainer(elem)
|
||||
return link
|
||||
|
||||
null
|
||||
|
||||
@pickExisting: (sel, textEditor) ->
|
||||
if sel.isCollapsed and link = $(sel.anchorNode).closest('a')[0]
|
||||
return link
|
||||
|
||||
if sel.isCollapsed
|
||||
return null
|
||||
|
||||
range = sel.getRangeAt(0)
|
||||
|
||||
if range.startContainer == range.endContainer
|
||||
return @pickLinkInSingleContainer(range.startContainer, textEditor)
|
||||
|
||||
if link = @pickLinkAt(range.startContainer, range.commonAncestorContainer, 1, range.endContainer)
|
||||
return link
|
||||
|
||||
if startParent = App.UiElement.richtext.buildParentsList(range.startContainer, range.commonAncestorContainer).pop()
|
||||
for elem in App.UiElement.richtext.allDirectionalSiblings(startParent, 1, range.endContainer)
|
||||
if link = @pickLinkInSingleContainer(elem)
|
||||
return link
|
||||
|
||||
if link = @pickLinkAt(range.endContainer, range.commonAncestorContainer, -1)
|
||||
return link
|
||||
|
||||
return null
|
|
@ -1,7 +1,11 @@
|
|||
class App.UiElement.richtext.additions.RichTextToolPopup extends App.ControllerForm
|
||||
events:
|
||||
'submit form': 'onSubmit'
|
||||
'click .js-unlink': 'onUnlink'
|
||||
'click .js-clear': 'onClear'
|
||||
|
||||
labelNew: 'Link'
|
||||
labelExisting: 'Update'
|
||||
labelClear: 'Remove'
|
||||
|
||||
formParams: (params) ->
|
||||
# needs implementation
|
||||
|
@ -9,13 +13,13 @@ class App.UiElement.richtext.additions.RichTextToolPopup extends App.ControllerF
|
|||
constructor: (params) ->
|
||||
if params.selection.type is 'existing'
|
||||
url = params.selection.dom.attr('href')
|
||||
label = 'Update'
|
||||
label = @labelExisting
|
||||
additional = [{
|
||||
className: 'btn btn--danger js-unlink'
|
||||
text: 'Remove'
|
||||
className: 'btn btn--danger js-clear'
|
||||
text: @labelClear
|
||||
}]
|
||||
else
|
||||
label = 'Link'
|
||||
label = @labelNew
|
||||
|
||||
defaultParams =
|
||||
params: @formParams(params)
|
||||
|
@ -39,16 +43,17 @@ class App.UiElement.richtext.additions.RichTextToolPopup extends App.ControllerF
|
|||
getAjaxAttributes: (field, attributes) ->
|
||||
@delegate?.getAjaxAttributes?(field, attributes)
|
||||
|
||||
onUnlink: (e) ->
|
||||
onClear: (e) =>
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
switch @selection.type
|
||||
when 'existing'
|
||||
$(@selection.dom).contents().unwrap()
|
||||
@clear()
|
||||
|
||||
$(@event.currentTarget).popover('hide')
|
||||
|
||||
clear: ->
|
||||
# needs implementation
|
||||
|
||||
@wrapElement: (wrapper, selection) ->
|
||||
topLevelOriginals = App.UiElement.richtext.buildParentsList(selection.range.startContainer, selection.range.commonAncestorContainer).reverse()
|
||||
|
||||
|
@ -114,15 +119,15 @@ class App.UiElement.richtext.additions.RichTextToolPopup extends App.ControllerF
|
|||
|
||||
wrapper.insertAfter(topLevelOriginalStart)
|
||||
|
||||
wrapLink: ->
|
||||
apply: (callback) ->
|
||||
# needs implementation
|
||||
callback()
|
||||
|
||||
onSubmit: (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
@wrapLink()
|
||||
|
||||
$(@event.currentTarget).popover('destroy')
|
||||
@apply =>
|
||||
$(@event.currentTarget).popover('destroy')
|
||||
|
||||
didInitialize: ->
|
||||
switch @selection.type
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
# coffeelint: disable=camel_case_classes
|
||||
class App.UiElement.richtext.toolButtons.embed_video extends App.UiElement.richtext.additions.RichTextToolButton
|
||||
@icon: 'cloud'
|
||||
@text: 'Video'
|
||||
@klass: -> App.UiElement.richtext.additions.RichTextToolPopupVideo
|
||||
@initializeAttributes:
|
||||
model:
|
||||
configure_attributes: [
|
||||
{
|
||||
name: 'link'
|
||||
display: 'Link'
|
||||
tag: 'input'
|
||||
placeholder: 'Youtube or Vimeo address'
|
||||
}
|
||||
]
|
||||
|
||||
@pickExisting: (sel, textEditor) ->
|
||||
startNode = null
|
||||
startOffset = null
|
||||
|
||||
endNode = null
|
||||
endOffset = null
|
||||
|
||||
return if !textEditor[0].contains(sel.anchorNode)
|
||||
|
||||
walker = document.createTreeWalker(textEditor[0])
|
||||
|
||||
walker.currentNode = sel.anchorNode
|
||||
|
||||
while !startNode and (walker.currentNode.nodeName == '#text' || walker.currentNode.nodeName == 'SPAN') and walker.currentNode
|
||||
if walker.currentNode instanceof Text
|
||||
offset = walker.currentNode.textContent.indexOf '('
|
||||
if offset? and offset > -1
|
||||
startNode = walker.currentNode
|
||||
startOffset = offset
|
||||
|
||||
walker.previousNode()
|
||||
|
||||
walker.currentNode = sel.anchorNode # back to start
|
||||
|
||||
while !endNode and (walker.currentNode.nodeName == '#text' || walker.currentNode.nodeName == 'SPAN') and walker.currentNode
|
||||
if walker.currentNode instanceof Text
|
||||
offset = walker.currentNode.textContent.indexOf ')'
|
||||
if offset? and offset > -1 and (walker.currentNode != sel.anchorNode || offset > startOffset)
|
||||
endNode = walker.currentNode
|
||||
endOffset = offset + 1
|
||||
|
||||
walker.nextNode()
|
||||
|
||||
if startNode and endNode
|
||||
range = document.createRange()
|
||||
range.setStart(startNode, startOffset)
|
||||
range.setEnd(endNode, endOffset)
|
||||
|
||||
copy = range.cloneContents()
|
||||
|
||||
wrapper = document.createElement('span')
|
||||
wrapper.append(copy)
|
||||
|
||||
range.deleteContents()
|
||||
range.insertNode(wrapper)
|
||||
|
||||
wrapper
|
|
@ -0,0 +1,18 @@
|
|||
# coffeelint: disable=camel_case_classes
|
||||
class App.UiElement.richtext.toolButtons.insert_image extends App.UiElement.richtext.additions.RichTextToolButton
|
||||
@icon: 'web'
|
||||
@text: 'Image'
|
||||
@klass: -> App.UiElement.richtext.additions.RichTextToolPopupImage
|
||||
@initializeAttributes:
|
||||
model:
|
||||
configure_attributes: [
|
||||
{
|
||||
name: 'link'
|
||||
display: 'Image'
|
||||
tag: 'input'
|
||||
type: 'file'
|
||||
}
|
||||
]
|
||||
|
||||
@pickExisting: (sel, textEditor) ->
|
||||
selectedImage = textEditor.find('img.objectResizingEditorActive')[0]
|
|
@ -1,5 +1,5 @@
|
|||
# coffeelint: disable=camel_case_classes
|
||||
class App.UiElement.richtext.toolButtons.link_answer extends App.UiElement.richtext.additions.RichTextToolButton
|
||||
class App.UiElement.richtext.toolButtons.link_answer extends App.UiElement.richtext.additions.RichTextToolButtonLink
|
||||
@icon: 'knowledge-base-answer'
|
||||
@text: 'Link Answer'
|
||||
@klass: -> App.UiElement.richtext.additions.RichTextToolPopupAnswer
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# coffeelint: disable=camel_case_classes
|
||||
class App.UiElement.richtext.toolButtons.link extends App.UiElement.richtext.additions.RichTextToolButton
|
||||
class App.UiElement.richtext.toolButtons.link extends App.UiElement.richtext.additions.RichTextToolButtonLink
|
||||
@icon: 'chain'
|
||||
@text: 'Weblink'
|
||||
@klass: -> App.UiElement.richtext.additions.RichTextToolPopupLink
|
||||
|
|
|
@ -18,7 +18,7 @@ class App.UiElement.richtext.additions.RichTextToolPopupAnswer extends App.UiEle
|
|||
|
||||
dom
|
||||
|
||||
wrapLink: ->
|
||||
apply: (callback) ->
|
||||
id = @el.find('input').val()
|
||||
object = App.KnowledgeBaseAnswerTranslation.find(id)
|
||||
textEditor = $(@event.currentTarget).closest('.richtext.form-control').find('[contenteditable]')
|
||||
|
@ -41,3 +41,10 @@ class App.UiElement.richtext.additions.RichTextToolPopupAnswer extends App.UiEle
|
|||
@applyOnto(newElem, object)
|
||||
placeholder.wrap(newElem)
|
||||
placeholder.contents()
|
||||
|
||||
callback()
|
||||
|
||||
clear: ->
|
||||
switch @selection.type
|
||||
when 'existing'
|
||||
$(@selection.dom).contents().unwrap()
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
class App.UiElement.richtext.additions.RichTextToolPopupImage extends App.UiElement.richtext.additions.RichTextToolPopup
|
||||
labelNew: 'Insert'
|
||||
labelExisting: 'Replace'
|
||||
|
||||
apply: (callback) ->
|
||||
@el.find('btn--create').attr('disabled', true)
|
||||
|
||||
file = @el.find('input')[0].files[0]
|
||||
|
||||
reader = new FileReader()
|
||||
|
||||
reader.addEventListener('load', =>
|
||||
@insertImage(reader.result)
|
||||
callback()
|
||||
, false)
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
applyOnto: (dom, base64) ->
|
||||
dom.attr('src', base64)
|
||||
|
||||
insertImage: (base64) ->
|
||||
textEditor = $(@event.currentTarget).closest('.richtext.form-control').find('[contenteditable]')
|
||||
|
||||
switch @selection.type
|
||||
when 'existing'
|
||||
@applyOnto(@selection.dom, base64)
|
||||
when 'append'
|
||||
newElem = $('<img>')[0]
|
||||
newElem.src = base64
|
||||
newElem.style = 'width: 1000px; max-width: 100%;'
|
||||
@selection.dom.append(newElem)
|
||||
when 'caret'
|
||||
newElem = $('<img>')
|
||||
newElem.attr('src', base64)
|
||||
newElem.attr('style', 'width: 1000px; max-width: 100%;')
|
||||
|
||||
surroundingDom = @selection.dom[0]
|
||||
|
||||
if surroundingDom instanceof Text
|
||||
@selection.dom[0].splitText(@selection.offset)
|
||||
|
||||
newElem.insertAfter(@selection.dom)
|
||||
when 'range'
|
||||
newElem = $('<img>')
|
||||
newElem.attr('src', base64)
|
||||
newElem.attr('style', 'width: 1000px; max-width: 100%;')
|
||||
|
||||
placeholder = textEditor.find('span.highlight-emulator')
|
||||
|
||||
placeholder.empty()
|
||||
placeholder.append(newElem)
|
||||
|
||||
clear: ->
|
||||
switch @selection.type
|
||||
when 'existing'
|
||||
@selection.dom.closest('.enableObjectResizingShim').remove()
|
||||
@selection.dom.remove() # just in case shim was lost while the dialog was open
|
|
@ -26,7 +26,7 @@ class App.UiElement.richtext.additions.RichTextToolPopupLink extends App.UiEleme
|
|||
else
|
||||
input
|
||||
|
||||
wrapLink: ->
|
||||
apply: (callback) ->
|
||||
input = @el.find('input').val()
|
||||
url = @ensureProtocol(input)
|
||||
|
||||
|
@ -50,3 +50,10 @@ class App.UiElement.richtext.additions.RichTextToolPopupLink extends App.UiEleme
|
|||
@applyOnto(newElem, url)
|
||||
placeholder.wrap(newElem)
|
||||
placeholder.contents()
|
||||
|
||||
callback()
|
||||
|
||||
clear: ->
|
||||
switch @selection.type
|
||||
when 'existing'
|
||||
$(@selection.dom).contents().unwrap()
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
class App.UiElement.richtext.additions.RichTextToolPopupVideo extends App.UiElement.richtext.additions.RichTextToolPopup
|
||||
labelNew: 'Insert'
|
||||
labelExisting: 'Replace'
|
||||
|
||||
@regexps: {
|
||||
youtube: [
|
||||
/youtube.com\/watch\?v=([\w]+)/
|
||||
/youtu.be\/([\w]+)/
|
||||
],
|
||||
vimeo: [
|
||||
/vimeo.com\/([\w]+)/
|
||||
]
|
||||
}
|
||||
|
||||
@detectProviderAndId: (input) =>
|
||||
return if !input
|
||||
|
||||
output = null
|
||||
|
||||
for provider, regexps of @regexps
|
||||
for regexp in regexps
|
||||
if result = input.match(regexp)
|
||||
return [provider, result[1]]
|
||||
|
||||
@urlToMarkup: (input) ->
|
||||
parsed = @detectProviderAndId(input)
|
||||
|
||||
return if !parsed
|
||||
|
||||
"( widget: video, provider: #{parsed[0]}, id: #{parsed[1]} )"
|
||||
|
||||
apply: (callback) ->
|
||||
input = @el.find('input').val()
|
||||
markup = @constructor.urlToMarkup(input)
|
||||
|
||||
if !markup
|
||||
new App.ControllerErrorModal(
|
||||
message: 'Invalid video URL'
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@insertVideo(markup)
|
||||
callback()
|
||||
|
||||
insertVideo: (markup) ->
|
||||
textEditor = $(@event.currentTarget).closest('.richtext.form-control').find('[contenteditable]')
|
||||
|
||||
switch @selection.type
|
||||
when 'existing'
|
||||
@selection.dom.text(markup)
|
||||
when 'append'
|
||||
newElem = document.createTextNode(markup)
|
||||
@selection.dom.append(newElem)
|
||||
when 'caret'
|
||||
newElem = document.createTextNode(markup)
|
||||
|
||||
surroundingDom = @selection.dom[0]
|
||||
|
||||
if surroundingDom instanceof Text
|
||||
@selection.dom[0].splitText(@selection.offset)
|
||||
|
||||
$(newElem).insertAfter(@selection.dom)
|
||||
when 'range'
|
||||
newElem = document.createTextNode(markup)
|
||||
|
||||
placeholder = textEditor.find('span.highlight-emulator')
|
||||
|
||||
placeholder.empty()
|
||||
placeholder.append(newElem)
|
|
@ -82,9 +82,16 @@ class App.KnowledgeBaseReaderController extends App.Controller
|
|||
@renderPopovers()
|
||||
|
||||
renderBody: (translation) ->
|
||||
body = $($.parseHTML(translation.content().body))
|
||||
body = translation.content().body
|
||||
body = @prepareLinks(body)
|
||||
body = @prepareVideos(body)
|
||||
|
||||
for linkDom in body.find('a').andSelf('a').toArray()
|
||||
@answerBody.html(body)
|
||||
|
||||
prepareLinks: (input) ->
|
||||
input = $($.parseHTML(input))
|
||||
|
||||
for linkDom in input.find('a').andSelf('a').toArray()
|
||||
switch $(linkDom).attr('data-target-type')
|
||||
when 'knowledge-base-answer'
|
||||
if object = App.KnowledgeBaseAnswerTranslation.find $(linkDom).attr('data-target-id')
|
||||
|
@ -92,7 +99,30 @@ class App.KnowledgeBaseReaderController extends App.Controller
|
|||
else
|
||||
$(linkDom).attr 'href', '#'
|
||||
|
||||
@answerBody.html(body)
|
||||
$('<container>').append(input).html()
|
||||
|
||||
prepareVideos: (input) ->
|
||||
input.replace /\(([\s]*)widget:([\s]*)video[\W]([\s\S])+?\)/g, (match) ->
|
||||
settings = match
|
||||
.slice(1, -1)
|
||||
.split(',')
|
||||
.map (pair) -> pair.split(':').map (elem) -> elem.trim()
|
||||
.reduce (memo, elem) ->
|
||||
memo[elem[0]] = elem[1]
|
||||
return memo
|
||||
, {}
|
||||
|
||||
# coffeelint: disable=indentation
|
||||
url = switch settings.provider
|
||||
when 'youtube'
|
||||
"http://www.youtube.com/embed/#{settings.id}"
|
||||
when 'vimeo'
|
||||
"https://player.vimeo.com/video/#{settings.id}"
|
||||
# coffeelint: enable=indentation
|
||||
|
||||
return match unless url
|
||||
|
||||
"<div class='videoWrapper'><iframe id='#{settings.provider}#{settings.id}' type='text/html' src='#{url}' frameborder='0'></iframe></div>"
|
||||
|
||||
renderAttachments: (attachments) ->
|
||||
@answerAttachments.html App.view('generic/attachments')(
|
||||
|
|
|
@ -64,6 +64,8 @@ class App.KnowledgeBaseAnswer extends App.Model
|
|||
buttons: [
|
||||
'link'
|
||||
'link_answer'
|
||||
'insert_image'
|
||||
'embed_video'
|
||||
]
|
||||
display: 'Content'
|
||||
tag: 'richtext'
|
||||
|
|
|
@ -1233,3 +1233,18 @@ b {
|
|||
@extend %clickable;
|
||||
}
|
||||
}
|
||||
|
||||
.videoWrapper {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 */
|
||||
padding-top: 25px;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.videoWrapper iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -11622,3 +11622,19 @@ span.is-disabled {
|
|||
.highlight-emulator {
|
||||
background-color: highlight;
|
||||
}
|
||||
|
||||
|
||||
.videoWrapper {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 */
|
||||
padding-top: 25px;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.videoWrapper iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
module KnowledgeBaseRichTextHelper
|
||||
def prepare_rich_text(input)
|
||||
prepare_rich_text_videos(prepare_rich_text_links(input))
|
||||
end
|
||||
|
||||
def prepare_rich_text_links(input)
|
||||
scrubber = Loofah::Scrubber.new do |node|
|
||||
next if node.name != 'a'
|
||||
|
@ -22,4 +26,26 @@ module KnowledgeBaseRichTextHelper
|
|||
|
||||
parsed
|
||||
end
|
||||
|
||||
def prepare_rich_text_videos(input)
|
||||
input.gsub(/\(([\s]*)widget:([\s]*)video[\W]([\s\S])+?\)/) do |match|
|
||||
settings = match
|
||||
.slice(1...-1)
|
||||
.split(',')
|
||||
.map { |pair| pair.split(':').map(&:strip) }
|
||||
.to_h
|
||||
.symbolize_keys
|
||||
|
||||
url = case settings[:provider]
|
||||
when 'youtube'
|
||||
"http://www.youtube.com/embed/#{settings[:id]}"
|
||||
when 'vimeo'
|
||||
"https://player.vimeo.com/video/#{settings[:id]}"
|
||||
end
|
||||
|
||||
return match unless url
|
||||
|
||||
"<div class='videoWrapper'><iframe id='#{settings[:provider]}#{settings[:id]}' type='text/html' src='#{url}' frameborder='0'></iframe></div>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,6 +50,21 @@ Checks if file is used inline
|
|||
parsed = Loofah.scrub_fragment(raw, scrubber).to_s
|
||||
parsed = HtmlSanitizer.strict(parsed)
|
||||
|
||||
scrubber_cleaner = Loofah::Scrubber.new(direction: :bottom_up) do |node|
|
||||
case node.name
|
||||
when 'span'
|
||||
node.children.reject { |t| ["\n", "\r", "\r\n"].include?(t.text) }.each { |child| node.before child }
|
||||
|
||||
node.remove
|
||||
when 'div'
|
||||
node.children.to_a.select { |t| t.text.match?(/\A([\n\r]+)\z/) }.each(&:remove)
|
||||
|
||||
node.remove if node.children.none? && node.classes.none?
|
||||
end
|
||||
end
|
||||
|
||||
parsed = Loofah.scrub_fragment(parsed, scrubber_cleaner).to_s
|
||||
|
||||
(parsed, attachments_inline) = HtmlSanitizer.replace_inline_images(parsed, image_prefix)
|
||||
|
||||
send("#{attr}=", parsed)
|
||||
|
|
|
@ -8,7 +8,7 @@ data-available-locales='<%= @object_locales.map(&:locale).join(',') %>'>
|
|||
</h1>
|
||||
|
||||
<div class="article-content">
|
||||
<%= prepare_rich_text_links @object.translation.content.body_with_urls.html_safe %>
|
||||
<%= prepare_rich_text(@object.translation.content.body_with_urls).html_safe %>
|
||||
</div>
|
||||
|
||||
<% if (attachments = @object.attachments_sorted) && attachments.present? %>
|
||||
|
|
|
@ -2,14 +2,21 @@ FactoryBot.define do
|
|||
factory 'knowledge_base/answer', aliases: %i[knowledge_base_answer] do
|
||||
transient do
|
||||
add_translation { true }
|
||||
translation_traits { [] }
|
||||
end
|
||||
|
||||
category { create(:knowledge_base_category) }
|
||||
|
||||
before(:create) do |answer|
|
||||
before(:create) do |answer, context|
|
||||
next if answer.translations.present?
|
||||
|
||||
answer.translations << build('knowledge_base/answer/translation', answer: answer)
|
||||
answer.translations << build('knowledge_base/answer/translation', *context.translation_traits, answer: answer)
|
||||
end
|
||||
|
||||
trait :with_video do
|
||||
transient do
|
||||
translation_traits { [:with_video] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,5 +24,9 @@ FactoryBot.define do
|
|||
translation.kb_locale = translation.answer.category.knowledge_base.kb_locales.first
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_video do
|
||||
content { build(:knowledge_base_answer_translation_content, :with_video) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,5 +8,9 @@ FactoryBot.define do
|
|||
create(:knowledge_base_answer_translation, content: content)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_video do
|
||||
body { '( widget: video, provider: youtube, id: vTTzwJsHpU8 )' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,6 +23,10 @@ RSpec.shared_context 'basic Knowledge Base', current_user_id: 1 do
|
|||
create(:knowledge_base_answer, category: category, published_at: 1.week.ago)
|
||||
end
|
||||
|
||||
let :published_answer_with_video do
|
||||
create(:knowledge_base_answer, :with_video, category: category, published_at: 1.week.ago)
|
||||
end
|
||||
|
||||
let :internal_answer do
|
||||
create(:knowledge_base_answer, category: category, internal_at: 1.week.ago)
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe 'Knowledge Base Locale Answer Edit', type: :system, authenticated
|
|||
published_answer && draft_answer && internal_answer
|
||||
end
|
||||
|
||||
it 'when entering a long text' do
|
||||
it 'wraps long texts' do
|
||||
long_string = '3KKFA9DAWE9VJYNNnpYRRtMwfa168O1yvpD2t9QXsfb3cppGV6KZ12q0UUJIy5r4Exfk18GnWPR0A3SoDsjxIHz1Gcu4aCEVzenilSOu4gAfxnB6k3mSBUOGIfdgChEBYhcHGgiCmV2EoXu4gG7GAJxKJhM2d4NUiL5RZttGtMXYYFr2Jsg7MV7xXGcygnsLMYqnwzOJxBK0vH3fzhdIZd6YrqR3fggaY0RyKtVigOBZ2SETC8s238Z9eDL4gfUW'
|
||||
|
||||
visit "#knowledge_base/#{knowledge_base.id}/locale/#{primary_locale.system_locale.locale}/answer/#{draft_answer.id}/edit"
|
||||
|
@ -19,4 +19,31 @@ RSpec.describe 'Knowledge Base Locale Answer Edit', type: :system, authenticated
|
|||
expect(page).to have_css('.page-header-title') { |elem| !elem.obscured? }
|
||||
end
|
||||
end
|
||||
|
||||
context 'embedded video' do
|
||||
|
||||
it 'has adding functionality' do
|
||||
visit "#knowledge_base/#{knowledge_base.id}/locale/#{primary_locale.system_locale.locale}/answer/#{published_answer.id}/edit"
|
||||
|
||||
sleep 3 # wait for popover killer to pass
|
||||
|
||||
find('a[data-action="embed_video"]').click
|
||||
|
||||
within('.popover-content') do
|
||||
find('input').fill_in with: 'https://www.youtube.com/watch?v=vTTzwJsHpU8'
|
||||
find('[type=submit]').click
|
||||
end
|
||||
|
||||
within('.richtext-content') do
|
||||
expect(page).to have_text('( widget: video, provider: youtube, id: vTTzwJsHpU8 )')
|
||||
end
|
||||
end
|
||||
|
||||
it 'loads stored' do
|
||||
visit "#knowledge_base/#{knowledge_base.id}/locale/#{primary_locale.system_locale.locale}/answer/#{published_answer_with_video.id}"
|
||||
|
||||
iframe = find('iframe')
|
||||
expect(iframe['src']).to start_with('http://www.youtube.com/embed/')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
19
spec/system/knowledge_base_public/answer_spec.rb
Normal file
19
spec/system/knowledge_base_public/answer_spec.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Public Knowledge Base answer', type: :system, authenticated: false do
|
||||
include_context 'basic Knowledge Base'
|
||||
|
||||
context 'video content' do
|
||||
|
||||
before do
|
||||
published_answer_with_video
|
||||
end
|
||||
|
||||
it 'shows video player' do
|
||||
visit help_answer_path(primary_locale.system_locale.locale, category, published_answer_with_video)
|
||||
|
||||
iframe = find('iframe')
|
||||
expect(iframe['src']).to start_with('http://www.youtube.com/embed/')
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue