7346c06d7ff51a79edde4f2adc3a88c618e1f068
[friendica.git/.git] / view / theme / frio / js / theme.js
1 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPLv3-or-later
2
3 var jotcache = ''; //The jot cache. We use it as cache to restore old/original jot content
4
5 $(document).ready(function(){
6         // Destroy unused perfect scrollbar in aside element
7         $('aside').perfectScrollbar('destroy');
8
9         //fade in/out based on scrollTop value
10         var scrollStart;
11
12         $(window).scroll(function () {
13                 let currentScroll = $(this).scrollTop();
14
15                 // Top of the page or going down = hide the button
16                 if (!scrollStart || !currentScroll || currentScroll > scrollStart) {
17                         $("#back-to-top").fadeOut();
18                         scrollStart = currentScroll;
19                 }
20
21                 // Going up enough = show the button
22                 if (scrollStart - currentScroll > 100) {
23                         $("#back-to-top").fadeIn();
24                         scrollStart = currentScroll;
25                 }
26         });
27
28         // scroll body to 0px on click
29         $("#back-to-top").click(function () {
30                 $("body,html").animate({
31                         scrollTop: 0
32                 }, 400);
33                 return false;
34         });
35
36         // add the class "selected" to group widges li if li > a does have the class group-selected
37         if( $("#sidebar-group-ul li a").hasClass("group-selected")) {
38                 $("#sidebar-group-ul li a.group-selected").parent("li").addClass("selected");
39         }
40
41         // add the class "selected" to forums widges li if li > a does have the class forum-selected
42         if( $("#forumlist-sidbar-ul li a").hasClass("forum-selected")) {
43                 $("#forumlist-sidbar-ul li a.forum-selected").parent("li").addClass("selected");
44         }
45
46         // add the class "active" to tabmenuli if li > a does have the class active
47         if( $("#tabmenu ul li a").hasClass("active")) {
48                 $("#tabmenu ul li a.active").parent("li").addClass("active");
49         }
50
51         // give select fields an boostrap classes
52         // @todo: this needs to be changed in friendica core
53         $(".field.select, .field.custom").addClass("form-group");
54         $(".field.select > select, .field.custom > select").addClass("form-control");
55
56         // move the tabbar to the second nav bar
57         $("section .tabbar-wrapper").first().appendTo("#topbar-second > .container > #tabmenu");
58
59         // add mask css url to the logo-img container
60         //
61         // This is for firefox - we use a mask which looks like the friendica logo to apply user collers
62         // to the friendica logo (the mask is in nav.tpl at the botom). To make it work we need to apply the
63         // correct url. The only way which comes to my mind was to do this with js
64         // So we apply the correct url (with the link to the id of the mask) after the page is loaded.
65         if($("#logo-img").length ) {
66                 var pageurl = "url('" + window.location.href + "#logo-mask')";
67                 $("#logo-img").css({"mask": pageurl});
68         }
69
70         // make responsive tabmenu with flexmenu.js
71         // the menupoints which doesn't fit in the second nav bar will moved to a
72         // dropdown menu. Look at common_tabs.tpl
73         $("ul.tabs.flex-nav").flexMenu({
74                 'cutoff': 2,
75                 'popupClass': "dropdown-menu pull-right",
76                 'popupAbsolute': false,
77                 'target': ".flex-target"
78         });
79
80         // add Jot button to the second navbar
81         let $jotButton = $("#jotOpen");
82         if ($jotButton.length) {
83                 $jotButton.appendTo("#topbar-second > .container > #navbar-button");
84                 if ($("#jot-popup").is(":hidden")) {
85                         $jotButton.hide();
86                 }
87                 $jotButton.on('click', function (e) {
88                         e.preventDefault();
89                         jotShow();
90                 });
91         }
92
93         let $body = $('body');
94
95         // show bulk deletion button at network page if checkbox is checked
96         $body.change("input.item-select", function(){
97                 var checked = false;
98
99                 // We need to get all checked items, so it would close the delete button
100                 // if we uncheck one item and others are still checked.
101                 // So return checked = true if there is any checked item
102                 $('input.item-select').each( function() {
103                         if($(this).is(':checked')) {
104                                 checked = true;
105                                 return false;
106                         }
107                 });
108
109                 if(checked) {
110                         $("#item-delete-selected").fadeTo(400, 1);
111                         $("#item-delete-selected").show();
112                 } else {
113                         $("#item-delete-selected").fadeTo(400, 0, function(){
114                                 $("#item-delete-selected").hide();
115                         });
116                 }
117         });
118
119         // initialize the bootstrap tooltips
120         $body.tooltip({
121                 selector: '[data-toggle="tooltip"]',
122                 container: 'body',
123                 animation: true,
124                 html: true,
125                 placement: 'auto',
126                 trigger: 'hover',
127                 delay: {
128                         show: 500,
129                         hide: 100
130                 },
131                 sanitizeFn: function (content) {
132                         return DOMPurify.sanitize(content)
133                 },
134         });
135
136         // initialize the bootstrap-select
137         $('.selectpicker').selectpicker();
138
139         // add search-heading to the seccond navbar
140         if( $(".search-heading").length) {
141                 $(".search-heading").appendTo("#topbar-second > .container > #tabmenu");
142         }
143
144         // add search results heading to the second navbar
145         // and insert the search value to the top nav search input
146         if( $(".search-content-wrapper").length ) {
147                 // get the text of the heading (we catch the plain text because we don't
148                 // want to have a h4 heading in the navbar
149                 var searchText = $(".section-title-wrapper > h2").text();
150                 // insert the plain text in a <h4> heading and give it a class
151                 var newText = '<h4 class="search-heading">'+searchText+'</h4>';
152                 // append the new heading to the navbar
153                 $("#topbar-second > .container > #tabmenu").append(newText);
154
155                 // try to get the value of the original search input to insert it
156                 // as value in the nav-search-input
157                 var searchValue = $("#search-wrapper .form-group-search input").val();
158
159                 // if the orignal search value isn't available use the location path as value
160                 if( typeof searchValue === "undefined") {
161                         // get the location path
162                         var urlPath = window.location.search
163                         // and split it up in its parts
164                         var splitPath = urlPath.split(/(\?search?=)(.*$)/);
165
166                         if(typeof splitPath[2] !== 'undefined') {
167                                 // decode the path (e.g to decode %40 to the character @)
168                                 var searchValue = decodeURIComponent(splitPath[2]);
169                         }
170                 }
171
172                 if( typeof searchValue !== "undefined") {
173                         $("#nav-search-input-field").val(searchValue);
174                 }
175         }
176
177         // move the "Save the search" button to the second navbar
178         $(".search-content-wrapper #search-save").appendTo("#topbar-second > .container > #navbar-button");
179
180         // append the vcard-short-info to the second nav after passing the element
181         // with .fn (vcard username). Use scrollspy to get the scroll position.
182         if( $("aside .vcard .fn").length) {
183                 $(".vcard .fn").scrollspy({
184                         min: $(".vcard .fn").position().top - 50,
185                         onLeaveTop: function onLeave(element) {
186                                 $("#vcard-short-info").fadeOut(500, function () {
187                                         $("#vcard-short-info").appendTo("#vcard-short-info-wrapper");
188                                 });
189                         },
190                         onEnter: function(element) {
191                                 $("#vcard-short-info").appendTo("#nav-short-info");
192                                 $("#vcard-short-info").fadeIn(500);
193                         },
194                 });
195         }
196
197         // move the forum contact information of the network page into the second navbar
198         if( $(".network-content-wrapper > #viewcontact_wrapper-network").length) {
199                 // get the contact-wrapper element and append it to the second nav bar
200                 // Note: We need the first() element with this class since at the present time we
201                 // store also the js template information in the html code and thats why
202                 // there are two elements with this class but we don't want the js template
203                 $(".network-content-wrapper > #viewcontact_wrapper-network .contact-wrapper").first().appendTo("#nav-short-info");
204         }
205
206         // move heading from network stream to the second navbar nav-short-info section
207         if( $(".network-content-wrapper > .section-title-wrapper").length) {
208                 // get the heading element
209                 var heading = $(".network-content-wrapper > .section-title-wrapper > h2");
210                 // get the text of the heading
211                 var headingContent = heading.text();
212                 // create a new element with the content of the heading
213                 var newText = '<h4 class="heading" data-toggle="tooltip" title="'+headingContent+'">'+headingContent+'</h4>';
214                 // remove the old heading element
215                 heading.remove(),
216                 // put the new element to the second nav bar
217                 $("#topbar-second #nav-short-info").append(newText);
218         }
219
220         if( $(".community-content-wrapper").length) {
221                 // get the heading element
222                 var heading = $(".community-content-wrapper > h3").first();
223                 // get the text of the heading
224                 var headingContent = heading.text();
225                 // create a new element with the content of the heading
226                 var newText = '<h4 class="heading">'+headingContent+'</h4>';
227                 // remove the old heading element
228                 heading.remove(),
229                 // put the new element to the second nav bar
230                 $("#topbar-second > .container > #tabmenu").append(newText);
231         }
232
233         // Dropdown menus with the class "dropdown-head" will display the active tab
234         // as button text
235         $body.on('click', '.dropdown-head .dropdown-menu li a, .dropdown-head .dropdown-menu li button', function(){
236                 toggleDropdownText(this);
237         });
238
239         // Change the css class while clicking on the switcher elements
240         $(".toggle label, .toggle .toggle-handle").click(function(event){
241                 event.preventDefault();
242                 // Get the value of the input element
243                 var input = $(this).siblings("input");
244                 var val = 1-input.val();
245                 var id = input.attr("id");
246
247                 // The css classes for "on" and "off"
248                 var onstyle = "btn-primary";
249                 var offstyle = "btn-default off";
250
251                 // According to the value of the input element we need to decide
252                 // which class need to be added and removed when changing the switch
253                 var removedclass = (val == 0 ? onstyle : offstyle);
254                 var addedclass = (val == 0 ? offstyle : onstyle)
255                 $("#"+id+"_onoff").addClass(addedclass).removeClass(removedclass);
256
257                 // After changing the switch the input element is getting
258                 // the newvalue
259                 input.val(val);
260         });
261
262         // Set the padding for input elements with inline buttons
263         //
264         // In Frio we use some input elements where the submit button is visually
265         // inside the the input field (through css). We need to set a padding-right
266         // to the input element where the padding value would be at least the width
267         // of the button. Otherwise long user input would be invisible because it is
268         // behind the button.
269         $body.on('click', '.form-group-search > input', function() {
270                 // Get the width of the button (if the button isn't available
271                 // buttonWidth will be null
272                 var buttonWidth = $(this).next('.form-button-search').outerWidth();
273
274                 if (buttonWidth) {
275                         // Take the width of the button and ad 5px
276                         var newWidth = buttonWidth + 5;
277                         // Set the padding of the input element according
278                         // to the width of the button
279                         $(this).css('padding-right', newWidth);
280                 }
281
282         });
283
284         /*
285          * This event handler hides all comment UI when the user clicks anywhere on the page
286          * It ensures that we aren't closing the current comment box
287          *
288          * We are making an exception for buttons because of a race condition with the
289          * comment opening button that results in an already closed comment UI.
290          */
291         $(document).on('mousedown', function(event) {
292                 if (event.target.type === 'button') {
293                         return true;
294                 }
295
296                 var $dontclosethis = $(event.target).closest('.wall-item-comment-wrapper').find('.comment-edit-form');
297                 $('.wall-item-comment-wrapper .comment-edit-submit-wrapper:visible').each(function() {
298                         var $parent = $(this).parent('.comment-edit-form');
299                         var itemId = $parent.data('itemId');
300
301                         if ($dontclosethis[0] != $parent[0]) {
302                                 var textarea = $parent.find('textarea').get(0)
303
304                                 commentCloseUI(textarea, itemId);
305                         }
306                 });
307         });
308
309         // Customize some elements when the app is used in standalone mode on Android
310         if (window.matchMedia('(display-mode: standalone)').matches) {
311                 // Open links to source outside of the webview
312                 $('body').on('click', '.plink', function (e) {
313                         $(e.target).attr('target', '_blank');
314                 });
315         }
316
317         /*
318          * This event listeners ensures that the textarea size is updated event if the
319          * value is changed externally (textcomplete, insertFormatting, fbrowser...)
320          */
321         $(document).on('change', 'textarea', function(event) {
322                 autosize.update(event.target);
323         });
324
325         /*
326          * Sticky aside on page scroll
327          * We enable the sticky aside only when window is wider than
328          * 976px - which is the maximum width where the aside is shown in
329          * mobile style - because on chrome-based browsers (desktop and
330          * android) the sticky plugin in mobile style causes the browser to
331          * scroll back to top the main content, making it impossible
332          * to navigate.
333          * A side effect is that the sitky aside isn't really responsive,
334          * since is enabled or not at page loading time.
335          */
336         if ($(window).width() > 976) {
337                 $("aside").stick_in_parent({
338                         offset_top: 100, // px, header + tab bar + spacing
339                         recalc_every: 10
340                 });
341                 // recalculate sticky aside on clicks on <a> elements
342                 // this handle height changes on expanding submenus
343                 $("aside").on("click", "a", function(){
344                         $(document.body).trigger("sticky_kit:recalc");
345                 });
346         }
347
348         /*
349          * Add or remove "aside-out" class to body tag
350          * when the mobile aside is shown or hidden.
351          * The class is used in css to disable scroll in page when the aside
352          * is shown.
353          */
354         $("aside")
355                 .on("shown.bs.offcanvas", function() {
356                         $body.addClass("aside-out");
357                 })
358                 .on("hidden.bs.offcanvas", function() {
359                         $body.removeClass("aside-out");
360                 });
361
362         // Event listener for 'Show & hide event map' button in the network stream.
363         $body.on("click", ".event-map-btn", function() {
364                 showHideEventMap(this);
365         });
366
367         // Comment form submit
368         $body.on('submit', '.comment-edit-form', function(e) {
369                 let $form = $(this);
370                 let id = $form.data('item-id');
371
372                 // Compose page form exception: id is always 0 and form must not be submitted asynchronously
373                 if (id === 0) {
374                         return;
375                 }
376
377                 e.preventDefault();
378
379                 let $commentSubmit = $form.find('.comment-edit-submit').button('loading');
380
381                 unpause();
382                 commentBusy = true;
383
384                 $.post(
385                         'item',
386                         $form.serialize(),
387                         'json'
388                 )
389                 .then(function(data) {
390                         if (data.success) {
391                                 $('#comment-edit-wrapper-' + id).hide();
392                                 let $textarea = $('#comment-edit-text-' + id);
393                                 $textarea.val('');
394                                 if ($textarea.get(0)) {
395                                         commentClose($textarea.get(0), id);
396                                 }
397                                 if (timer) {
398                                         clearTimeout(timer);
399                                 }
400                                 timer = setTimeout(NavUpdate,10);
401                                 force_update = true;
402                                 update_item = id;
403                         }
404                         if (data.reload) {
405                                 window.location.href = data.reload;
406                         }
407                 })
408                 .always(function() {
409                         $commentSubmit.button('reset');
410                 });
411         });
412
413         $body.on('submit', '.modal-body #poke-wrapper', function(e) {
414                 e.preventDefault();
415
416                 let $form = $(this);
417                 let $pokeSubmit = $form.find('button[type=submit]').button('loading');
418
419                 $.post(
420                         $form.attr('action'),
421                         $form.serialize(),
422                         'json'
423                 )
424                 .then(function(data) {
425                         if (data.success) {
426                                 $('#modal').modal('hide');
427                         }
428                 })
429                 .always(function() {
430                         $pokeSubmit.button('reset');
431                 });
432         })
433 });
434
435 function openClose(theID) {
436         var elem = document.getElementById(theID);
437
438         if( $(elem).is(':visible')) {
439                 $(elem).slideUp(200);
440         }
441         else {
442                 $(elem).slideDown(200);
443         }
444 }
445
446 function showHide(theID) {
447         var elem = document.getElementById(theID);
448         var edit = document.getElementById("comment-edit-submit-wrapper-" + theID.match('[0-9$]+'));
449
450         if ($(elem).is(':visible')) {
451                 if (!$(edit).is(':visible')) {
452                         edit.style.display = "block";
453                 }
454                 else {
455                         elem.style.display = "none";
456                 }
457         }
458         else {
459                 elem.style.display = "block";
460         }
461 }
462
463 // Show & hide event map in the network stream by button click.
464 function showHideEventMap(elm) {
465         // Get the id of the map element - it should be provided through
466         // the atribute "data-map-id".
467         var mapID = elm.getAttribute('data-map-id');
468
469         // Get translation labels.
470         var mapshow = elm.getAttribute('data-show-label');
471         var maphide = elm.getAttribute('data-hide-label');
472
473         // Change the button labels.
474         if (elm.innerText == mapshow) {
475                 $('#' + elm.id).text(maphide);
476         } else {
477                 $('#' + elm.id).text(mapshow);
478         }
479         // Because maps are iframe elements, we cant hide it through css (display: none).
480         // We solve this issue by putting the map outside the screen with css.
481         // So the first time the 'Show map' button is pressed we move the map
482         // element into the screen area.
483         var mappos = $('#' + mapID).css('position');
484
485         if (mappos === 'absolute') {
486                 $('#' + mapID).hide();
487                 $('#' + mapID).css({position: 'relative', left: 'auto', top: 'auto'});
488                 openClose(mapID);
489         } else {
490                 openClose(mapID);
491         }
492         return false;
493 }
494
495 function justifyPhotos() {
496         justifiedGalleryActive = true;
497         $('#photo-album-contents').justifiedGallery({
498                 margins: 3,
499                 border: 0,
500                 sizeRangeSuffixes: {
501                         'lt48': '-6',
502                         'lt80': '-5',
503                         'lt300': '-4',
504                         'lt320': '-2',
505                         'lt640': '-1',
506                         'lt1024': '-0'
507                 }
508         }).on('jg.complete', function(e){ justifiedGalleryActive = false; });
509 }
510
511 // Load a js script to the html head.
512 function loadScript(url, callback) {
513         // Check if the script is already in the html head.
514         var oscript = $('head script[src="' + url + '"]');
515
516         // Delete the old script from head.
517         if (oscript.length > 0) {
518                 oscript.remove();
519         }
520         // Adding the script tag to the head as suggested before.
521         var head = document.getElementsByTagName('head')[0];
522         var script = document.createElement('script');
523         script.type = 'text/javascript';
524         script.src = url;
525
526         // Then bind the event to the callback function.
527         // There are several events for cross browser compatibility.
528         script.onreadystatechange = callback;
529         script.onload = callback;
530
531         // Fire the loading.
532         head.appendChild(script);
533 }
534
535 // Does we need a ? or a & to append values to a url
536 function qOrAmp(url) {
537         if(url.search('\\?') < 0) {
538                 return '?';
539         } else {
540                 return '&';
541         }
542 }
543
544 String.prototype.normalizeLink = function () {
545         var ret = this.replace('https:', 'http:');
546         var ret = ret.replace('//www', '//');
547         return ret.rtrim();
548 };
549
550 function cleanContactUrl(url) {
551         var parts = parseUrl(url);
552
553         if(! ("scheme" in parts) || ! ("host" in parts)) {
554                 return url;
555         }
556
557         var newUrl =parts["scheme"] + "://" + parts["host"];
558
559         if("port" in parts) {
560                 newUrl += ":" + parts["port"];
561         }
562
563         if("path" in parts) {
564                 newUrl += parts["path"];
565         }
566
567 //      if(url != newUrl) {
568 //              console.log("Cleaned contact url " + url + " to " + newUrl);
569 //      }
570
571         return newUrl;
572 }
573
574 function parseUrl (str, component) { // eslint-disable-line camelcase
575         //       discuss at: http://locutusjs.io/php/parse_url/
576         //      original by: Steven Levithan (http://blog.stevenlevithan.com)
577         // reimplemented by: Brett Zamir (http://brett-zamir.me)
578         //         input by: Lorenzo Pisani
579         //         input by: Tony
580         //      improved by: Brett Zamir (http://brett-zamir.me)
581         //           note 1: original by http://stevenlevithan.com/demo/parseuri/js/assets/parseuri.js
582         //           note 1: blog post at http://blog.stevenlevithan.com/archives/parseuri
583         //           note 1: demo at http://stevenlevithan.com/demo/parseuri/js/assets/parseuri.js
584         //           note 1: Does not replace invalid characters with '_' as in PHP,
585         //           note 1: nor does it return false with
586         //           note 1: a seriously malformed URL.
587         //           note 1: Besides function name, is essentially the same as parseUri as
588         //           note 1: well as our allowing
589         //           note 1: an extra slash after the scheme/protocol (to allow file:/// as in PHP)
590         //        example 1: parse_url('http://user:pass@host/path?a=v#a')
591         //        returns 1: {scheme: 'http', host: 'host', user: 'user', pass: 'pass', path: '/path', query: 'a=v', fragment: 'a'}
592         //        example 2: parse_url('http://en.wikipedia.org/wiki/%22@%22_%28album%29')
593         //        returns 2: {scheme: 'http', host: 'en.wikipedia.org', path: '/wiki/%22@%22_%28album%29'}
594         //        example 3: parse_url('https://host.domain.tld/a@b.c/folder')
595         //        returns 3: {scheme: 'https', host: 'host.domain.tld', path: '/a@b.c/folder'}
596         //        example 4: parse_url('https://gooduser:secretpassword@www.example.com/a@b.c/folder?foo=bar')
597         //        returns 4: { scheme: 'https', host: 'www.example.com', path: '/a@b.c/folder', query: 'foo=bar', user: 'gooduser', pass: 'secretpassword' }
598
599         var query
600
601         var mode = (typeof require !== 'undefined' ? require('../info/ini_get')('locutus.parse_url.mode') : undefined) || 'php'
602
603         var key = [
604                 'source',
605                 'scheme',
606                 'authority',
607                 'userInfo',
608                 'user',
609                 'pass',
610                 'host',
611                 'port',
612                 'relative',
613                 'path',
614                 'directory',
615                 'file',
616                 'query',
617                 'fragment'
618         ]
619
620         // For loose we added one optional slash to post-scheme to catch file:/// (should restrict this)
621         var parser = {
622                 php: new RegExp([
623                         '(?:([^:\\/?#]+):)?',
624                         '(?:\\/\\/()(?:(?:()(?:([^:@\\/]*):?([^:@\\/]*))?@)?([^:\\/?#]*)(?::(\\d*))?))?',
625                         '()',
626                         '(?:(()(?:(?:[^?#\\/]*\\/)*)()(?:[^?#]*))(?:\\?([^#]*))?(?:#(.*))?)'
627                 ].join('')),
628                 strict: new RegExp([
629                         '(?:([^:\\/?#]+):)?',
630                         '(?:\\/\\/((?:(([^:@\\/]*):?([^:@\\/]*))?@)?([^:\\/?#]*)(?::(\\d*))?))?',
631                         '((((?:[^?#\\/]*\\/)*)([^?#]*))(?:\\?([^#]*))?(?:#(.*))?)'
632                 ].join('')),
633                 loose: new RegExp([
634                         '(?:(?![^:@]+:[^:@\\/]*@)([^:\\/?#.]+):)?',
635                         '(?:\\/\\/\\/?)?',
636                         '((?:(([^:@\\/]*):?([^:@\\/]*))?@)?([^:\\/?#]*)(?::(\\d*))?)',
637                         '(((\\/(?:[^?#](?![^?#\\/]*\\.[^?#\\/.]+(?:[?#]|$)))*\\/?)?([^?#\\/]*))',
638                         '(?:\\?([^#]*))?(?:#(.*))?)'
639                 ].join(''))
640         }
641
642         var m = parser[mode].exec(str)
643         var uri = {}
644         var i = 14
645
646         while (i--) {
647                 if (m[i]) {
648                         uri[key[i]] = m[i]
649                 }
650         }
651
652         if (component) {
653                 return uri[component.replace('PHP_URL_', '').toLowerCase()]
654         }
655
656         if (mode !== 'php') {
657                 var name = (typeof require !== 'undefined' ? require('../info/ini_get')('locutus.parse_url.queryKey') : undefined) || 'queryKey'
658                 parser = /(?:^|&)([^&=]*)=?([^&]*)/g
659                 uri[name] = {}
660                 query = uri[key[12]] || ''
661                 query.replace(parser, function ($0, $1, $2) {
662                         if ($1) {
663                                 uri[name][$1] = $2
664                         }
665                 })
666         }
667
668         delete uri.source
669         return uri
670 }
671
672 // trim function to replace whithespace after the string
673 String.prototype.rtrim = function() {
674         var trimmed = this.replace(/\s+$/g, '');
675         return trimmed;
676 };
677
678 /**
679  * Scroll the screen to the item element whose id is provided, then highlights it
680  *
681  * Note: jquery.color.js is required
682  *
683  * @param {string} elementId The item element id
684  * @returns {undefined}
685  */
686 function scrollToItem(elementId) {
687         if (typeof elementId === "undefined") {
688                 return;
689         }
690
691         var $el = $('#' + elementId +  ' > .media');
692         // Test if the Item exists
693         if (!$el.length) {
694                 return;
695         }
696
697         // Define the colors which are used for highlighting
698         var colWhite = {backgroundColor:'#F5F5F5'};
699         var colShiny = {backgroundColor:'#FFF176'};
700
701         // Get the Item Position (we need to substract 100 to match correct position
702         var itemPos = $el.offset().top - 100;
703
704         // Scroll to the DIV with the ID (GUID)
705         $('html, body').animate({
706                 scrollTop: itemPos
707         }, 400).promise().done( function() {
708                 // Highlight post/commenent with ID  (GUID)
709                 $el.animate(colWhite, 1000).animate(colShiny).animate({backgroundColor: 'transparent'}, 600);
710         });
711 }
712
713 // format a html string to pure text
714 function htmlToText(htmlString) {
715         // Replace line breaks with spaces
716         var text = htmlString.replace(/<br>/g, ' ');
717         // Strip the text out of the html string
718         text = text.replace(/<[^>]*>/g, '');
719
720         return text;
721 }
722
723 /**
724  * Sends a /like API call and updates the display of the relevant action button
725  * before the update reloads the item.
726  *
727  * @param {int}     ident The id of the relevant item
728  * @param {string}  verb  The verb of the action
729  * @param {boolean} un    Whether to perform an activity removal instead of creation
730  */
731 function doLikeAction(ident, verb, un) {
732         if (verb.indexOf('attend') === 0) {
733                 $('.item-' + ident + ' .button-event:not(#' + verb + '-' + ident + ')').removeClass('active');
734         }
735         $('#' + verb + '-' + ident).toggleClass('active');
736
737         dolike(ident, verb, un);
738 }
739
740 // Decodes a hexadecimally encoded binary string
741 function hex2bin (s) {
742         //  discuss at: http://locutus.io/php/hex2bin/
743         // original by: Dumitru Uzun (http://duzun.me)
744         //   example 1: hex2bin('44696d61')
745         //   returns 1: 'Dima'
746         //   example 2: hex2bin('00')
747         //   returns 2: '\x00'
748         //   example 3: hex2bin('2f1q')
749         //   returns 3: false
750         var ret = [];
751         var i = 0;
752         var l;
753         s += '';
754
755         for (l = s.length; i < l; i += 2) {
756                 var c = parseInt(s.substr(i, 1), 16);
757                 var k = parseInt(s.substr(i + 1, 1), 16);
758                 if (isNaN(c) || isNaN(k)) {
759                         return false;
760                 }
761                 ret.push((c << 4) | k);
762         }
763         return String.fromCharCode.apply(String, ret);
764 }
765
766 // Convert binary data into hexadecimal representation
767 function bin2hex (s) {
768         // From: http://phpjs.org/functions
769         // +   original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
770         // +   bugfixed by: Onno Marsman
771         // +   bugfixed by: Linuxworld
772         // +   improved by: ntoniazzi (http://phpjs.org/functions/bin2hex:361#comment_177616)
773         // *     example 1: bin2hex('Kev');
774         // *     returns 1: '4b6576'
775         // *     example 2: bin2hex(String.fromCharCode(0x00));
776         // *     returns 2: '00'
777
778         var i, l, o = "", n;
779
780         s += "";
781
782         for (i = 0, l = s.length; i < l; i++) {
783                 n = s.charCodeAt(i).toString(16);
784                 o += n.length < 2 ? "0" + n : n;
785         }
786
787         return o;
788 }
789
790 // Dropdown menus with the class "dropdown-head" will display the active tab
791 // as button text
792 function toggleDropdownText(elm) {
793                 $(elm).closest(".dropdown").find('.btn').html($(elm).text() + ' <span class="caret"></span>');
794                 $(elm).closest(".dropdown").find('.btn').val($(elm).data('value'));
795                 $(elm).closest("ul").children("li").show();
796                 $(elm).parent("li").hide();
797 }
798
799 // Check if element does have a specific class
800 function hasClass(elem, cls) {
801         return (" " + elem.className + " " ).indexOf( " "+cls+" " ) > -1;
802 }
803 // @license-end