Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to insert a placeholder element in Summernote?

I'm developing a plugin for Summernote WYSIWYG editor (version 0.8.1) to insert iframe elements into the code.

Working with the samples provided, I managed to get the plugin button in the menu, which opens a dialog where I can enter an URL and a title. It's no problem to add an iframe Tag to the source, but this is not what I want.

I want to insert a placeholder into the code, with a markup like (or similar to) this:

<div class="ext-iframe-subst" data-src="http://www.test.example" data-title="iframe title"><span>iframe URL: http://www.test.example</span></div>

Now, summernote lets me edit the contents of the span, but I'd like to have a placeholder instead, that can't be modified in the editor.

How can I insert a placeholder into the editor, that has the following properties:

  • It is editable as a single block (can be deleted with a single delete)
  • On click, I can open a popover similar to the link or image popover to adjust size f.i.
  • The inner content is not modifyable

This is what I have so far:

// Extends plugins for adding iframes.
//  - plugin is external module for customizing.
$.extend($.summernote.plugins, {
  /**
   * @param {Object} context - context object has status of editor.
   */
  'iframe': function (context) {
    var self = this;

    // ui has renders to build ui elements.
    //  - you can create a button with `ui.button`
    var ui = $.summernote.ui;

    var $editor = context.layoutInfo.editor;
    var options = context.options;
    var lang = options.langInfo;

    // add context menu button
    context.memo('button.iframe', function () {
      return ui.button({
        contents: '<i class="fa fa-newspaper-o"/>',
        tooltip: lang.iframe.iframe,
        click: context.createInvokeHandler('iframe.show')
      }).render();
    });


    // This events will be attached when editor is initialized.
    this.events = {
      // This will be called after modules are initialized.
      'summernote.init': function (we, e) {
        console.log('IFRAME initialized', we, e);
      },
      // This will be called when user releases a key on editable.
      'summernote.keyup': function (we, e) {
        console.log('IFRAME keyup', we, e);
      }
    };

    // This method will be called when editor is initialized by $('..').summernote();
    // You can create elements for plugin
    this.initialize = function () {
      var $container = options.dialogsInBody ? $(document.body) : $editor;

      var body = '<div class="form-group row-fluid">' +
          '<label>' + lang.iframe.url + '</label>' +
          '<input class="ext-iframe-url form-control" type="text" />' +
          '<label>' + lang.iframe.title + '</label>' +
          '<input class="ext-iframe-title form-control" type="text" />' +
          '<label>' + lang.iframe.alt + '</label>' +
          '<textarea class="ext-iframe-alt form-control" placeholder="' + lang.iframe.alttext + '" rows=""10""></textarea>' +
          '</div>';
      var footer = '<button href="#" class="btn btn-primary ext-iframe-btn disabled" disabled>' + lang.iframe.insert + '</button>';

      this.$dialog = ui.dialog({
        title: lang.iframe.insert,
        fade: options.dialogsFade,
        body: body,
        footer: footer
      }).render().appendTo($container);
    };

    // This methods will be called when editor is destroyed by $('..').summernote('destroy');
    // You should remove elements on `initialize`.
    this.destroy = function () {
      this.$dialog.remove();
      this.$dialog = null;
    };


    this.bindEnterKey = function ($input, $btn) {
      $input.on('keypress', function (event) {
        if (event.keyCode === 13) { //key.code.ENTER) {
          $btn.trigger('click');
        }
      });
    };



    this.createIframeNode = function (data) {
      var $iframeSubst = $('<div class="ext-iframe-subst"><span>' + lang.iframe.iframe + '</span></div>');

      $iframeSubst.attr("data-src", data.url).attr("data-title", data.title);

      return $iframeSubst[0];
    };


    this.show = function () {
      var text = context.invoke('editor.getSelectedText');
      context.invoke('editor.saveRange');

      console.log("iframe.getInfo: " + text);

      this
        .showIframeDialog(text)
        .then(function (data) {
          // [workaround] hide dialog before restore range for IE range focus
          ui.hideDialog(self.$dialog);
          context.invoke('editor.restoreRange');

          // build node
          var $node = self.createIframeNode(data);

          if ($node) {
            // insert iframe node
            context.invoke('editor.insertNode', $node);
          }
        })
        .fail(function () {
          context.invoke('editor.restoreRange');
        });

    };

    this.showIframeDialog = function (text) {
      return $.Deferred(function (deferred) {
        var $iframeUrl = self.$dialog.find('.ext-iframe-url');
        var $iframeTitle = self.$dialog.find('.ext-iframe-title');
        var $iframeBtn = self.$dialog.find('.ext-iframe-btn');

        ui.onDialogShown(self.$dialog, function () {
          context.triggerEvent('dialog.shown');

          $iframeUrl.val(text).on('input', function () {
            ui.toggleBtn($iframeBtn, $iframeUrl.val());
          }).trigger('focus');

          $iframeBtn.click(function (event) {
            event.preventDefault();

            deferred.resolve({ url: $iframeUrl.val(), title: $iframeTitle.val() });
          });

          self.bindEnterKey($iframeUrl, $iframeBtn);
        });

        ui.onDialogHidden(self.$dialog, function () {
          $iframeUrl.off('input');
          $iframeBtn.off('click');

          if (deferred.state() === 'pending') {
            deferred.reject();
          }
        });

        ui.showDialog(self.$dialog);
      });
    };


  }
});

// add localization texts
$.extend($.summernote.lang['en-US'], {
    iframe: {
      iframe: 'iframe',
      url: 'iframe URL',
      title: 'title',
      insert: 'insert iframe',
      alt: 'Text alternative',
      alttext: 'you should provide a text alternative for the content in this iframe.',
      test: 'Test'
    }
});
like image 803
gpinkas Avatar asked Nov 21 '22 12:11

gpinkas


1 Answers

You can use contenteditable attribute on your span element and it will work and keep the iframe plugin HTML in the editor and it will delete the whole block when hitting Del or Backspace keys.

There are some demo plugins in the Github repository and there is one that demontrates usage of dialog and popover editing and you can check the logic and code here.

In createIframeNode we create the element and set the data attributes

this.createIframeNode = function (data) {
  var $iframeSubst = $(
    '<div class="ext-iframe-subst"><span contenteditable="false">' +
    lang.iframe.url + ': ' + data.url +
    '</span></div>'
  );

  $iframeSubst
    .attr("data-src", data.url)
    .attr("data-title", data.title);

  return $iframeSubst[0];
};

We also create a currentEditing variable to save the element under cursor when the popover menu popups so the opoup dialog will know that we're editing an element and not create a new one.

In updateIframeNode we're using the currentEditing element to update

Here we re-create only the span element because the currentEditing is the actual div.ext-iframe-subst and then we update the data attributes:

this.updateIframeNode = function (data) {
  $(currentEditing).html(
    '<span contenteditable="false">' +
    lang.iframe.url + ': ' + data.url +
    '</span>'
  )

  $(currentEditing)
    .attr("data-src", data.url)
    .attr("data-title", data.title);
}

Full working plugin

Run the code snippet and try to insert iframes using the button with the square icon. You can edit existing iFrame elements and the block deletes all together.

/**
 * @param {Object} context - context object has status of editor.
 */
var iframePlugin = function (context) {
  var self = this;

  // ui has renders to build ui elements.
  //  - you can create a button with `ui.button`
  var ui = $.summernote.ui;
  var dom = $.summernote.dom;

  var $editor = context.layoutInfo.editor;
  var currentEditing = null;
  var options = context.options;
  var lang = options.langInfo;

  // add context menu button
  context.memo('button.iframe', function () {
    return ui.button({
      contents: '<i class="note-icon-frame"/>',
      tooltip: lang.iframe.iframe,
      click: (event) => {
        currentEditing = null;
        context.createInvokeHandler('iframe.show')(event);
      }
    }).render();
  });

  context.memo('button.iframeDialog', function () {
    return ui.button({
      contents: '<i class="note-icon-frame"/>',
      tooltip: lang.iframe.iframe,
      click: (event) => {
        context.createInvokeHandler('iframe.show')(event);
        // currentEditing
      }
    }).render();
  });


  // This events will be attached when editor is initialized.
  this.events = {
    // This will be called after modules are initialized.
    'summernote.init': function (we, e) {
      $('data.ext-iframe', e.editable).each(function() { self.setContent($(this)); });
    },
    // This will be called when user releases a key on editable.
    'summernote.keyup summernote.mouseup summernote.change summernote.scroll': function() {
      self.update();
    },
    'summernote.dialog.shown': function() {
      self.hidePopover();
    },
  };

  // This method will be called when editor is initialized by $('..').summernote();
  // You can create elements for plugin
  this.initialize = function () {
    var $container = options.dialogsInBody ? $(document.body) : $editor;

    var body = '<div class="form-group row-fluid">' +
        '<label>' + lang.iframe.url + '</label>' +
        '<input class="ext-iframe-url form-control" type="text" />' +
        '<label>' + lang.iframe.title + '</label>' +
        '<input class="ext-iframe-title form-control" type="text" />' +
        // '<label>' + lang.iframe.alt + '</label>' +
        // '<textarea class="ext-iframe-alt form-control" placeholder="' + lang.iframe.alttext + '" rows=""10""></textarea>' +
        '</div>';
    var footer = '<button href="#" class="btn btn-primary ext-iframe-btn disabled" disabled>' + lang.iframe.insertOrUpdate + '</button>';

    this.$dialog = ui.dialog({
      title: lang.iframe.insert,
      fade: options.dialogsFade,
      body: body,
      footer: footer
    }).render().appendTo($container);

    // create popover
    this.$popover = ui.popover({
      className: 'ext-iframe-popover',
    }).render().appendTo('body');
    var $content = self.$popover.find('.popover-content');

    context.invoke('buttons.build', $content, options.popover.iframe);
  };

  // This methods will be called when editor is destroyed by $('..').summernote('destroy');
  // You should remove elements on `initialize`.
  this.destroy = function () {
    self.$popover.remove();
    self.$popover = null;
    self.$dialog.remove();
    self.$dialog = null;
  };


  this.bindEnterKey = function ($input, $btn) {
    $input.on('keypress', function (event) {
      if (event.keyCode === 13) { //key.code.ENTER) {
        $btn.trigger('click');
      }
    });
  };

  self.update = function() {
    // Prevent focusing on editable when invoke('code') is executed
    if (!context.invoke('editor.hasFocus')) {
      self.hidePopover();
      return;
    }

    var rng = context.invoke('editor.createRange');
    var visible = false;
    var $data = $(rng.sc).closest('div.ext-iframe-subst');

    if ($data.length) {
      currentEditing = $data[0];
      var pos = dom.posFromPlaceholder(currentEditing);
      const containerOffset = $(options.container).offset();
      pos.top -= containerOffset.top;
      pos.left -= containerOffset.left;

      self.$popover.css({
        display: 'block',
        left: pos.left,
        top: pos.top,
      });

      // save editor target to let size buttons resize the container
      context.invoke('editor.saveTarget', currentEditing);

      visible = true;
    }

    // hide if not visible
    if (!visible) {
      self.hidePopover();
    }
  };

  self.hidePopover = function() {
    self.$popover.hide();
  };

  this.createIframeNode = function (data) {
    var $iframeSubst = $(
      '<div class="ext-iframe-subst"><span contenteditable="false">' +
      lang.iframe.url + ': ' + data.url +
      '</span></div>'
    );

    $iframeSubst.attr("data-src", data.url).attr("data-title", data.title);
    return $iframeSubst[0];
  };

  this.updateIframeNode = function (data) {
    $(currentEditing).html(
      '<span contenteditable="false">' +
      lang.iframe.url + ': ' + data.url +
      '</span>'
    )

    $(currentEditing).attr("data-src", data.url).attr("data-title", data.title);
  }

  this.show = function () {
    var text = context.invoke('editor.getSelectedText');
    context.invoke('editor.saveRange');

    this
      .showIframeDialog(text)
      .then(function (data) {
        // [workaround] hide dialog before restore range for IE range focus
        ui.hideDialog(self.$dialog);
        context.invoke('editor.restoreRange');

        if (currentEditing) {
          self.updateIframeNode(data);
        } else {
          // build node
          var $node = self.createIframeNode(data);

          if ($node) {
            // insert iframe node
            context.invoke('editor.insertNode', $node);
          }
        }
      })
      .fail(function () {
        context.invoke('editor.restoreRange');
      });
  };

  this.showIframeDialog = function (text) {
    return $.Deferred(function (deferred) {
      var $iframeUrl = self.$dialog.find('.ext-iframe-url');
      var $iframeTitle = self.$dialog.find('.ext-iframe-title');
      var $iframeBtn = self.$dialog.find('.ext-iframe-btn');

      ui.onDialogShown(self.$dialog, function () {
        context.triggerEvent('dialog.shown');

        var dataSrc = currentEditing ? $(currentEditing).attr('data-src') : '';
        var dataTitle = currentEditing ? $(currentEditing).attr('data-title') : '';

        $iframeTitle.val(dataTitle);
        $iframeUrl.val(dataSrc).on('input', function () {
          ui.toggleBtn($iframeBtn, $iframeUrl.val());
        }).trigger('focus');

        $iframeBtn.click(function (event) {
          event.preventDefault();

          deferred.resolve({ url: $iframeUrl.val(), title: $iframeTitle.val() });
        });

        self.bindEnterKey($iframeUrl, $iframeBtn);
      });

      ui.onDialogHidden(self.$dialog, function () {
        $iframeUrl.off('input');
        $iframeBtn.off('click');

        if (deferred.state() === 'pending') {
          deferred.reject();
        }
      });

      ui.showDialog(self.$dialog);
    });
  };
}

// Extends plugins for adding iframes.
//  - plugin is external module for customizing.
$.extend(true, $.summernote, {
  plugins: {
    iframe: iframePlugin,
  },
  options: {
    popover: {
      iframe: [
        ['iframe', ['iframeDialog']],
      ],
    },
  },
  lang: {
    'en-US': {
      iframe: {
        iframe: 'iframe',
        url: 'iframe URL',
        title: 'title',
        insertOrUpdate: 'insert/update iframe',
        alt: 'Text alternative',
        alttext: 'you should provide a text alternative for the content in this iframe.',
        test: 'Test',
      },
    },
  },
});

$(document).ready(function() {
  $('#editor').summernote({
    height: 200,
    toolbar: [
      ['operation', ['undo', 'redo']],
      ['style', ['bold', 'italic', 'underline']],
      ['color', ['color']],       
      ['insert', ['iframe', 'link','picture', 'hr']],
      ['view', ['codeview']],
     ],
  });
});
<!-- include libraries(jQuery, bootstrap) -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>

<!-- include summernote css/js -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote.min.js"></script>
  
<div id="editor">Hello Summernote</div>
like image 127
Christos Lytras Avatar answered Feb 24 '23 04:02

Christos Lytras