/* * The javascript for friendicas hovercard. Bootstraps popover is needed. * * Much parts of the code are from Hannes Mannerheims * qvitter code (https://github.com/hannesmannerheim/qvitter) * * It is licensed under the GNU Affero General Public License * */ $(document).ready(function(){ // Elements with the class "userinfo" will get a hover-card. // Note that this elements does need a href attribute which links to // a valid profile url $('.userinfo').on("mouseover", function(e) { var timeNow = new Date().getTime(); removeAllhoverCards(e,timeNow); var hoverCardData = false; var hrefAttr = false; var targetElement = $(this); // get href-attribute if(targetElement.is('[href]')) { hrefAttr = targetElement.attr('href'); } else { return true; } // no hover card if the element has the no-hover-card class if(targetElement.hasClass('no-hover-card')) { return true; } // no hovercard for anchor links if(hrefAttr.substring(0,1) == '#') { return true; } targetElement.attr('data-awaiting-hover-card',timeNow); // Take link href attribute as link to the profile var profileurl = hrefAttr; // the url to get the contact and template data var url = baseurl + "/frio_hovercard"; // store the title in an other data attribute beause bootstrap // popover destroys the title.attribute. We can restore it later var title = targetElement.attr("title"); targetElement.attr({"data-orig-title": title, title: ""}); // Timeoute until the hover-card does appear setTimeout(function(){ if(targetElement.is(":hover") && parseInt(targetElement.attr('data-awaiting-hover-card'),10) == timeNow) { if($('.hovercard').length == 0) { // no card if there already is one open // get an additional data atribute if the card is active targetElement.attr('data-hover-card-active',timeNow); // get the whole html content of the hover card and // push it to the bootstrap popover getHoverCardContent(profileurl, url, function(data){ if(data) { targetElement.popover({ html: true, placement: 'auto', trigger: 'manual', template: '
', content: data }).popover('show'); } }); } } }, 500); }).on("mouseleave", function(e) { // action when mouse leaves the hover-card var timeNow = new Date().getTime(); // copy the original title to the title atribute var title = $(this).attr("data-orig-title"); $(this).attr({"data-orig-title": "", title: title}); removeAllhoverCards(e,timeNow); }); }); // hover cards should be removed very easily, e.g. when any of these events happen $('body').on("mouseleave touchstart scroll click dblclick mousedown mouseup submit keydown keypress keyup", function(e){ var timeNow = new Date().getTime(); removeAllhoverCards(e,timeNow); }); // removes all hover cards function removeAllhoverCards(event,priorTo) { // don't remove hovercards until after 100ms, so user have time to move the cursor to it (which gives it the dont-remove-card class) setTimeout(function(){ $.each($('.hovercard'),function(){ var title = $(this).attr("data-orig-title"); // don't remove card if it was created after removeAllhoverCards() was called if($(this).data('card-created') < priorTo) { // don't remove it if we're hovering it right now! if(!$(this).hasClass('dont-remove-card')) { $('[data-hover-card-active="' + $(this).data('card-created') + '"]').removeAttr('data-hover-card-active'); $(this).popover("hide"); } } }); },100); } // if we're hovering a hover card, give it a class, so we don't remove it $('body').on('mouseover','.hovercard', function(e) { $(this).addClass('dont-remove-card'); }); $('body').on('mouseleave','.hovercard', function(e) { $(this).removeClass('dont-remove-card'); $(this).popover("hide"); }); // Ajax request to get json contact data function getContactData(purl, url, actionOnSuccess) { var postdata = { mode : 'modal', profileurl : purl, datatype : 'json', }; // Normalize and clean the profile so we can use a standardized url // as key for the cache var nurl = cleanContactUrl(purl).normalizeLink(); // If the contact is allready in the cache use the cached result instead // of doing a new ajax request if(nurl in getContactData.cache) { setTimeout(function() { actionOnSuccess(getContactData.cache[nurl]); } , 1); return; } $.ajax({ url: url, data: postdata, dataType: "json", success: function(data, textStatus, request){ // Check if the nurl (normalized profile url) is present and store it to the cache // The nurl will be the identifier in the object if(data.nurl.length > 0) { // Test if the contact is allready connected with the user (if url containing // the expression ("redir/") We will store different cache keys if((data.url.search("redir/")) >= 0 ) { var key = data.url; } else { var key = data.nurl; } getContactData.cache[key] = data; } actionOnSuccess(data, url, request); }, error: function(data) { actionOnSuccess(false, data, url); } }); } getContactData.cache = {}; // current time in milliseconds, to send each request to make sure // we 're not getting 304 response function timeNow() { return new Date().getTime(); } String.prototype.normalizeLink = function () { var ret = this.replace('https:', 'http:'); var ret = ret.replace('//www', '//'); return ret.rtrim(); }; function cleanContactUrl(url) { var parts = parseUrl(url); if(! ("scheme" in parts) || ! ("host" in parts)) { return url; } var newUrl =parts["scheme"] + "://" + parts["host"]; if("port" in parts) { newUrl += ":" + parts["port"]; } if("path" in parts) { newUrl += parts["path"]; } // if(url != newUrl) { // console.log("Cleaned contact url " + url + " to " + newUrl); // } return newUrl; } function parseUrl (str, component) { // eslint-disable-line camelcase // discuss at: http://locutusjs.io/php/parse_url/ // original by: Steven Levithan (http://blog.stevenlevithan.com) // reimplemented by: Brett Zamir (http://brett-zamir.me) // input by: Lorenzo Pisani // input by: Tony // improved by: Brett Zamir (http://brett-zamir.me) // note 1: original by http://stevenlevithan.com/demo/parseuri/js/assets/parseuri.js // note 1: blog post at http://blog.stevenlevithan.com/archives/parseuri // note 1: demo at http://stevenlevithan.com/demo/parseuri/js/assets/parseuri.js // note 1: Does not replace invalid characters with '_' as in PHP, // note 1: nor does it return false with // note 1: a seriously malformed URL. // note 1: Besides function name, is essentially the same as parseUri as // note 1: well as our allowing // note 1: an extra slash after the scheme/protocol (to allow file:/// as in PHP) // example 1: parse_url('http://user:pass@host/path?a=v#a') // returns 1: {scheme: 'http', host: 'host', user: 'user', pass: 'pass', path: '/path', query: 'a=v', fragment: 'a'} // example 2: parse_url('http://en.wikipedia.org/wiki/%22@%22_%28album%29') // returns 2: {scheme: 'http', host: 'en.wikipedia.org', path: '/wiki/%22@%22_%28album%29'} // example 3: parse_url('https://host.domain.tld/a@b.c/folder') // returns 3: {scheme: 'https', host: 'host.domain.tld', path: '/a@b.c/folder'} // example 4: parse_url('https://gooduser:secretpassword@www.example.com/a@b.c/folder?foo=bar') // returns 4: { scheme: 'https', host: 'www.example.com', path: '/a@b.c/folder', query: 'foo=bar', user: 'gooduser', pass: 'secretpassword' } var query var mode = (typeof require !== 'undefined' ? require('../info/ini_get')('locutus.parse_url.mode') : undefined) || 'php' var key = [ 'source', 'scheme', 'authority', 'userInfo', 'user', 'pass', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'fragment' ] // For loose we added one optional slash to post-scheme to catch file:/// (should restrict this) var parser = { php: new RegExp([ '(?:([^:\\/?#]+):)?', '(?:\\/\\/()(?:(?:()(?:([^:@\\/]*):?([^:@\\/]*))?@)?([^:\\/?#]*)(?::(\\d*))?))?', '()', '(?:(()(?:(?:[^?#\\/]*\\/)*)()(?:[^?#]*))(?:\\?([^#]*))?(?:#(.*))?)' ].join('')), strict: new RegExp([ '(?:([^:\\/?#]+):)?', '(?:\\/\\/((?:(([^:@\\/]*):?([^:@\\/]*))?@)?([^:\\/?#]*)(?::(\\d*))?))?', '((((?:[^?#\\/]*\\/)*)([^?#]*))(?:\\?([^#]*))?(?:#(.*))?)' ].join('')), loose: new RegExp([ '(?:(?![^:@]+:[^:@\\/]*@)([^:\\/?#.]+):)?', '(?:\\/\\/\\/?)?', '((?:(([^:@\\/]*):?([^:@\\/]*))?@)?([^:\\/?#]*)(?::(\\d*))?)', '(((\\/(?:[^?#](?![^?#\\/]*\\.[^?#\\/.]+(?:[?#]|$)))*\\/?)?([^?#\\/]*))', '(?:\\?([^#]*))?(?:#(.*))?)' ].join('')) } var m = parser[mode].exec(str) var uri = {} var i = 14 while (i--) { if (m[i]) { uri[key[i]] = m[i] } } if (component) { return uri[component.replace('PHP_URL_', '').toLowerCase()] } if (mode !== 'php') { var name = (typeof require !== 'undefined' ? require('../info/ini_get')('locutus.parse_url.queryKey') : undefined) || 'queryKey' parser = /(?:^|&)([^&=]*)=?([^&]*)/g uri[name] = {} query = uri[key[12]] || '' query.replace(parser, function ($0, $1, $2) { if ($1) { uri[name][$1] = $2 } }) } delete uri.source return uri } // trim function to replace whithespace after the string String.prototype.rtrim = function() { var trimmed = this.replace(/\s+$/g, ''); return trimmed; }; // Get hover-card template data and the contact-data and transform it with // the help of jSmart. At the end we have full html content of the hovercard function getHoverCardContent(purl, url, callback) { // fetch the raw content of the template getHoverCardTemplate(url, function(stpl) { var template = unescape(stpl); // get the contact data getContactData (purl, url, function(data) { if(typeof template != 'undefined') { // get the hover-card variables var variables = getHoverCardVariables(data); var tpl; // use friendicas template delimiters instead of // the original one jSmart.prototype.left_delimiter = '{{'; jSmart.prototype.right_delimiter = '}}'; // create a new jSmart instant with the raw content // of the template var tpl = new jSmart (template); // insert the variables content into the template content var HoverCardContent = tpl.fetch(variables); callback(HoverCardContent); } }); }); // This is interisting. this pice of code ajax request are done asynchron. // To make it work getHOverCardTemplate() and getHOverCardData have to return it's // data (no succes handler for each of this). I leave it here, because it could be useful. // https://lostechies.com/joshuaflanagan/2011/10/20/coordinating-multiple-ajax-requests-with-jquery-when/ // $.when( // getHoverCardTemplate(url), // getContactData (term, url ) // // ).done(function(template, profile){ // if(typeof template != 'undefined') { // var variables = getHoverCardVariables(profile); // // jSmart.prototype.left_delimiter = '{{'; // jSmart.prototype.right_delimiter = '}}'; // var tpl = new jSmart (template); // var html = tpl.fetch(variables); // // return html; // } // }); } // Ajax request to get the raw template content function getHoverCardTemplate (url, callback) { var postdata = { mode: 'modal', datatype: 'tpl' }; // Look if we have the template already in the cace, so we don't have // request it again if('hovercard' in getHoverCardTemplate.cache) { setTimeout(function() { callback(getHoverCardTemplate.cache['hovercard']); } , 1); return; } $.ajax({ url: url, data: postdata, success: function(data, textStatus) { // write the data in the cache getHoverCardTemplate.cache['hovercard'] = data; callback(data); } }).fail(function () {callback([]); }); } getHoverCardTemplate.cache = {}; // The Variables used for the template function getHoverCardVariables(object) { var profile = { name: object.name, nick: object.nick, addr: object.addr, thumb: object.thumb, url: object.url, nurl: object.nurl, location: object.location, gender: object.gender, about: object.about, network: object.network, tags: object.tags, bd: object.bd, account_type: object.account_type, actions: object.actions }; var variables = { profile: profile}; return variables; } // This is the html template for the hover-card // Since we grab the original hovercard.tpl we don't // need it anymore function hovercard_template() { var tempate = '\
\
\
\
\ \ \ \
\
\
\

{{$profile.name}}

{{if $profile.account_type}}{{$profile.account_type}}{{/if}}\
\
\ {{$profile.addr}}\ {{if $profile.network}} ({{$profile.network}}){{/if}}\
\ {{*{{if $profile.about}}
{{$profile.about}}
{{/if}}*}}\ \
\
\ {{* here are the differnt actions like privat message, poke, delete and so on *}}\ {{* @todo we have two different photo menus one for contacts and one for items at the network stream. We currently use the contact photo menu, so the items options are missing We need to move them *}}\
\ {{if $profile.actions.pm}}{{/if}}\ {{if $profile.actions.poke}}{{/if}}\
\
\ {{if $profile.actions.edit}}{{/if}}\ {{if $profile.actions.drop}}{{/if}}\ {{if $profile.actions.follow}}{{/if}}\
\
\
\ \
\ \
\
\ {{if $profile.tags}}{{/if}}'; }