mgilson
mgilson

Reputation: 309821

How to use links in Medium Editor?

I've been trying out the excellent Medium Editor. The problem that I've been having is that I can't seem to get links to "work".

At the simplest, here's some HTML/JS to use to demonstrate the problem:

HTML:

<html>
<head>
  <script src="//cdn.jsdelivr.net/medium-editor/latest/js/medium-editor.min.js"></script>
  <link rel="stylesheet" href="//cdn.jsdelivr.net/medium-editor/latest/css/medium-editor.min.css" type="text/css" media="screen" charset="utf-8">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/medium-editor/latest/css/themes/beagle.min.css" type="text/css">
</head>
<body>
  <div class='editable'>
    Hello world.  <a href="http://www.google.com">link</a>
  </div>
</body>
</html>

Javascript:

var editor = new MediumEditor('.editable');

This fiddle demonstrates the problem (using the code above).

It seems to me that clicking the link should take me wherever the link's href is targeting. The only way to use the link is to right click and either open in a new tab or new window -- which I don't want to ask my users to do.

I feel like I must be missing something simple in the configuration (either the Anchor Preview Options or the Anchor Form Options). Unfortunately, I'm not seeing it.

In my actual application, I'm not using jQuery, but I am using angularjs. If a strictly Medium Editor answer doesn't exist, I can fall back to using basic JS or anything that angularjs provides.

Upvotes: 18

Views: 5336

Answers (4)

mgilson
mgilson

Reputation: 309821

What I really wanted when I asked the question was behavior similar to Google Docs when in "edit" mode (as described by Nate Mielnik). I opened an issue on the Medium Editor tracker and they decided not to implement it as part of the core medium editor, but they noted that they would be happy to have someone add that functionality as an extension.

So, I decided to implement that functionality as an extension as suggested. It can be found as part of MediumTools1. The project is still in very early stages (e.g. I haven't done anything to make the styling look better, or to use better minifying practices, etc. but we'll happily accept Pull Requests for that).

The guts of the code look like this:

var ClassName = {
  INNER: 'medium-editor-toolbar-anchor-preview-inner',
  INNER_CHANGE: 'medium-editor-toolbar-anchor-preview-inner-change',
  INNER_REMOVE: 'medium-editor-toolbar-anchor-preview-inner-remove'
}

var AnchorPreview = MediumEditor.extensions.anchorPreview;
GdocMediumAnchorPreview = MediumEditor.Extension.extend.call(
  AnchorPreview, {

    /** @override */
    getTemplate: function () {
      return '<div class="medium-editor-toolbar-anchor-preview">' +
        '  <a class="' + ClassName.INNER + '"></a>' +
        '  -' +
        '  <a class="' + ClassName.INNER_CHANGE + '">Change</a>' +
        '  |' +
        '  <a class="' + ClassName.INNER_REMOVE + '">Remove</a>' +
        '</div>';
    },

    /** @override */
    createPreview: function () {
      var el = this.document.createElement('div');

      el.id = 'medium-editor-anchor-preview-' + this.getEditorId();
      el.className = 'medium-editor-anchor-preview';
      el.innerHTML = this.getTemplate();

      var targetBlank =
          this.getEditorOption('targetBlank') ||
          this.getEditorOption('gdocAnchorTargetBlank');
      if (targetBlank) {
        el.querySelector('.' + ClassName.INNER).target = '_blank';
      }

      var changeEl = el.querySelector('.' + ClassName.INNER_CHANGE);
      this.on(changeEl, 'click', this.handleClick.bind(this));

      var unlinkEl = el.querySelector('.' + ClassName.INNER_REMOVE);
      this.on(unlinkEl, 'click', this.handleUnlink.bind(this));

      return el;
    },

    /** Unlink the currently active anchor. */
    handleUnlink: function() {
      var activeAnchor = this.activeAnchor;
      if (activeAnchor) {
        this.activeAnchor.outerHTML = this.activeAnchor.innerHTML;
        this.hidePreview();
      }
    }
  });

As an explanation, I just use medium's flavor of prototypical inheritance to "subclass" the original/builtin AnchorPreview extension. I override the getTemplate method to add the additional links into the markup. Then I borrowed a lot from the base implementation of getPreview, but I bound new actions to each of the links as appropriate. Finally, I needed to have an action for "unlinking" the link when "Remove" is clicked, so I added a method for that. The unlink method could probably be done a little better using contenteditable magic (to make sure that it is part of the browser's undo stack), but I didn't spend the time to figure that out (though it would make a good Pull Request for anyone interested :-).

1Currently, it's the only part, but I hope that'll change at some point. . .

Upvotes: 0

Nate Mielnik
Nate Mielnik

Reputation: 571

So medium-editor is built on top of the built-in browser support for contenteditable elements. When you instantiate medium-editor, it will add the contenteditable=true attribute to whatever element(s) you provided it.

By default, since the text is now editable (the contenteditable attribute makes the browser treat it as WYSIWYG text) the browser no longer supports clicking on the links to navigate. So, medium-editor is not blocking these link clicks from happening, the browsers do it inherently as part of making the text editable.

medium-editor has built in extensions for interacting with links:

  • anchor extension
    • allows for adding/removing links
  • anchor-preview extension
    • shows a tooltip when hovering a link
    • when the tooltip is clicked, allows for editing the href of the link via the anchor extension

I think the underlying goal of the editor is the misunderstanding here. The editor allows for editing text, and in order to add/remove/update links, you need to be able to click into it without automatically navigating away. This is what I think of as 'edit' mode.

However, the html produced as a result of editing is valid html, and if you take that html and put it inside an element that does NOT have the contenteditable=true attribute, everything will work as expected. I think of this as 'publish mode'

I look at editors like word or google docs, and you see a similar kind of behavior where when you edit the document, the links don't just navigate away when you click on them, you have to actually choose to navigate them through a separate action after you click the link. However, on a 'published' version of the document, clicking the link will actually open a browser window and navigate there.

I think this does make for a good suggestion as an enhancement to the existing anchor-preview extension. Perhaps the tooltip that appears when hovering a link could have multiple options in it (ie Edit Link | Remove Link | Navigate to URL).

tldr;

Links are not navigable on click when 'editing' text in a browser via the built-in WYSIWYG support (contenteditable). When not in 'edit' mode, the links will work as expected. This could make for a nice enhancement to the medium-editor anchor-preview extension.

Upvotes: 3

Valijon
Valijon

Reputation: 13103

I've found how to bind event.

Here is full event list https://github.com/yabwe/medium-editor/blob/master/CUSTOM-EVENTS.md

Try to change your code to

var editor = new MediumEditor('.editable')
    .subscribe("editableClick", function(e){if (e.target.href) {window.open(e.target.href)}})

https://jsfiddle.net/fhr18gm1/

Upvotes: 11

mgilson
mgilson

Reputation: 309821

Working off some ideas from @Valijon in the comments, I was able to get it to work using the following code:

var iElement = angular.element(mediumEditorElement);

iElement.on('click', function(event) {
  if (
      event.target && event.target.tagName == 'A' &&
      event.target.href && !event.defaultPrevented) {
    $window.open(event.target.href, '_blank');
  }
});

I think the key is that apparently the editor lets the event propogate to the ancestor elements, so I was able to just listen for the click on the top level editor element.

Here, $window is angular's $window service -- If you're not using angularjs, window would do the trick and I used angular.element to ease the event listener registry, but you could do it the old-fashioned way (or using the JS framework of your choice).

Upvotes: 0

Related Questions