aboutsummaryrefslogtreecommitdiffhomepage
path: root/tpl/default/js/shaarli.js
diff options
context:
space:
mode:
Diffstat (limited to 'tpl/default/js/shaarli.js')
-rw-r--r--tpl/default/js/shaarli.js312
1 files changed, 286 insertions, 26 deletions
diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js
index 30d8ed6f..4ebb7815 100644
--- a/tpl/default/js/shaarli.js
+++ b/tpl/default/js/shaarli.js
@@ -76,9 +76,12 @@ window.onload = function () {
76 } 76 }
77 } 77 }
78 78
79 document.getElementById('menu-toggle').addEventListener('click', function (e) { 79 var menuToggle = document.getElementById('menu-toggle');
80 toggleMenu(); 80 if (menuToggle != null) {
81 }); 81 menuToggle.addEventListener('click', function (e) {
82 toggleMenu();
83 });
84 }
82 85
83 window.addEventListener(WINDOW_CHANGE_EVENT, closeMenu); 86 window.addEventListener(WINDOW_CHANGE_EVENT, closeMenu);
84 })(this, this.document); 87 })(this, this.document);
@@ -213,14 +216,14 @@ window.onload = function () {
213 /** 216 /**
214 * Autofocus text fields 217 * Autofocus text fields
215 */ 218 */
216 // ES6 syntax 219 var autofocusElements = document.querySelectorAll('.autofocus');
217 let autofocusElements = document.querySelectorAll('.autofocus'); 220 var breakLoop = false;
218 for (let autofocusElement of autofocusElements) { 221 [].forEach.call(autofocusElements, function(autofocusElement) {
219 if (autofocusElement.value == '') { 222 if (autofocusElement.value == '' && ! breakLoop) {
220 autofocusElement.focus(); 223 autofocusElement.focus();
221 break; 224 breakLoop = true;
222 } 225 }
223 } 226 });
224 227
225 /** 228 /**
226 * Handle sub menus/forms 229 * Handle sub menus/forms
@@ -255,10 +258,9 @@ window.onload = function () {
255 * Remove CSS target padding (for fixed bar) 258 * Remove CSS target padding (for fixed bar)
256 */ 259 */
257 if (location.hash != '') { 260 if (location.hash != '') {
258 var anchor = document.querySelector(location.hash); 261 var anchor = document.getElementById(location.hash.substr(1));
259 if (anchor != null) { 262 if (anchor != null) {
260 var padsize = anchor.clientHeight; 263 var padsize = anchor.clientHeight;
261 console.log(document.querySelector(location.hash).clientHeight);
262 this.window.scroll(0, this.window.scrollY - padsize); 264 this.window.scroll(0, this.window.scrollY - padsize);
263 anchor.style.paddingTop = 0; 265 anchor.style.paddingTop = 0;
264 } 266 }
@@ -300,21 +302,6 @@ window.onload = function () {
300 } 302 }
301 303
302 /** 304 /**
303 * TimeZome select
304 * FIXME! way too hackish
305 */
306 var toRemove = document.getElementById('timezone-remove');
307 if (toRemove != null) {
308 var firstSelect = toRemove.getElementsByTagName('select')[0];
309 var secondSelect = toRemove.getElementsByTagName('select')[1];
310 toRemove.parentNode.removeChild(toRemove);
311 var toAdd = document.getElementById('timezone-add');
312 var newTimezone = '<span class="timezone-continent">Continent ' + firstSelect.outerHTML + '</span>';
313 newTimezone += ' <span class="timezone-country">Country ' + secondSelect.outerHTML + '</span>';
314 toAdd.innerHTML = newTimezone;
315 }
316
317 /**
318 * Awesomplete trigger. 305 * Awesomplete trigger.
319 */ 306 */
320 var tags = document.getElementById('lf_tags'); 307 var tags = document.getElementById('lf_tags');
@@ -366,8 +353,256 @@ window.onload = function () {
366 } 353 }
367 }); 354 });
368 }); 355 });
356
357 var continent = document.getElementById('continent');
358 var city = document.getElementById('city');
359 if (continent != null && city != null) {
360 continent.addEventListener('change', function (event) {
361 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
362 });
363 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
364 }
365
366 /**
367 * Bulk actions
368 */
369 var linkCheckboxes = document.querySelectorAll('.delete-checkbox');
370 var bar = document.getElementById('actions');
371 [].forEach.call(linkCheckboxes, function(checkbox) {
372 checkbox.style.display = 'block';
373 checkbox.addEventListener('click', function(event) {
374 var count = 0;
375 var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
376 [].forEach.call(linkCheckedCheckboxes, function(checkbox) {
377 count++;
378 });
379 if (count == 0 && bar.classList.contains('open')) {
380 bar.classList.toggle('open');
381 } else if (count > 0 && ! bar.classList.contains('open')) {
382 bar.classList.toggle('open');
383 }
384 });
385 });
386
387 var deleteButton = document.getElementById('actions-delete');
388 var token = document.querySelector('input[type="hidden"][name="token"]');
389 if (deleteButton != null && token != null) {
390 deleteButton.addEventListener('click', function(event) {
391 event.preventDefault();
392
393 var links = [];
394 var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
395 [].forEach.call(linkCheckedCheckboxes, function(checkbox) {
396 links.push({
397 'id': checkbox.value,
398 'title': document.querySelector('.linklist-item[data-id="'+ checkbox.value +'"] .linklist-link').innerHTML
399 });
400 });
401
402 var message = 'Are you sure you want to delete '+ links.length +' links?\n';
403 message += 'This action is IRREVERSIBLE!\n\nTitles:\n';
404 var ids = '';
405 links.forEach(function(item) {
406 message += ' - '+ item['title'] +'\n';
407 ids += item['id'] +'+';
408 });
409
410 if (window.confirm(message)) {
411 window.location = '?delete_link&lf_linkdate='+ ids +'&token='+ token.value;
412 }
413 });
414 }
415
416 /**
417 * Tag list operations
418 *
419 * TODO: support error code in the backend for AJAX requests
420 */
421 var existingTags = document.querySelector('input[name="taglist"]').value.split(' ');
422 var awesomepletes = [];
423
424 // Display/Hide rename form
425 var renameTagButtons = document.querySelectorAll('.rename-tag');
426 [].forEach.call(renameTagButtons, function(rename) {
427 rename.addEventListener('click', function(event) {
428 event.preventDefault();
429 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
430 var form = block.querySelector('.rename-tag-form');
431 if (form.style.display == 'none' || form.style.display == '') {
432 form.style.display = 'block';
433 } else {
434 form.style.display = 'none';
435 }
436 block.querySelector('input').focus();
437 });
438 });
439
440 // Rename a tag with an AJAX request
441 var renameTagSubmits = document.querySelectorAll('.validate-rename-tag');
442 [].forEach.call(renameTagSubmits, function(rename) {
443 rename.addEventListener('click', function(event) {
444 event.preventDefault();
445 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
446 var input = block.querySelector('.rename-tag-input');
447 var totag = input.value.replace('/"/g', '\\"');
448 if (totag.trim() == '') {
449 return;
450 }
451 var fromtag = block.getAttribute('data-tag');
452 var token = document.getElementById('token').value;
453
454 xhr = new XMLHttpRequest();
455 xhr.open('POST', '?do=changetag');
456 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
457 xhr.onload = function() {
458 if (xhr.status !== 200) {
459 alert('An error occurred. Return code: '+ xhr.status);
460 location.reload();
461 } else {
462 block.setAttribute('data-tag', totag);
463 input.setAttribute('name', totag);
464 input.setAttribute('value', totag);
465 findParent(input, 'div', {'class': 'rename-tag-form'}).style.display = 'none';
466 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
467 block.querySelector('a.tag-link').setAttribute('href', '?searchtags='+ encodeURIComponent(totag));
468 block.querySelector('a.rename-tag').setAttribute('href', '?do=changetag&fromtag='+ encodeURIComponent(totag));
469
470 // Refresh awesomplete values
471 for (var key in existingTags) {
472 if (existingTags[key] == fromtag) {
473 existingTags[key] = totag;
474 }
475 }
476 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
477 }
478 };
479 xhr.send('renametag=1&fromtag='+ encodeURIComponent(fromtag) +'&totag='+ encodeURIComponent(totag) +'&token='+ token);
480 refreshToken();
481 });
482 });
483
484 // Validate input with enter key
485 var renameTagInputs = document.querySelectorAll('.rename-tag-input');
486 [].forEach.call(renameTagInputs, function(rename) {
487
488 rename.addEventListener('keypress', function(event) {
489 if (event.keyCode === 13) { // enter
490 findParent(event.target, 'div', {'class': 'tag-list-item'}).querySelector('.validate-rename-tag').click();
491 }
492 });
493 });
494
495 // Delete a tag with an AJAX query (alert popup confirmation)
496 var deleteTagButtons = document.querySelectorAll('.delete-tag');
497 [].forEach.call(deleteTagButtons, function(rename) {
498 rename.style.display = 'inline';
499 rename.addEventListener('click', function(event) {
500 event.preventDefault();
501 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
502 var tag = block.getAttribute('data-tag');
503 var token = document.getElementById('token').value;
504
505 if (confirm('Are you sure you want to delete the tag "'+ tag +'"?')) {
506 xhr = new XMLHttpRequest();
507 xhr.open('POST', '?do=changetag');
508 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
509 xhr.onload = function() {
510 block.remove();
511 };
512 xhr.send(encodeURI('deletetag=1&fromtag='+ tag +'&token='+ token));
513 refreshToken();
514 }
515 });
516 });
517
518 updateAwesompleteList('.rename-tag-input', document.querySelector('input[name="taglist"]').value.split(' '), awesomepletes);
369}; 519};
370 520
521/**
522 * Find a parent element according to its tag and its attributes
523 *
524 * @param element Element where to start the search
525 * @param tagName Expected parent tag name
526 * @param attributes Associative array of expected attributes (name=>value).
527 *
528 * @returns Found element or null.
529 */
530function findParent(element, tagName, attributes)
531{
532 while (element) {
533 if (element.tagName.toLowerCase() == tagName) {
534 var match = true;
535 for (var key in attributes) {
536 if (! element.hasAttribute(key)
537 || (attributes[key] != '' && element.getAttribute(key).indexOf(attributes[key]) == -1)
538 ) {
539 match = false;
540 break;
541 }
542 }
543
544 if (match) {
545 return element;
546 }
547 }
548 element = element.parentElement;
549 }
550 return null;
551}
552
553/**
554 * Ajax request to refresh the CSRF token.
555 */
556function refreshToken()
557{
558 var xhr = new XMLHttpRequest();
559 xhr.open('GET', '?do=token');
560 xhr.onload = function() {
561 var token = document.getElementById('token');
562 token.setAttribute('value', xhr.responseText);
563 };
564 xhr.send();
565}
566
567/**
568 * Update awesomplete list of tag for all elements matching the given selector
569 *
570 * @param selector CSS selector
571 * @param tags Array of tags
572 * @param instances List of existing awesomplete instances
573 */
574function updateAwesompleteList(selector, tags, instances)
575{
576 // First load: create Awesomplete instances
577 if (instances.length == 0) {
578 var elements = document.querySelectorAll(selector);
579 [].forEach.call(elements, function (element) {
580 instances.push(new Awesomplete(
581 element,
582 {'list': tags}
583 ));
584 });
585 } else {
586 // Update awesomplete tag list
587 for (var key in instances) {
588 instances[key].list = tags;
589 }
590 }
591 return instances;
592}
593
594/**
595 * html_entities in JS
596 *
597 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
598 */
599function htmlEntities(str)
600{
601 return str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
602 return '&#'+i.charCodeAt(0)+';';
603 });
604}
605
371function activateFirefoxSocial(node) { 606function activateFirefoxSocial(node) {
372 var loc = location.href; 607 var loc = location.href;
373 var baseURL = loc.substring(0, loc.lastIndexOf("/")); 608 var baseURL = loc.substring(0, loc.lastIndexOf("/"));
@@ -391,3 +626,28 @@ function activateFirefoxSocial(node) {
391 var activate = new CustomEvent("ActivateSocialFeature"); 626 var activate = new CustomEvent("ActivateSocialFeature");
392 node.dispatchEvent(activate); 627 node.dispatchEvent(activate);
393} 628}
629
630/**
631 * Add the class 'hidden' to city options not attached to the current selected continent.
632 *
633 * @param cities List of <option> elements
634 * @param currentContinent Current selected continent
635 * @param reset Set to true to reset the selected value
636 */
637function hideTimezoneCities(cities, currentContinent) {
638 var first = true;
639 if (reset == null) {
640 reset = false;
641 }
642 [].forEach.call(cities, function (option) {
643 if (option.getAttribute('data-continent') != currentContinent) {
644 option.className = 'hidden';
645 } else {
646 option.className = '';
647 if (reset === true && first === true) {
648 option.setAttribute('selected', 'selected');
649 first = false;
650 }
651 }
652 });
653}