ckeditor5 icon indicating copy to clipboard operation
ckeditor5 copied to clipboard

Add support for asynchronous oEmbed services

Open Reinmar opened this issue 6 years ago • 10 comments

So far we implemented a support only for synchronous media preview generation. This means that we can preview things like YT and Vimeo, for which we can predict the <iframe>s code, but we cannot provide previews for Twitter, Google maps, etc., where we'd have to query external services. Placeholders are displayed for this kind of medias.

Media with previews:

image

Media without previews:

image

The next step here would be to allow getting previews in an asynchronous way so can show previews of all possible services.

A possible simplification of this task would be implementing this only for the config.mediaEmbed.semanticDataOutput option because in this option nothing bad happens if the users closes the app before all previews are retrieved. However, with our PendingActions plugin, handling the non-semantic case should be fairly easy too.


If you'd like to see this feature implemented react with :+1: to this post.

Reinmar avatar Aug 24 '18 12:08 Reinmar

I'm looking at doing something like this personally, and I have an existing media embed endpoint on my application's backend. Would it be relatively straightforward to allow writing a provider definition where the html function is asynchronous? This would leave it up to the user to manage whatever API calls they need to do in there and return a string once everything is all set. For my use-case I need to support embedding any type of resource that the user pastes in, and there needs to be a preview which comes up right away for it. What I'd love to be able to do is something like:

{
  url: /.+/,
  html: async match => {
    const url = match[0]
    const embedResponse = await fetch(embedEndpointURL, url)
    return renderEmbed(embedResponse)
  }
}

where renderEmbed returns a string.

If it would be a relatively small change to support this I'd be willing to take a shot at opening a PR to do so, provided you all are open to that :)

alicewriteswrongs avatar Jan 23 '19 16:01 alicewriteswrongs

actually, you can feel free to ignore my comment, I figured out another way to do it. Thanks!

alicewriteswrongs avatar Jan 23 '19 18:01 alicewriteswrongs

actually, you can feel free to ignore my comment, I figured out another way to do it. Thanks!

@aliceriot What did you come up with? What does it work like?

oleq avatar Jan 31 '19 09:01 oleq

@aliceriot could you please share your solution.

davidmuchiri avatar Oct 31 '19 11:10 davidmuchiri

@aliceriot I would like to know the solution too

hielfx avatar Dec 05 '19 16:12 hielfx

sure, the code for it is open source, so you can read the whole implementation. I'm using an embed service called embedly. they have basically two products, one is a REST API and the other is a JS bundle you include on the page via a script tag. My original goal was to use the API, because we were using that elsewhere on the same site, but I ended up for just the CKEditor implementation using the 'Cards' product.

The documentation for the embedly product is here: https://docs.embed.ly/docs/cards

Basically once you include it on the page it will find any <a> tag with class .embedly-card and rewrite that into a preview of the url on the href of your <a> tag. So I was able to write my own synchronous provider for CKEditor that just returns a properly-formatted <a> tag and then the embedly JS rewrites it into a card for me.

Here's my ckeditor code: https://github.com/mitodl/open-discussions/blob/master/static/js/components/ArticleEditor.js

It's a React component, but if you're not familiar with React this is the important bit:

const editor = await CustomEditor.create(initialData || [], {
  mediaEmbed: {
    previewsInData: true,
    providers: [
      {
        name: "embedly",
        url: /.+/,
        html: match => {
          const url = match[0]

          // we'll render this to a string because CKEditor
          // doesn't support any other return value for this function.
          // the embed.ly platform JS finds elements with the .embedly-card
          // class and turns them into embed cards
          return renderEmbedlyCard(url)
        }
      }
    ]
  }
})

the renderEmbedlyCard function looks like this:

export const renderEmbedlyCard = (url: string): string =>
  ReactDOMServer.renderToStaticMarkup(
    <a
      data-card-chrome="0"
      data-card-controls="0"
      data-card-key={SETTINGS.embedlyKey}
      href={url}
      className="embedly-card"
    />
  )

So basically when I instantiate the Editor object for ckeditor I pass in my custom provider for mediaEmbed. This more or less takes in a URL and then returns a static HTML string, which I'm producing using React (yea that's a bit overkill but w/e). The <a> tag is what will be rendered in the editor, and it will then be rewritten by the embedly platform. The data-card-chrome things are some configuration values for how embedly ends up rendering the card.

So that's about it. The solution that I came up with ended up being not nearly as React-ey or integrated as I wanted, but this has been working reliable for a while.

I should note that in my application I'm also using ckeditor to display stored documents later, so this solution works there too. But if you needed to save to html, markdown, or something else and render the output outside of the context of ckeditor you would need to take a different approach.

Hope that's somewhat helpful!

alicewriteswrongs avatar Dec 05 '19 17:12 alicewriteswrongs

@aliceriot I know this is old but can you explain how "ReactDOMServer.renderToStaticMarkup" works for a non React user?

As far as i'm aware platform.js doesn't pick up new html that's injected into the dom, but i'm guessing this "renderToStaticMarkup" method somehow resolves that?

Is there a way to get this working with vanilla javascript and the embed.ly platform?

robjbrain avatar Feb 25 '21 01:02 robjbrain

@robjbrain hey! it is indeed old but it still goes to my email so here we are, haha

ReactDOMServer.renderToStaticMarkup basically takes a React component and renders it out to a plain HTML string. The <a> tag that you see being passed in above is actually a React component, but the return value of the function will look basically like that. I only used it to take advantage of JSX for setting attributes on the <a> tag.

You could write a vanilla JS function like this that served the same purpose:

const renderEmbedlyCard = (url) => (
    `<a
      data-card-chrome="0"
      data-card-controls="0"
      data-card-key="${embedlyKey}"
      href="${url}"
      class="embedly-card"
    />`
 )

As to embedly platform.js picking up new additions to the DOM, I'm not sure tbh, it was a few years ago that I wrote this, but I don't think we had to do anything to get it to pick up newly added cards. I took a look through our code really quickly and didn't see anything that looked like it was trying to work around that limitations. But I'm not sure! So maybe.

alicewriteswrongs avatar Feb 25 '21 02:02 alicewriteswrongs

I checked and it does seem to pick up new additions to the DOM.

neongreen avatar Sep 10 '21 13:09 neongreen

Hello,

Are there any updates on this topic? I am interested in managing to insert Twitter into CKEditor in an Angular project.

Thanks

cointreau17 avatar Apr 28 '24 20:04 cointreau17