Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

codemirror - detect and create links inside editor

I am using codemirror, configured to display javascript.

I have code like this:

...
var ref =  'http://www.example.com/test.html';
var ref2 = 'http://www.example.com/test2.html';
...

When displaying the editor it would be great if I could click on the links that might be present in the editor. The link would open the page on a different tab obviously.

is there an easy way to achieve this ?

like image 997
Zo72 Avatar asked Nov 14 '14 15:11

Zo72


3 Answers

Not really easy, but what you'd do is:

  • Write an overlay mode that recognizes such links. Basically, this is a mode that spits out a custom token type when it finds something that looks like a link, and null otherwise. You can use the simple mode addon to make this easier. You can use this token type's CSS class (for example "link" becomes cm-link) to style your links.

  • Make your editor use your overlay by calling the addOverlay method.

  • Register a mousedown event handler on your editor (instance.getWrapperElement().addEventListener(...)).

  • In this handler, check whether the event's target has the link CSS class. If it does, the user is clicking a link.

  • If so, use the coordsChar method, using the coordinates from your mouse event, to find the position in the document that was clicked. Extract the actual link from the document text around that position, and follow it.

  • (Or, even better, instead of directly interfering with the click, which might be intended to put the cursor in the link or select it, show a widget containing a regular link whenever the cursor is inside of link text.)

like image 195
Marijn Avatar answered Oct 08 '22 07:10

Marijn


Here is a solution I came up with:

demo here: plunkr

code:

<!DOCTYPE html>
<html>

<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.17.0/codemirror.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.17.0/codemirror.css"/>

<style>
    html, body { height:100%; }
    .CodeMirror .cm-url { color: blue; }      
</style>
</head>

<body>
<script>

var cm = CodeMirror(document.body);
cm.setValue('hover over the links below\nlink1 https://plnkr.co/edit/5m31E14HUEhSXrXtOkNJ some text\nlink2 google.com\n');

hyperlinkOverlay(cm);

function hoverWidgetOnOverlay(cm, overlayClass, widget) {
    cm.addWidget({line:0, ch:0}, widget, true);
    widget.style.position = 'fixed';
    widget.style.zIndex=100000;
    widget.style.top=widget.style.left='-1000px'; // hide it 
    widget.dataset.token=null;

    cm.getWrapperElement().addEventListener('mousemove', e => {
        let onToken=e.target.classList.contains("cm-"+overlayClass), onWidget=(e.target===widget || widget.contains(e.target));

        if (onToken && e.target.innerText!==widget.dataset.token) { // entered token, show widget
            var rect = e.target.getBoundingClientRect();
            widget.style.left=rect.left+'px';
            widget.style.top=rect.bottom+'px';
            //let charCoords=cm.charCoords(cm.coordsChar({ left: e.pageX, top:e.pageY }));
            //widget.style.left=(e.pageX-5)+'px';  
            //widget.style.top=(cm.charCoords(cm.coordsChar({ left: e.pageX, top:e.pageY })).bottom-1)+'px';

            widget.dataset.token=e.target.innerText;
            if (typeof widget.onShown==='function') widget.onShown();

        } else if ((e.target===widget || widget.contains(e.target))) { // entered widget, call widget.onEntered
            if (widget.dataset.entered==='true' && typeof widget.onEntered==='function')  widget.onEntered();
            widget.dataset.entered='true';

        } else if (!onToken && widget.style.left!=='-1000px') { // we stepped outside
            widget.style.top=widget.style.left='-1000px'; // hide it 
            delete widget.dataset.token;
            widget.dataset.entered='false';
            if (typeof widget.onHidden==='function') widget.onHidden();
        }

        return true;
    });
}

function hyperlinkOverlay(cm) {
    if (!cm) return;
    
    const rx_word = "\" "; // Define what separates a word

    function isUrl(s) {
        if (!isUrl.rx_url) {
            // taken from https://gist.github.com/dperini/729294
            isUrl.rx_url=/^(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i;
            // valid prefixes
            isUrl.prefixes=['http:\/\/', 'https:\/\/', 'ftp:\/\/', 'www.'];
            // taken from https://w3techs.com/technologies/overview/top_level_domain/all
            isUrl.domains=['com','ru','net','org','de','jp','uk','br','pl','in','it','fr','au','info','nl','ir','cn','es','cz','kr','ua','ca','eu','biz','za','gr','co','ro','se','tw','mx','vn','tr','ch','hu','at','be','dk','tv','me','ar','no','us','sk','xyz','fi','id','cl','by','nz','il','ie','pt','kz','io','my','lt','hk','cc','sg','edu','pk','su','bg','th','top','lv','hr','pe','club','rs','ae','az','si','ph','pro','ng','tk','ee','asia','mobi'];
        }

        if (!isUrl.rx_url.test(s)) return false;
        for (let i=0; i<isUrl.prefixes.length; i++) if (s.startsWith(isUrl.prefixes[i])) return true;
        for (let i=0; i<isUrl.domains.length; i++) if (s.endsWith('.'+isUrl.domains[i]) || s.includes('.'+isUrl.domains[i]+'\/') ||s.includes('.'+isUrl.domains[i]+'?')) return true;
        return false;
    }

    cm.addOverlay({
        token: function(stream) {
            let ch = stream.peek();
            let word = "";

            if (rx_word.includes(ch) || ch==='\uE000' || ch==='\uE001') {
                stream.next();
                return null;
            }

            while ((ch = stream.peek()) && !rx_word.includes(ch)) {
                word += ch;
                stream.next();
            }

            if (isUrl(word)) return "url"; // CSS class: cm-url
        }}, 
        { opaque : true }  // opaque will remove any spelling overlay etc
    );

    let widget=document.createElement('button');
    widget.innerHTML='&rarr;'
    widget.onclick=function(e) { 
        if (!widget.dataset.token) return;
        let link=widget.dataset.token;
        if (!(new RegExp('^(?:(?:https?|ftp):\/\/)', 'i')).test(link)) link="http:\/\/"+link;
        window.open(link, '_blank'); 
        return true;
    };
    hoverWidgetOnOverlay(cm, 'url', widget);
}

</script>
</body>

</html>
like image 32
kofifus Avatar answered Oct 08 '22 07:10

kofifus


Here is a starting point, but it need to be improved.
LIVE DEMO

  function makeHyperLink(innerTextInside)
  {
    var all = document.getElementsByTagName("*");
    for (var i=0, max=all.length; i < max; i++) {
      if(all[i].innerText == innerTextInside)
      {
        all[i].innerHTML="<a target='_blank' href='https://google.com'>THIS IS A LINK TO GOOGLE</a>"
      }
    }
  }
like image 1
Jonathan Applebaum Avatar answered Oct 08 '22 06:10

Jonathan Applebaum