It would be great to automatically turn links that are pasted as plain text (e.g. "Some text with a link http://prosemirror.net ...") into clickable links in the editor.
Are you talking about pasting or about being directly clickable here? Pasting links and having them show up as links seems to work fine. contentEditable links are not clickable by default, for the very good reason that this'd make it impossible to put the cursor inside of them. But you can right-click them and select one of the link-related options, or enable the tooltipMenu, and have a tooltip with a non-editable version of the link's target url show up when you put the cursor inside of the link.
I am talking about pasting links and having them show up as links. What seems to work fine is pasting content that was copied from an html source. But as soon as I copy and paste plain text (e.g. copy the url from the browser address bar), links are not recognized and inserted as normal text elements.
Ironically, the example text that I wrote in the issue got turned into an html link automatically, which made it work with ProseMirror (in case you copied that text).
Try to copy and paste this text instead:
Some text with a link http://prosemirror.net some more text.
I think the most common use case is being able to copy and paste a link directly from the browser address bar.
Ah, I see what you mean now. I don't think this is behavior that is always desirable, and I don't think the core library should do it. But I guess shipping with a module that implements it would be a good idea.
Doing this in a module sounds good. It would be nice to be able to define custom paste handlers that can decide whether or not to override the default parsing behavior. I couldn't find any functionality that supports something like this. Is this something you have planned or that is already possible?
My main use case would still be the detection of URLs. Other use cases that I can think of and that I might want to implement in the future are the detection of dates (generate custom node with a datepicker) and the detection of emoticons.
One possibility is for the editor to allow associating regexps with node types, and have the DOM parser run the corresponding handler whenever text matching that regexp is found. It won't be on by default, since I'm treating DOM input as semantic HTML and want as little magic as possible, but it would make it easy for sites to add functionality like this. What do you think?
Sounds really nice! Would definitely solve my use cases :)
Sounds to me like this would make a good input rule you could add. Are they active on paste?
Yes, pasted HTML content goes through the DOM parser.
I just tried the following simple input rule, but it is only triggered after hitting the 'o' key. Pasting the whole word 'hello' does not trigger the input rule.
import {InputRule, addInputRule} from '../src/inputrules';
addInputRule(pm, new InputRule(/hello$/, '', () => console.log('hello')));
Is this the intended behavior or a bug? Is all content that goes through the DOM parser supposed to go through the input rules?
And the use cases that I can think of would differ in when I would ideally want pasted content and typed content to trigger an input rule:
I'm not sure how I could achieve this behavior by just using input rules even if they were triggered on paste. In this case it would be necessary to define different input rules for typed content and pasted content. Furthermore, pasted content should be able to trigger multiple input rules as the pasted content might contain multiple links spread across multiple paragraphs. That's why I think @marijnh's proposed solution would still be worth the effort as it enables more use cases in comparison to just using the existing input rule mechanism. If the input rules were triggered on paste, it should work as a temporary solution.
What do you think?
Those are good points. BTW, change '' to ' ' and see if your example works after a space.
Here is what I attempted:
import {InputRule} from "prosemirror/dist/inputrules"
let urlex = /((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)/
LinkMark.register("autoInput","startLink", new InputRule(urlex," ",
function(pm, match, pos) {
let url = match[0]
console.log(url)
pm.setMark(this,pos,{href: url, title: ""})
}
))
It did trigger after the space when typing. It didn't style correctly because the pm.setMark is probably wrong. Pasting triggered after a space too but for the reasons you give, a more general solution would be better.
@pboysen Thanks for the code snippet! Using ' ' works as expected when typing, but I intentionally tried it with '' to see if the input rule is triggered by a paste which does not contain a space at the end.
I made some changes to your code snippet and got it to work:
import {InputRule} from 'prosemirror/dist/inputrules';
import {LinkMark} from 'prosemirror/dist/model';
LinkMark.register('autoInput', new InputRule(
'makeLink',
/((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?) $/,
' ',
function(pm, match, pos) {
const url = match[1];
const startPos = pos.move(-match[0].length);
pm.tr.addMark(startPos, startPos.move(url.length), this.create({href: url, title: url})).apply();
}
))
Note that I'm not using the latest development version of ProseMirror, which is why the arguments to register and InputRule are a little different.
Thanks for the additional code! I hadn't played with input rules before so it was a good experience. Spent more time trying to find a decent url regexp. I did find a url-regexp npm module which might work better in production.
The resolution to issue #221 should now make this relatively easy. Here's a rough sketch:
pm.on("transformPasted", function(slice) {
return new Slice(linkify(slice.content), slice.openLeft, slice.openRight)
})
// Very simplistic url-matching regexp
var url = /\bhttps?:\/\/[\w_\/\.]+/g
// Expand urls in text nodes in a given fragment to linked text
// (and recursively go through child nodes)
function linkify(fragment) {
var linkified = []
for (var i = 0; i < fragment.childCount; i++) {
var child = fragment.child(i)
if (child.isText) {
var pos = 0, m
while (m = url.exec(child.text)) {
var start = m.index, end = start + m[0].length, link = child.type.schema.marks.link
if (start > 0) linkified.push(child.copy(child.text.slice(pos, start)))
var urlText = child.text.slice(start, end)
linkified.push(child.type.create(null, urlText, link.create({href: urlText}).addToSet(child.marks)))
pos = end
}
if (pos < child.text.length) linkified.push(child.copy(child.text.slice(pos)))
} else {
linkified.push(child.copy(linkify(child.content)))
}
}
return Fragment.fromArray(linkified)
}
In case someone wants to use the snippet, there's a bug in the while clause creating an endless loop. Changing it to while (m = url.exec(child.text.slice(pos))) fixes it.
No, really, there isn't. The url regexp has a global flag, and will maintain a lastIndex. (Also I tested it.)
Oh I'm sorry, that was my fault then!
I tested the snippet using the url-regex package where I was doing urlRegex().exec(child.text), which created a fresh regexp with every loop and created the endless loop. You're totally right that it works just fine when reusing the same regexp like you did. Thanks for pointing that out! Should have tested your snippet unchanged, sorry for that.
Updated this code for 0.18 - which no longer allows strings for create and copy methods used above (and now uses the transformPasted prop)
(this is typescript)
let linkPlugin = new Plugin({
props: {
transformPasted: (slice: Slice) => {
return new Slice(linkify(slice.content), slice.openLeft, slice.openRight)
}
}
})
const HTTP_LINK_REGEX = /\bhttps?:\/\/[\w_\/\.]+/g
let linkify = function(fragment: Fragment): Fragment {
var linkified : Node[] = []
fragment.forEach(function(child: Node){
if (child.isText) {
const text = child.text as string
var pos = 0, match
while (match = HTTP_LINK_REGEX.exec(text)) {
var start = match.index
var end = start + match[0].length
var link = child.type.schema.marks['link']
// simply copy across the text from before the match
if (start > 0) {
linkified.push(child.cut(pos, start))
}
const urlText = text.slice(start, end)
linkified.push(
child.cut(start, end).mark(link.create({href: urlText}).addToSet(child.marks))
)
pos = end
}
// copy over whatever is left
if (pos < text.length) {
linkified.push(child.cut(pos))
}
} else {
linkified.push(child.copy(linkify(child.content)))
}
})
return Fragment.fromArray(linkified)
}
For posterity, if you want to use the code above you have to rename two variables per:
The openLeft and openRight properties of Slice objects have been renamed to openStart and openEnd to avoid confusion in right-to-left text.
transformPasted: (slice: Slice) => {
return new Slice(linkify(slice.content), slice.openStart, slice.openEnd)
}
Most helpful comment
The resolution to issue #221 should now make this relatively easy. Here's a rough sketch: