Quill: Auto format links (on type and paste)

Created on 19 May 2014  路  28Comments  路  Source: quilljs/quill

If I paste http://google.com into a Quill editor, it would be nice if it would (perhaps optionally) create a link out of it.

feature

Most helpful comment

I'm not sure why this issue is closed -- you can use a matcher to autolink pasted URLs but that doesn't help with autolinking while the user is typing. If it helps anyone else, I wrote code to handle both cases.

For pasting, the matcher above got me on the right track but didn't work for pasting more than just an URL, e.g. this is https://www.google.com a string with an URL. I changed it to:

quill.clipboard.addMatcher(Node.TEXT_NODE, function(node, delta) {
  var regex = /https?:\/\/[^\s]+/g;
  if(typeof(node.data) !== 'string') return;
  var matches = node.data.match(regex);

  if(matches && matches.length > 0) {
    var ops = [];
    var str = node.data;
    matches.forEach(function(match) {
      var split = str.split(match);
      var beforeLink = split.shift();
      ops.push({ insert: beforeLink });
      ops.push({ insert: match, attributes: { link: match } });
      str = split.join(match);
    });
    ops.push({ insert: str });
    delta.ops = ops;
  }

  return delta;
});

And for typing, I wrote this:

// Autolink URLs when typing
quill.on('text-change', function(delta, oldDelta, source) {
  var regex = /https?:\/\/[^\s]+$/;
  if(delta.ops.length === 2 && delta.ops[0].retain && isWhitespace(delta.ops[1].insert)) {
    var endRetain = delta.ops[0].retain;
    var text = quill.getText().substr(0, endRetain);
    var match = text.match(regex);

    if(match !== null) {
      var url = match[0];

      var ops = [];
      if(endRetain > url.length) {
        ops.push({ retain: endRetain - url.length });
      }

      ops = ops.concat([
        { delete: url.length },
        { insert: url, attributes: { link: url } }
      ]);

      quill.updateContents({
        ops: ops
      });
    }
  }
});

I haven't tested super extensively but that seems to do the trick.

All 28 comments

It should already preserve pasted links. If I highlight, copy and paste your comment into quilljs.com the link is preserved. Feel free to reopen if it's not working in a particular browser or if I misunderstood the question.

Ah, what I was referring to is pasting the raw text 'http://google.com' into the editor, not an already formatted link(which is what Github creates). Basically, my editor isn't exposing the toolbar link button for simplicity, but I still want hand-typed links to be formatted correctly.

I see this applies to both typing and pasted text then. I'll reopen as a feature request and edit the title to make this more explicit.

What's the status on this?

We'll try to use the Milestones feature soon to provide more visibility to current development but this particular feature is not being worked on at the moment.

Hi,
Was this request was ever developed, either in the core code or as a custom module?

Thanks

No I think this is still an open request.

You can add a custom matcher for this behavior in 1.0. One of the unit tests is a useful example for this but of course you might want different logic or regex to determine what a link is.

The custom matcher works for pasting. What's the solution for typing a link?

@jhaddow Do you have a working example of a custom matcher for pasting? I'm struggling to get it working.

@jhchen +1 for having this functionality in the core.

Got a custom matcher working. Documenting it here in case it's helpful to anyone else:

quill_editor.clipboard.addMatcher(Node.TEXT_NODE, function(node, delta) {
  var regex = /https?:\/\/[^\s]+/g;
  if (regex.exec(node.data) != null) {
    delta.ops = [{ insert: node.data, attributes: { link: node.data }}];
  }
  return delta;
});

Note: this is a very minimal implementation (reduced down from the test case linked above) that just solves my use-case. I'm only dealing with single links pasted in and just returning the whole pasted value as a clickable link.

Beware that it overwrites any other ops in delta. This may or may not work for you.

I'm not sure why this issue is closed -- you can use a matcher to autolink pasted URLs but that doesn't help with autolinking while the user is typing. If it helps anyone else, I wrote code to handle both cases.

For pasting, the matcher above got me on the right track but didn't work for pasting more than just an URL, e.g. this is https://www.google.com a string with an URL. I changed it to:

quill.clipboard.addMatcher(Node.TEXT_NODE, function(node, delta) {
  var regex = /https?:\/\/[^\s]+/g;
  if(typeof(node.data) !== 'string') return;
  var matches = node.data.match(regex);

  if(matches && matches.length > 0) {
    var ops = [];
    var str = node.data;
    matches.forEach(function(match) {
      var split = str.split(match);
      var beforeLink = split.shift();
      ops.push({ insert: beforeLink });
      ops.push({ insert: match, attributes: { link: match } });
      str = split.join(match);
    });
    ops.push({ insert: str });
    delta.ops = ops;
  }

  return delta;
});

And for typing, I wrote this:

// Autolink URLs when typing
quill.on('text-change', function(delta, oldDelta, source) {
  var regex = /https?:\/\/[^\s]+$/;
  if(delta.ops.length === 2 && delta.ops[0].retain && isWhitespace(delta.ops[1].insert)) {
    var endRetain = delta.ops[0].retain;
    var text = quill.getText().substr(0, endRetain);
    var match = text.match(regex);

    if(match !== null) {
      var url = match[0];

      var ops = [];
      if(endRetain > url.length) {
        ops.push({ retain: endRetain - url.length });
      }

      ops = ops.concat([
        { delete: url.length },
        { insert: url, attributes: { link: url } }
      ]);

      quill.updateContents({
        ops: ops
      });
    }
  }
});

I haven't tested super extensively but that seems to do the trick.

@schneidmaster Thanks for the code. The pasting code is working perfectly, but I couldn't make it work while typing. Could you please look into that?

Update: Copy-paste in your code is working only for the first copy, the second time I try to copy something, the paste doesn't work.

@Masum06 My code is working fine for me, both when typing a link and when pasting multiple times.

I'd suggest making sure you're using a recent version of quill (I'm on 1.1.3) but I posted the code as a general favor to folks trying to solve this problem; I'm not going to debug your specific setup & use case.

@schneidmaster thanks for sharing the code! I got it to work. The Autolink URLs when typing did not work at first because isWhitespace function was not included. I added the below isWhitespace function then it worked fine. @Masum06 you might give this a try.

function isWhitespace(ch) {
  var whiteSpace = false
  if ((ch == ' ') || (ch == '\t') || (ch == '\n')) {
    whiteSpace = true;
  }
  return whiteSpace;
},

Oh yeah, I imported isWhitespace from the is-whitespace npm package (e.g. import isWhitespace from 'is-whitespace' at the top of the file where you're configuring quill).

thank you @schneidmaster!
Good job!

Recognition works well, Is it possible to add target="_blank" property during node creation?
I tried adding target attribute here

{ insert: url, attributes: { link: url , target: '_blank'} }

but it doesn't work.

Do you know how to achive this?
Thanks

Inspired by @schneidmaster, this is how i fixed it in the keyboard module instead for the converting link when typing functionality:

spaceToLink: {
    collapsed: true,
    key: ' ',
    prefix: /https?:\/\/[^\s]+/, // call handler only when matched this regex
    handler: (() => {
      let prevOffset = 0;
      return function (range, context) {
        let url;
        const regex = /https?:\/\/[^\s]+/g;
        const text = this.quill.getText(prevOffset, range.index);
        const match = text.match(regex);
        if (match === null) {
          prevOffset = range.index;
          return true;
        }
        if (match.length > 1) {
          url = match[match.length - 1];
        } else {
          url = match[0];
        }
        const ops = [];
        ops.push({ retain: range.index - url.length });
        ops.push({ delete: url.length });
        ops.push({ insert: url, attributes: { link: url } });
        this.quill.updateContents({ ops });
        prevOffset = range.index;
        return true;
      }
    })()
  }

Can anybody please share a working plunker here..

This should be an inbuilt feature with help of a library like autolinker.js

@kilgarenone How do you use this snipped, I'm a bit lost?

Hi @n1ghtmare, it's been awhile since I worked with quilljs, but you could have a look how I did it here https://github.com/kilgarenone/haven/blob/master/src/app/shared/editor/modules/keyboard.ts
Hope that helped!

@kilgarenone Thanks, I appreciate the response. Sorry to bump such an old thread. Will try to figure it out from the code in the link.

For pasting, I also used @schneidmaster 's code. I cleaned it up to accept more than 1 URL pattern. Here it is!

function createUrlKeyboardHandler(urlRegexp: RegExp, quill: Quill) {
  return (range: RangeStatic, context: { prefix: string }) => {
    const prefixMatch = context.prefix.match(urlRegexp);
    if (prefixMatch === null) return true;
    const prefixLength = prefixMatch[0].length;
    const prefixStart = range.index - prefixLength;
    const url = quill.getText(prefixStart, prefixLength);
    quill.formatText(prefixStart, prefixLength, { link: url }, 'user');
    return true;
  };
}
  // Put the below somewhere with access to Quill
  const urlPatterns = [/https?:\/\/[^\s]+/, /www\.[^\s]+/];

  // Typing space after a URL matching these formats will turn it into a link
  for (const baseUrlPattern of urlPatterns) {
    const urlPattern = new RegExp(`${baseUrlPattern.source}$`);
    quillKeyboard.addBinding({
      collapsed: true,
      key: ' ',
      prefix: urlPattern,
      handler: createUrlKeyboardHandler(urlPattern, quill),
    });
  }

  // Pasting URLs adds a link
  quill.clipboard.addMatcher(Node.TEXT_NODE, (node: Text, delta) => {
    const combinedPattern = urlPatterns
      .map(pattern => `(${pattern.source})`)
      .join('|');
    const combinedRegexp = new RegExp(combinedPattern, 'g');

    const ops: DeltaOperation[] = [];
    const str = node.data;
    let lastMatchEndIndex = 0;
    let match: RegExpExecArray | null = combinedRegexp.exec(str);
    while (match !== null) {
      if (match.index > lastMatchEndIndex) {
        ops.push({ insert: str.slice(lastMatchEndIndex, match.index) });
      }
      ops.push({ insert: match[0], attributes: { link: match[0] } });
      lastMatchEndIndex = match.index + match[0].length;
      match = combinedRegexp.exec(str);
    }

    if (lastMatchEndIndex < str.length) {
      ops.push({ insert: str.slice(lastMatchEndIndex) });
    }

    delta.ops = ops;
    return delta;
  });

2020 here, links still broken. Documentation is unclear as to what the latest version is.
Is the project even still maintained?

Eventhough magic url works nicely... it doesn't offer a callback... which would be a great help ;)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

GildedHonour picture GildedHonour  路  3Comments

DaniilVeriga picture DaniilVeriga  路  3Comments

rsdrsd picture rsdrsd  路  3Comments

emanuelbsilva picture emanuelbsilva  路  3Comments

Softvision-MariusComan picture Softvision-MariusComan  路  3Comments