如何在 Summernote 中插入占位符元素?

How to insert a placeholder element in Summernote?

我正在为 Summernote 所见即所得编辑器(版本 0.8.1)开发一个插件,用于将 iframe 元素插入到代码中。

使用提供的示例,我设法在菜单中找到插件按钮,这会打开一个对话框,我可以在其中输入 URL 和标题。源码加个iframe标签没问题,但这不是我想要的

我想在代码中插入一个占位符,带有类似(或类似)的标记:

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

现在,summernote 允许我编辑跨度的内容,但我想要一个不能在编辑器中修改的占位符。

如何在编辑器中插入具有以下属性的占位符:

这是我目前拥有的:

// 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'
    }
});

尝试使用

$(document).ready(function() {
        $('#summernote').summernote({
            placeholder: 'Hello stand alone ui',
            tabsize: 2,
            height: 120
        });
});
<!-- 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/summernote@0.8.18/dist/summernote.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js"></script>


<div id="summernote"></div>

您可以在您的 span 元素上使用 contenteditable 属性,它将起作用并在编辑器中保留 iframe 插件 HTML,它将删除整个块当按下 DelBackspace 键时。

Github repository and there is one that demontrates usage of dialog and popover editing and you can check the logic and code here中有一些演示插件。

createIframeNode中我们创建元素并设置数据属性

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];
};

我们还创建了一个 currentEditing 变量来在弹出菜单弹出时保存光标下的元素,这样弹出对话框就会知道我们正在编辑一个元素而不是创建一个新元素。

updateIframeNode 中,我们使用 currentEditing 元素来更新

这里我们只重新创建 span 元素,因为 currentEditing 是实际的 div.ext-iframe-subst 然后我们更新数据属性:

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);
}

完整的工作插件

运行 代码片段并尝试使用带有方形图标的按钮插入 iframe。您可以编辑现有的 iFrame 元素,块将一起删除。

/**
 * @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/summernote@0.8.18/dist/summernote.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js"></script>
  
<div id="editor">Hello Summernote</div>

默认情况下,当 summernote 创建它的编辑器时 div 包含内容


/p> 所以如果你清空 summernote onload 那么问题就会解决。

有删除内容的代码:

      $(function () {
        $('#summernote').summernote({
            inheritPlaceholder: true,
            placeholder: 'Enter your Inquiry here...',
        });
        $('#summernote').summernote('code', ''); //This line remove summercontent when load
    });