Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Embed not visible in toolbox #11

Open
vsvanshi opened this issue Apr 6, 2019 · 24 comments
Open

Embed not visible in toolbox #11

vsvanshi opened this issue Apr 6, 2019 · 24 comments
Assignees

Comments

@vsvanshi
Copy link

vsvanshi commented Apr 6, 2019

Tried to include the embed plugin in toolbox, Everthing is working without errors, but its not visible in the toolbox.

@TMiller00
Copy link

The toolbox static getter appears to be missing

@dellow
Copy link

dellow commented Apr 12, 2019

Yep same here. Not seeing in toolbox.

@gohabereg
Copy link
Member

Hey there!

Embed is inserted to the page when you paste link to the resource into the paragraph block. There is no icon in toolbox because tool doesn't have UI for "empty" case.

@dellow
Copy link

dellow commented Apr 12, 2019

Oh sweet! It works. My bad I didn't really notice this in the description.

@gohabereg
Copy link
Member

Will add a note about that into README

@dellow
Copy link

dellow commented Apr 12, 2019

It's sort of in there already. I think it's human nature thing because pretty much all the other plugins have a toolbox icon and other editors tend to have an icon, you don't expect this to be different but it works great.

I imagine most people are not reading the description they just want to get this cool tool installed.

Really great work on all of this by the way.

@zhkuskov
Copy link

Please, add icon to toolbox

@silverark
Copy link

Doh, I just tripped over this too. Must read the docs.... must read the docs.

@nikitadesign
Copy link

Посмотрел ссылку на документацию, но к сожалению, не понятно как установить иконку. Можно привести пример, того как это сделать?

@neSpecc
Copy link
Contributor

neSpecc commented Sep 7, 2019

Посмотрел ссылку на документацию, но к сожалению, не понятно как установить иконку. Можно привести пример, того как это сделать?

можно форкнуть плагин, добавить в метод render какой-то интерфейс, который будет отображаться по клику на иконку, затем уже определить иконку с помощью свойства toolbox.icon.

@darius00klokj
Copy link

Hi, i was able to embed youtube by placing the URL, but soundcloud is not supported. So when i try pasting the embed code provided by SC, which uses an iframe, it does not allow me. Embed should only remove iframe when the src is supported right?
Screenshot 2019-11-07 09 30 49

@huevoncito
Copy link

@gohabereg This still doesn't work for me for some reason. When I paste a youtube link into the paragraph text it saves as normal text (i.e. { type: "paragraph", data: { text: "https://youtube.com/watch?...." } }

I notice the same thing happens in the demo ( although the demo outputs a <a> elem on paste, whereas my version just shows text. )

Am I missing something?

Thanks again for the fantastic work on this editor and the plugin system. It's great stuff.

@tom-byrom
Copy link

No icon is a UX nightmare!! Cannot understand why it's not there.

The people using this are likely users on the front end, who aren't expected to read documentation on how to use, heck they wont even know what they are using apart from nice front end form.

I think, as someone above states the first place they will look to embed something is in the toolbar where everything else is? Maybe a nice box to paste a link would suffice?

I think the fact that code literate people struggle to realise this tells you that normal users would suffer also....

@sosie-js
Copy link

sosie-js commented Sep 5, 2020

In fact what it is missing it a static toolbar always shown for embed inline or block injections, let's say it is a menubar with item buttons to inject embed blocks. This is not handled by the actual toolbar because the actual toolbar for inline stuff is shown and applies on text selection. Then the request is similar to issue insert() embed programmatically #16 . I replied there.

@charlower
Copy link

Damn, I was stuck on this for HOURS. Would be great if there was an icon in the toolbox - easy to miss in the documentation, also a bit confusing from a UX perspective.

@sandrotanner
Copy link

my humble opinion: even though it technically provides no advantage you should still add the possibility for inlineToolbox icon, since many non-powerusers will otherwise instantly forget that the feature is there.

please merge #71 or provide a better reason why you don't want to

thanks :)

@oliverstasa
Copy link

if anyone's still dealing with this & it hasn't been fixed yet, just in case - adding the toolbox data step by step:

  • have git and yarn installed
  • git clone the repository url
  • yarn install
  • open src/index.js
  • add
    /**
    * data for Toolbox
    * @returns {{icon: string, title: string}}
    */
    static get toolbox() {
    return {
    icon: '<svg></svg>', // your SVG icon here
    title: 'Embed'
    }
    }
    and save the file
  • yarn build
  • include ./dist/bundle.js to your working file: import Embed from './embed/dist/bundle.js'; or <script src="./embed/dist/bundle.js"></script> and set const Embed = window.Embed;
  • use in new EditorJS(): ..., tools: {..., embed: {class: Embed, ...}, ...}, ...

@ponchautf
Copy link

This is a great plugin but the way embed actually works is not user friendly.

(keep in mind that most user ARE NOT developers, they just want button & click, for some of them event a copy paste is difficult)

From an end-user perspective, it's always best that inserting some content in a text editor is an action that respond to a button click.
If they past an url, they expect that the url is displayed in the text, not a block with a video preview.

Add an option to allow inserting an icon that display a text field where the user can paste URL will be fine and surely improve that way your plugin is used (and amount of user also)

@thinkdj
Copy link

thinkdj commented Dec 27, 2022

Agree with majority of the commenters here. It's not an optimal experience that every other tool is available as a toolbar icon, and embed is like an 'aha moment'. Most users wont paste the URL in the paragraph expecting it to turn into an embed, they might skip posting it altogether.

It would be great to have a toolbar icon for embed which brings up something like an overlay with an input and some description like supported sites etc. The user can paste the URL and on some action, the overlay disappears, and the same experience as it is now continues.

@GeorgNation
Copy link

Use this fix if you get an error when entering yarn build: expo/expo-cli#4619

@sethaddison
Copy link

sethaddison commented Oct 5, 2023

For those interested, adding a title and icon in the toolbox introduces a bug. By instantiating the embed plugin through the toolbox, rather than through a paste event in the paragraph plugin, the editor builds an empty embed object... resulting in empty frames in your front end. I wonder why there's no validation method in this embed plugin? The embed plugin also uses deprecated api methods, so hopefully they'll rebuild this soon and account for the "missing ui".

@shunMB
Copy link

shunMB commented Dec 14, 2023

Can't believe still the toolbox is not available(almost 5 years passes!).
For anyone thinking using this package, I just rewrote index.js, and this is example.

result:

9e36268e-1c77-482e-8bd8-6f0a1983b37c.mp4

index.js:

import SERVICES from './services';
import './index.css';
import { debounce } from 'debounce';

/**
 * @typedef {object} EmbedData
 * @description Embed Tool data
 * @property {string} service - service name
 * @property {string} url - source URL of embedded content
 * @property {string} embed - URL to source embed page
 * @property {number} [width] - embedded content width
 * @property {number} [height] - embedded content height
 * @property {string} [caption] - content caption
 */
/**
 * @typedef {object} PasteEvent
 * @typedef {object} HTMLElement
 * @typedef {object} Service
 * @description Service configuration object
 * @property {RegExp} regex - pattern of source URLs
 * @property {string} embedUrl - URL scheme to embedded page. Use '<%= remote_id %>' to define a place to insert resource id
 * @property {string} html - iframe which contains embedded content
 * @property {Function} [id] - function to get resource id from RegExp groups
 */
/**
 * @typedef {object} EmbedConfig
 * @description Embed tool configuration object
 * @property {object} [services] - additional services provided by user. Each property should contain Service object
 */

/**
 * @class Embed
 * @classdesc Embed Tool for Editor.js 2.0
 *
 * @property {object} api - Editor.js API
 * @property {EmbedData} _data - private property with Embed data
 * @property {HTMLElement} element - embedded content container
 *
 * @property {object} services - static property with available services
 * @property {object} patterns - static property with patterns for paste handling configuration
 */
export default class Embed {
  /**
   * @param {{data: EmbedData, config: EmbedConfig, api: object}}
   *   data — previously saved data
   *   config - user config for Tool
   *   api - Editor.js API
   *   readOnly - read-only mode flag
   */
  constructor({ data, api, readOnly }) {
    this.api = api;
    this._data = {};
    this.element = null;
    this.readOnly = readOnly;

    this.data = data;
  }

  /**
   * Static getter for representing the tool in the toolbox.
   */
  static get toolbox() {
    return {
      title: 'Embed',
      icon: '<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 1 0-6M14 11a5 5 0 0 1 0 6"></path><line x1="14" y1="7" x2="14" y2="7"></line><line x1="10" y1="17" x2="10" y2="17"></line></svg>'
    };
  }

  /**
   * @param {EmbedData} data - embed data
   * @param {RegExp} [data.regex] - pattern of source URLs
   * @param {string} [data.embedUrl] - URL scheme to embedded page. Use '<%= remote_id %>' to define a place to insert resource id
   * @param {string} [data.html] - iframe which contains embedded content
   * @param {number} [data.height] - iframe height
   * @param {number} [data.width] - iframe width
   * @param {string} [data.caption] - caption
   */
  set data(data) {
    if (!(data instanceof Object)) {
      throw Error('Embed Tool data should be object');
    }

    const { service, source, embed, width, height, caption = '' } = data;

    this._data = {
      service: service || this.data.service,
      source: source || this.data.source,
      embed: embed || this.data.embed,
      width: width || this.data.width,
      height: height || this.data.height,
      caption: caption || this.data.caption || '',
    };

    const oldView = this.element;

    if (oldView) {
      oldView.parentNode.replaceChild(this.render(), oldView);
    }
  }

  /**
   * @returns {EmbedData}
   */
  get data() {
    if (this.element) {
      const caption = this.element.querySelector(`.${this.api.styles.input}`);

      this._data.caption = caption ? caption.innerHTML : '';
    }

    return this._data;
  }

  /**
   * Get plugin styles
   *
   * @returns {object}
   */
  get CSS() {
    return {
      baseClass: this.api.styles.block,
      input: this.api.styles.input,
      container: 'embed-tool',
      containerLoading: 'embed-tool--loading',
      preloader: 'embed-tool__preloader',
      caption: 'embed-tool__caption',
      url: 'embed-tool__url',
      content: 'embed-tool__content',
    };
  }

  /**
   * Render Embed tool content
   *
   * @returns {HTMLElement}
   */
  render() {
    this.wrapper = document.createElement('div');

    this.serviceSelector = this.createServiceSelector();
    this.wrapper.appendChild(this.serviceSelector);

    this.form = this.createEmbedForm();
    this.wrapper.appendChild(this.form);

    if (this.data.service) {
      const embedContainer = this.createEmbedContainer(this.data);
      this.wrapper.appendChild(embedContainer);
    }

    return this.wrapper;
  }

  /**
   * Create a service selector UI.
   * 
   * @returns {HTMLElement} - Service selector element.
   */
  createServiceSelector() {
    const selector = document.createElement('div');
    selector.innerHTML = 'Select a service: <br>';

    const services = Object.keys(Embed.services);
    services.forEach(service => {
      const button = document.createElement('button');
      button.innerText = service;
      button.addEventListener('click', () => {
        this.selectService(service);
      });
      selector.appendChild(button);
    });

    return selector;
  }

  /**
   * Method to update the form based on the selected service.
   * 
   * @param {string} service - Selected service name.
   */
  selectService(service) {
    this.selectedService = service;
    this.form.style.display = 'block';
  }

  /**
   * Create the embed form.
   * 
   * @returns {HTMLElement} - Embed form element.
   */
  createEmbedForm() {
    const form = document.createElement('form');
    form.style.display = 'none';

    const input = document.createElement('input');
    input.placeholder = 'Paste embed URL here...';
    form.appendChild(input);

    const saveButton = document.createElement('button');
    saveButton.type = 'submit';
    saveButton.innerText = 'Save';
    form.appendChild(saveButton);

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      this.saveEmbed(input.value);
    });

    return form;
  }

  /**
   * Save the entered URL and generate embed content.
   * 
   * @param {string} url - User-entered URL.
   */
  saveEmbed(url) {
    if (!url) {
      return;
    }

    const service = this.detectService(url);
    if (!service) {
      return;
    }

    if (this.form && this.form.parentNode) {
      this.form.parentNode.removeChild(this.form);
    }
    if (this.serviceSelector && this.serviceSelector.parentNode) {
      this.serviceSelector.parentNode.removeChild(this.serviceSelector);
    }

    this.handleEmbed(service, url);
  }


  /**
   * Method for processing embed depending on the service.
   * 
   * @param {string} service - Service name.
   * @param {string} url - Embed URL.
   */
  handleEmbed(service, url) {
    const embedData = this.constructEmbedData(service, url);
    this.data = embedData;
    const embedContent = this.createEmbedContent(embedData);
    this.appendEmbedContent(embedContent);
  }


  /**
   * Append embed content to the container.
   * 
   * @param {HTMLElement} content - Embed content to append.
   */
  appendEmbedContent(content) {
    let embedContainer = this.wrapper.querySelector('.embed-container');

    if (!embedContainer) {
      embedContainer = this.createEmbedContainer();
      this.wrapper.appendChild(embedContainer);
    }

    embedContainer.innerHTML = '';

    embedContainer.appendChild(content);
  }


  /**
   * Detect the service based on the URL.
   * 
   * @param {string} url - User-entered URL.
   * @returns {string|null} - Detected service name or null.
   */
  detectService(url) {
    const serviceNames = Object.keys(SERVICES);
    for (let i = 0; i < serviceNames.length; i++) {
      const serviceName = serviceNames[i];
      const service = SERVICES[serviceName];
      if (service.regex.test(url)) {
        return serviceName;
      }
    }
    return null;
  }

  /**
   * Generate embed data
   * based on service name and URL.
   *
   * @param {string} serviceName - Service name.
   * @param {string} url - Content URL to be embedded.
   * @returns {object|null} - Embed data object or null.
   */
  constructEmbedData(serviceName, url) {
    const service = SERVICES[serviceName];
    const match = service.regex.exec(url);
    let remoteId = match[1];

    if (service.id) {
      remoteId = service.id(match);
    }

    if (!remoteId) {
      console.error('Remote ID is undefined for URL:', url);
      return null;
    }

    const embedUrl = service.embedUrl.replace('<%= remote_id %>', remoteId);
    const embedHtml = service.html.replace('<%= remote_id %>', remoteId);

    return {
      service: serviceName,
      source: url,
      embed: embedUrl,
      html: embedHtml,
      width: service.width || '100%',
      height: service.height || 'auto'
    };
  }

  /**
   * Create the container for embed content.
   *
   * @param {object} embedData - Embed data for the content.
   * @returns {HTMLElement} - Container element for embed content.
   */
  createEmbedContainer(embedData) {
    const container = document.createElement('div');
    container.classList.add('embed-container');

    if (embedData) {
      const embedContent = this.createEmbedContent(embedData);
      container.appendChild(embedContent);
    }

    return container;
  }

  /**
   * Generate embed content based on embed data.
   *
   * @param {object} embedData - Embed data.
   * @returns {HTMLElement} - Generated embed content element.
   */
  createEmbedContent(embedData) {
    const service = SERVICES[embedData.service];

    if (!service || !service.html) {
      console.error('Service HTML template is not defined for', embedData.service);
      return null;
    }

    const template = document.createElement('template');
    template.innerHTML = service.html.trim();
    const iframe = template.content.firstChild;

    if (service.embedUrl && service.id) {
      const remoteId = service.id(embedData.source.match(service.regex).slice(1));
      iframe.src = service.embedUrl.replace('<%= remote_id %>', remoteId);
    } else {
      console.error('embedUrl or id function is not defined for', embedData.service);
      return null;
    }

    iframe.width = embedData.width || '100%';
    iframe.height = embedData.height || 'auto';

    return iframe;
  }


  /**
   * Creates preloader to append to container while data is loading
   *
   * @returns {HTMLElement}
   */
  createPreloader() {
    const preloader = document.createElement('preloader');
    const url = document.createElement('div');

    url.textContent = this.data.source;

    preloader.classList.add(this.CSS.preloader);
    url.classList.add(this.CSS.url);

    preloader.appendChild(url);

    return preloader;
  }

  /**
   * Save current content and return EmbedData object
   *
   * @returns {EmbedData}
   */
  save() {
    return this.data;
  }

  /**
   * Handle pasted url and return Service object
   *
   * @param {PasteEvent} event - event with pasted data
   */
  /*
  onPaste(event) {
    const { key: service, data: url } = event.detail;

    const { regex, embedUrl, width, height, id = (ids) => ids.shift() } = Embed.services[service];
    const result = regex.exec(url).slice(1);
    const embed = embedUrl.replace(/<%= remote_id %>/g, id(result));

    this.data = {
      service,
      source: url,
      embed,
      width,
      height,
    };
  }
  */

  /**
   * Analyze provided config and make object with services to use
   *
   * @param {EmbedConfig} config - configuration of embed block element
   */
  static prepare({ config = {} }) {
    const { services = {} } = config;

    let entries = Object.entries(SERVICES);

    const enabledServices = Object
      .entries(services)
      .filter(([key, value]) => {
        return typeof value === 'boolean' && value === true;
      })
      .map(([key]) => key);

    const userServices = Object
      .entries(services)
      .filter(([key, value]) => {
        return typeof value === 'object';
      })
      .filter(([key, service]) => Embed.checkServiceConfig(service))
      .map(([key, service]) => {
        const { regex, embedUrl, html, height, width, id } = service;

        return [key, {
          regex,
          embedUrl,
          html,
          height,
          width,
          id,
        }];
      });

    if (enabledServices.length) {
      entries = entries.filter(([key]) => enabledServices.includes(key));
    }

    entries = entries.concat(userServices);

    Embed.services = entries.reduce((result, [key, service]) => {
      if (!(key in result)) {
        result[key] = service;

        return result;
      }

      result[key] = Object.assign({}, result[key], service);

      return result;
    }, {});

    Embed.patterns = entries
      .reduce((result, [key, item]) => {
        result[key] = item.regex;

        return result;
      }, {});
  }

  /**
   * Check if Service config is valid
   *
   * @param {Service} config - configuration of embed block element
   * @returns {boolean}
   */
  static checkServiceConfig(config) {
    const { regex, embedUrl, html, height, width, id } = config;

    let isValid = regex && regex instanceof RegExp &&
      embedUrl && typeof embedUrl === 'string' &&
      html && typeof html === 'string';

    isValid = isValid && (id !== undefined ? id instanceof Function : true);
    isValid = isValid && (height !== undefined ? Number.isFinite(height) : true);
    isValid = isValid && (width !== undefined ? Number.isFinite(width) : true);

    return isValid;
  }

  /**
   * Paste configuration to enable pasted URLs processing by Editor
   *
   * @returns {object} - object of patterns which contain regx for pasteConfig
   */
  static get pasteConfig() {
    return {
      patterns: Embed.patterns,
    };
  }

  /**
   * Notify core that read-only mode is supported
   *
   * @returns {boolean}
   */
  static get isReadOnlySupported() {
    return true;
  }
}

// Attach Embed class to the global object
window.Embed = Embed;

// Initialize EditorJS on DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
  const editor = new EditorJS({
    holder: 'editorjs',
    autofocus: true,
    tools: {
      embed: Embed
    }
  });
});

FYI: my code is just for POC.
I’m glad if this can help someone.

@bettysteger
Copy link

I've also overwritten the original Embed Block Tool to show an input field where you can paste the URL!

you must also overwrite the render function, and add a validate function (otherwise empty embed blocks will be saved)!

I just did this here: https://gist.github.com/bettysteger/9d613904c2b14c15182e6204863b79b3

@Brian290103
Copy link

The solution is straightforward: the Embed tool doesn't show an icon. However, if you paste a link (e.g., a YouTube link) directly into the editor, the YouTube video will be embedded automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests