]> git.immae.eu Git - github/shaarli/Shaarli.git/blame - tpl/default/js/shaarli.js
Fixing "Uncaught TypeError" in shaarli.js - fix #893
[github/shaarli/Shaarli.git] / tpl / default / js / shaarli.js
CommitLineData
b9b41d25
A
1/** @licstart The following is the entire license notice for the
2 * JavaScript code in this page.
3 *
4 * Copyright: (c) 2011-2015 Sébastien SAUVAGE <sebsauvage@sebsauvage.net>
5 * (c) 2011-2017 The Shaarli Community, see AUTHORS
6 *
7 * This software is provided 'as-is', without any express or implied warranty.
8 * In no event will the authors be held liable for any damages arising from
9 * the use of this software.
10 *
11 * Permission is granted to anyone to use this software for any purpose,
12 * including commercial applications, and to alter it and redistribute it
13 * freely, subject to the following restrictions:
14 *
15 * 1. The origin of this software must not be misrepresented; you must not
16 * claim that you wrote the original software. If you use this software
17 * in a product, an acknowledgment in the product documentation would
18 * be appreciated but is not required.
19 *
20 * 2. Altered source versions must be plainly marked as such, and must
21 * not be misrepresented as being the original software.
22 *
23 * 3. This notice may not be removed or altered from any source distribution.
24 *
25 * @licend The above is the entire license notice
26 * for the JavaScript code in this page.
27 */
28
402b0346
A
29window.onload = function () {
30
31 /**
32 * Retrieve an element up in the tree from its class name.
33 */
34 function getParentByClass(el, className) {
35 var p = el.parentNode;
36 if (p == null || p.classList.contains(className)) {
37 return p;
38 }
39 return getParentByClass(p, className);
40 }
41
42
43 /**
44 * Handle responsive menu.
45 * Source: http://purecss.io/layouts/tucked-menu-vertical/
46 */
47 (function (window, document) {
48 var menu = document.getElementById('shaarli-menu'),
49 WINDOW_CHANGE_EVENT = ('onorientationchange' in window) ? 'orientationchange':'resize';
50
51 function toggleHorizontal() {
52 [].forEach.call(
53 document.getElementById('shaarli-menu').querySelectorAll('.menu-transform'),
54 function(el){
55 el.classList.toggle('pure-menu-horizontal');
56 }
57 );
58 };
59
60 function toggleMenu() {
61 // set timeout so that the panel has a chance to roll up
62 // before the menu switches states
63 if (menu.classList.contains('open')) {
64 setTimeout(toggleHorizontal, 500);
65 }
66 else {
67 toggleHorizontal();
68 }
69 menu.classList.toggle('open');
70 document.getElementById('menu-toggle').classList.toggle('x');
71 };
72
73 function closeMenu() {
74 if (menu.classList.contains('open')) {
75 toggleMenu();
76 }
77 }
78
a0737313
A
79 var menuToggle = document.getElementById('menu-toggle');
80 if (menuToggle != null) {
81 menuToggle.addEventListener('click', function (e) {
82 toggleMenu();
83 });
84 }
402b0346
A
85
86 window.addEventListener(WINDOW_CHANGE_EVENT, closeMenu);
87 })(this, this.document);
88
89 /**
90 * Fold/Expand shaares description and thumbnail.
91 */
92 var foldAllButtons = document.getElementsByClassName('fold-all');
93 var foldButtons = document.getElementsByClassName('fold-button');
94
95 [].forEach.call(foldButtons, function (foldButton) {
96 // Retrieve description
97 var description = null;
98 var thumbnail = null;
99 var linklistItem = getParentByClass(foldButton, 'linklist-item');
100 if (linklistItem != null) {
101 description = linklistItem.querySelector('.linklist-item-description');
102 thumbnail = linklistItem.querySelector('.linklist-item-thumbnail');
103 if (description != null || thumbnail != null) {
104 foldButton.style.display = 'inline';
105 }
106 }
107
108 foldButton.addEventListener('click', function (event) {
109 event.preventDefault();
110 toggleFold(event.target, description, thumbnail);
111 });
112 });
113
114 if (foldAllButtons != null) {
115 [].forEach.call(foldAllButtons, function (foldAllButton) {
116 foldAllButton.addEventListener('click', function (event) {
117 event.preventDefault();
70401690 118 var state = foldAllButton.firstElementChild.getAttribute('class').indexOf('down') != -1 ? 'down' : 'up';
402b0346 119 [].forEach.call(foldButtons, function (foldButton) {
70401690
A
120 if (foldButton.firstElementChild.classList.contains('fa-chevron-up') && state == 'down'
121 || foldButton.firstElementChild.classList.contains('fa-chevron-down') && state == 'up'
122 ) {
123 return;
124 }
402b0346
A
125 // Retrieve description
126 var description = null;
127 var thumbnail = null;
128 var linklistItem = getParentByClass(foldButton, 'linklist-item');
129 if (linklistItem != null) {
130 description = linklistItem.querySelector('.linklist-item-description');
131 thumbnail = linklistItem.querySelector('.linklist-item-thumbnail');
132 if (description != null || thumbnail != null) {
133 foldButton.style.display = 'inline';
134 }
135 }
136
137 toggleFold(foldButton.firstElementChild, description, thumbnail);
138 });
139 foldAllButton.firstElementChild.classList.toggle('fa-chevron-down');
140 foldAllButton.firstElementChild.classList.toggle('fa-chevron-up');
141 });
142 });
143 }
144
145 function toggleFold(button, description, thumb)
146 {
147 // Switch fold/expand - up = fold
148 if (button.classList.contains('fa-chevron-up')) {
149 button.title = 'Expand';
150 if (description != null) {
151 description.style.display = 'none';
152 }
153 if (thumb != null) {
154 thumb.style.display = 'none';
155 }
156 }
157 else {
158 button.title = 'Fold';
159 if (description != null) {
160 description.style.display = 'block';
161 }
162 if (thumb != null) {
163 thumb.style.display = 'block';
164 }
165 }
166 button.classList.toggle('fa-chevron-down');
167 button.classList.toggle('fa-chevron-up');
168 }
169
170 /**
171 * Confirmation message before deletion.
172 */
173 var deleteLinks = document.querySelectorAll('.confirm-delete');
174 [].forEach.call(deleteLinks, function(deleteLink) {
175 deleteLink.addEventListener('click', function(event) {
176 if(! confirm('Are you sure you want to delete this link ?')) {
177 event.preventDefault();
178 }
179 });
180 });
181
182 /**
183 * Close alerts
184 */
185 var closeLinks = document.querySelectorAll('.pure-alert-close');
186 [].forEach.call(closeLinks, function(closeLink) {
187 closeLink.addEventListener('click', function(event) {
188 var alert = getParentByClass(event.target, 'pure-alert-closable');
189 alert.style.display = 'none';
190 });
191 });
192
193 /**
194 * New version dismiss.
195 * Hide the message for one week using localStorage.
196 */
197 var newVersionDismiss = document.getElementById('new-version-dismiss');
198 var newVersionMessage = document.querySelector('.new-version-message');
199 if (newVersionMessage != null
200 && localStorage.getItem('newVersionDismiss') != null
201 && parseInt(localStorage.getItem('newVersionDismiss')) + 7*24*60*60*1000 > (new Date()).getTime()
202 ) {
203 newVersionMessage.style.display = 'none';
204 }
205 if (newVersionDismiss != null) {
206 newVersionDismiss.addEventListener('click', function () {
207 localStorage.setItem('newVersionDismiss', (new Date()).getTime());
208 });
209 }
210
211 var hiddenReturnurl = document.getElementsByName('returnurl');
212 if (hiddenReturnurl != null) {
213 hiddenReturnurl.value = window.location.href;
214 }
215
216 /**
217 * Autofocus text fields
218 */
63836498
A
219 var autofocusElements = document.querySelectorAll('.autofocus');
220 var breakLoop = false;
221 [].forEach.call(autofocusElements, function(autofocusElement) {
222 if (autofocusElement.value == '' && ! breakLoop) {
b9b41d25 223 autofocusElement.focus();
63836498 224 breakLoop = true;
b9b41d25 225 }
63836498 226 });
402b0346
A
227
228 /**
229 * Handle sub menus/forms
230 */
231 var openers = document.getElementsByClassName('subheader-opener');
232 if (openers != null) {
233 [].forEach.call(openers, function(opener) {
234 opener.addEventListener('click', function(event) {
235 event.preventDefault();
236
237 var id = opener.getAttribute('data-open-id');
238 var sub = document.getElementById(id);
239
240 if (sub != null) {
241 [].forEach.call(document.getElementsByClassName('subheader-form'), function (element) {
242 if (element != sub) {
243 removeClass(element, 'open')
244 }
245 });
246
247 sub.classList.toggle('open');
248 }
249 });
250 });
251 }
252
253 function removeClass(element, classname) {
254 element.className = element.className.replace(new RegExp('(?:^|\\s)'+ classname + '(?:\\s|$)'), ' ');
255 }
256
257 /**
258 * Remove CSS target padding (for fixed bar)
259 */
260 if (location.hash != '') {
0040058d 261 var anchor = document.getElementById(location.hash.substr(1));
402b0346
A
262 if (anchor != null) {
263 var padsize = anchor.clientHeight;
402b0346
A
264 this.window.scroll(0, this.window.scrollY - padsize);
265 anchor.style.paddingTop = 0;
266 }
267 }
70401690
A
268
269 /**
270 * Text area resizer
271 */
272 var description = document.getElementById('lf_description');
273 var observe = function (element, event, handler) {
274 element.addEventListener(event, handler, false);
275 };
276 function init () {
277 function resize () {
278 description.style.height = 'auto';
279 description.style.height = description.scrollHeight+10+'px';
280 }
281 /* 0-timeout to get the already changed text */
282 function delayedResize () {
283 window.setTimeout(resize, 0);
284 }
285 observe(description, 'change', resize);
286 observe(description, 'cut', delayedResize);
287 observe(description, 'paste', delayedResize);
288 observe(description, 'drop', delayedResize);
289 observe(description, 'keydown', delayedResize);
290
291 resize();
292 }
2dd698fd 293
70401690
A
294 if (description != null) {
295 init();
2dd698fd
A
296 // Submit editlink form with CTRL + Enter in the text area.
297 description.addEventListener('keydown', function (event) {
298 if (event.ctrlKey && event.keyCode === 13) {
299 document.getElementById('button-save-edit').click();
300 }
301 });
70401690 302 }
b9b41d25 303
b9b41d25
A
304 /**
305 * Awesomplete trigger.
306 */
307 var tags = document.getElementById('lf_tags');
308 if (tags != null) {
309 awesompleteUniqueTag('#lf_tags');
310 }
311
312 /**
313 * bLazy trigger
314 */
315 var picwall = document.getElementById('picwall_container');
316 if (picwall != null) {
317 var bLazy = new Blazy();
318 }
319
320 /**
321 * Bookmarklet alert
322 */
323 var bookmarkletLinks = document.querySelectorAll('.bookmarklet-link');
324 var bkmMessage = document.getElementById('bookmarklet-alert');
325 [].forEach.call(bookmarkletLinks, function(link) {
326 link.addEventListener('click', function(event) {
327 event.preventDefault();
328 alert(bkmMessage.value);
329 });
330 });
331
332 /**
333 * Firefox Social
334 */
335 var ffButton = document.getElementById('ff-social-button');
336 if (ffButton != null) {
337 ffButton.addEventListener('click', function(event) {
338 activateFirefoxSocial(event.target);
339 });
340 }
341
342 /**
343 * Plugin admin order
344 */
345 var orderPA = document.querySelectorAll('.order');
346 [].forEach.call(orderPA, function(link) {
347 link.addEventListener('click', function(event) {
348 event.preventDefault();
349 if (event.target.classList.contains('order-up')) {
350 return orderUp(event.target.parentNode.parentNode.getAttribute('data-order'));
351 } else if (event.target.classList.contains('order-down')) {
352 return orderDown(event.target.parentNode.parentNode.getAttribute('data-order'));
353 }
354 });
355 });
a0737313
A
356
357 var continent = document.getElementById('continent');
358 var city = document.getElementById('city');
359 if (continent != null && city != null) {
29a837f3 360 continent.addEventListener('change', function (event) {
a0737313
A
361 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
362 });
363 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
364 }
29a837f3
A
365
366 /**
367 * Bulk actions
29a837f3 368 */
63836498
A
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++;
29a837f3 378 });
63836498
A
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 });
29a837f3 386
63836498
A
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();
29a837f3 392
63836498
A
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 });
29a837f3 401
63836498
A
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'] +'+';
29a837f3 408 });
63836498
A
409
410 if (window.confirm(message)) {
411 window.location = '?delete_link&lf_linkdate='+ ids +'&token='+ token.value;
412 }
413 });
29a837f3 414 }
aa4797ba
A
415
416 /**
417 * Tag list operations
418 *
419 * TODO: support error code in the backend for AJAX requests
420 */
9bf82f4f
LC
421 var tagList = document.querySelector('input[name="taglist"]');
422 var existingTags = tagList ? tagList.value.split(' ') : [];
82e3bb5f
A
423 var awesomepletes = [];
424
aa4797ba
A
425 // Display/Hide rename form
426 var renameTagButtons = document.querySelectorAll('.rename-tag');
427 [].forEach.call(renameTagButtons, function(rename) {
428 rename.addEventListener('click', function(event) {
429 event.preventDefault();
430 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
431 var form = block.querySelector('.rename-tag-form');
82e3bb5f
A
432 if (form.style.display == 'none' || form.style.display == '') {
433 form.style.display = 'block';
434 } else {
435 form.style.display = 'none';
436 }
437 block.querySelector('input').focus();
aa4797ba
A
438 });
439 });
440
441 // Rename a tag with an AJAX request
442 var renameTagSubmits = document.querySelectorAll('.validate-rename-tag');
443 [].forEach.call(renameTagSubmits, function(rename) {
444 rename.addEventListener('click', function(event) {
445 event.preventDefault();
446 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
447 var input = block.querySelector('.rename-tag-input');
448 var totag = input.value.replace('/"/g', '\\"');
449 if (totag.trim() == '') {
450 return;
451 }
452 var fromtag = block.getAttribute('data-tag');
453 var token = document.getElementById('token').value;
454
455 xhr = new XMLHttpRequest();
456 xhr.open('POST', '?do=changetag');
457 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
458 xhr.onload = function() {
459 if (xhr.status !== 200) {
460 alert('An error occurred. Return code: '+ xhr.status);
461 location.reload();
462 } else {
463 block.setAttribute('data-tag', totag);
464 input.setAttribute('name', totag);
465 input.setAttribute('value', totag);
82e3bb5f 466 findParent(input, 'div', {'class': 'rename-tag-form'}).style.display = 'none';
aa4797ba
A
467 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
468 block.querySelector('a.tag-link').setAttribute('href', '?searchtags='+ encodeURIComponent(totag));
469 block.querySelector('a.rename-tag').setAttribute('href', '?do=changetag&fromtag='+ encodeURIComponent(totag));
82e3bb5f
A
470
471 // Refresh awesomplete values
472 for (var key in existingTags) {
473 if (existingTags[key] == fromtag) {
474 existingTags[key] = totag;
475 }
476 }
477 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
aa4797ba
A
478 }
479 };
480 xhr.send('renametag=1&fromtag='+ encodeURIComponent(fromtag) +'&totag='+ encodeURIComponent(totag) +'&token='+ token);
481 refreshToken();
482 });
483 });
484
485 // Validate input with enter key
486 var renameTagInputs = document.querySelectorAll('.rename-tag-input');
487 [].forEach.call(renameTagInputs, function(rename) {
82e3bb5f 488
aa4797ba
A
489 rename.addEventListener('keypress', function(event) {
490 if (event.keyCode === 13) { // enter
491 findParent(event.target, 'div', {'class': 'tag-list-item'}).querySelector('.validate-rename-tag').click();
492 }
493 });
494 });
495
496 // Delete a tag with an AJAX query (alert popup confirmation)
497 var deleteTagButtons = document.querySelectorAll('.delete-tag');
498 [].forEach.call(deleteTagButtons, function(rename) {
499 rename.style.display = 'inline';
500 rename.addEventListener('click', function(event) {
501 event.preventDefault();
502 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
503 var tag = block.getAttribute('data-tag');
504 var token = document.getElementById('token').value;
505
506 if (confirm('Are you sure you want to delete the tag "'+ tag +'"?')) {
507 xhr = new XMLHttpRequest();
508 xhr.open('POST', '?do=changetag');
509 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
510 xhr.onload = function() {
511 block.remove();
512 };
513 xhr.send(encodeURI('deletetag=1&fromtag='+ tag +'&token='+ token));
514 refreshToken();
515 }
516 });
517 });
82e3bb5f 518
9bf82f4f 519 updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
402b0346 520};
b9b41d25 521
82e3bb5f
A
522/**
523 * Find a parent element according to its tag and its attributes
524 *
525 * @param element Element where to start the search
526 * @param tagName Expected parent tag name
527 * @param attributes Associative array of expected attributes (name=>value).
528 *
529 * @returns Found element or null.
530 */
aa4797ba
A
531function findParent(element, tagName, attributes)
532{
533 while (element) {
534 if (element.tagName.toLowerCase() == tagName) {
535 var match = true;
536 for (var key in attributes) {
537 if (! element.hasAttribute(key)
538 || (attributes[key] != '' && element.getAttribute(key).indexOf(attributes[key]) == -1)
539 ) {
540 match = false;
541 break;
542 }
543 }
544
545 if (match) {
546 return element;
547 }
548 }
549 element = element.parentElement;
550 }
551 return null;
552}
553
82e3bb5f
A
554/**
555 * Ajax request to refresh the CSRF token.
556 */
aa4797ba
A
557function refreshToken()
558{
559 var xhr = new XMLHttpRequest();
560 xhr.open('GET', '?do=token');
561 xhr.onload = function() {
562 var token = document.getElementById('token');
563 token.setAttribute('value', xhr.responseText);
564 };
565 xhr.send();
566}
567
82e3bb5f
A
568/**
569 * Update awesomplete list of tag for all elements matching the given selector
570 *
571 * @param selector CSS selector
572 * @param tags Array of tags
573 * @param instances List of existing awesomplete instances
574 */
575function updateAwesompleteList(selector, tags, instances)
576{
577 // First load: create Awesomplete instances
578 if (instances.length == 0) {
579 var elements = document.querySelectorAll(selector);
580 [].forEach.call(elements, function (element) {
581 instances.push(new Awesomplete(
582 element,
583 {'list': tags}
584 ));
585 });
586 } else {
587 // Update awesomplete tag list
588 for (var key in instances) {
589 instances[key].list = tags;
590 }
591 }
592 return instances;
593}
594
aa4797ba
A
595/**
596 * html_entities in JS
597 *
598 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
599 */
600function htmlEntities(str)
601{
602 return str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
603 return '&#'+i.charCodeAt(0)+';';
604 });
605}
606
b9b41d25
A
607function activateFirefoxSocial(node) {
608 var loc = location.href;
609 var baseURL = loc.substring(0, loc.lastIndexOf("/"));
610
611 // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
612 var data = {
613 name: "{$shaarlititle}",
614 description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.",
615 author: "Shaarli",
616 version: "1.0.0",
617
618 iconURL: baseURL + "/images/favicon.ico",
619 icon32URL: baseURL + "/images/favicon.ico",
620 icon64URL: baseURL + "/images/favicon.ico",
621
622 shareURL: baseURL + "{noparse}?post=%{url}&title=%{title}&description=%{text}&source=firefoxsocialapi{/noparse}",
623 homepageURL: baseURL
624 };
625 node.setAttribute("data-service", JSON.stringify(data));
626
627 var activate = new CustomEvent("ActivateSocialFeature");
628 node.dispatchEvent(activate);
629}
a0737313
A
630
631/**
632 * Add the class 'hidden' to city options not attached to the current selected continent.
633 *
634 * @param cities List of <option> elements
635 * @param currentContinent Current selected continent
636 * @param reset Set to true to reset the selected value
637 */
aa4797ba 638function hideTimezoneCities(cities, currentContinent) {
a0737313 639 var first = true;
aa4797ba
A
640 if (reset == null) {
641 reset = false;
642 }
29a837f3 643 [].forEach.call(cities, function (option) {
a0737313
A
644 if (option.getAttribute('data-continent') != currentContinent) {
645 option.className = 'hidden';
646 } else {
647 option.className = '';
648 if (reset === true && first === true) {
649 option.setAttribute('selected', 'selected');
650 first = false;
651 }
652 }
653 });
654}