]>
git.immae.eu Git - github/shaarli/Shaarli.git/blob - tpl/default/js/shaarli.js
1 /** @licstart The following is the entire license notice for the
2 * JavaScript code in this page.
4 * Copyright: (c) 2011-2015 Sébastien SAUVAGE <sebsauvage@sebsauvage.net>
5 * (c) 2011-2017 The Shaarli Community, see AUTHORS
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.
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:
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.
20 * 2. Altered source versions must be plainly marked as such, and must
21 * not be misrepresented as being the original software.
23 * 3. This notice may not be removed or altered from any source distribution.
25 * @licend The above is the entire license notice
26 * for the JavaScript code in this page.
29 window
.onload = function () {
32 * Retrieve an element up in the tree from its class name.
34 function getParentByClass(el
, className
) {
35 var p
= el
.parentNode
;
36 if (p
== null || p
.classList
.contains(className
)) {
39 return getParentByClass(p
, className
);
44 * Handle responsive menu.
45 * Source: http://purecss.io/layouts/tucked-menu-vertical/
47 (function (window
, document
) {
48 var menu
= document
.getElementById('shaarli-menu'),
49 WINDOW_CHANGE_EVENT
= ('onorientationchange' in window
) ? 'orientationchange':'resize';
51 function toggleHorizontal() {
53 document
.getElementById('shaarli-menu').querySelectorAll('.menu-transform'),
55 el
.classList
.toggle('pure-menu-horizontal');
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);
69 menu
.classList
.toggle('open');
70 document
.getElementById('menu-toggle').classList
.toggle('x');
73 function closeMenu() {
74 if (menu
.classList
.contains('open')) {
79 var menuToggle
= document
.getElementById('menu-toggle');
80 if (menuToggle
!= null) {
81 menuToggle
.addEventListener('click', function (e
) {
86 window
.addEventListener(WINDOW_CHANGE_EVENT
, closeMenu
);
87 })(this, this.document
);
90 * Fold/Expand shaares description and thumbnail.
92 var foldAllButtons
= document
.getElementsByClassName('fold-all');
93 var foldButtons
= document
.getElementsByClassName('fold-button');
95 [].forEach
.call(foldButtons
, function (foldButton
) {
96 // Retrieve description
97 var description
= 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';
108 foldButton
.addEventListener('click', function (event
) {
109 event
.preventDefault();
110 toggleFold(event
.target
, description
, thumbnail
);
114 if (foldAllButtons
!= null) {
115 [].forEach
.call(foldAllButtons
, function (foldAllButton
) {
116 foldAllButton
.addEventListener('click', function (event
) {
117 event
.preventDefault();
118 var state
= foldAllButton
.firstElementChild
.getAttribute('class').indexOf('down') != -1 ? 'down' : 'up';
119 [].forEach
.call(foldButtons
, function (foldButton
) {
120 if (foldButton
.firstElementChild
.classList
.contains('fa-chevron-up') && state
== 'down'
121 || foldButton
.firstElementChild
.classList
.contains('fa-chevron-down') && state
== 'up'
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';
137 toggleFold(foldButton
.firstElementChild
, description
, thumbnail
);
139 foldAllButton
.firstElementChild
.classList
.toggle('fa-chevron-down');
140 foldAllButton
.firstElementChild
.classList
.toggle('fa-chevron-up');
141 foldAllButton
.title
= state
=== 'down'
142 ? document
.getElementById('translation-fold-all').innerHTML
143 : document
.getElementById('translation-expand-all').innerHTML
148 function toggleFold(button
, description
, thumb
)
150 // Switch fold/expand - up = fold
151 if (button
.classList
.contains('fa-chevron-up')) {
152 button
.title
= document
.getElementById('translation-expand').innerHTML
;
153 if (description
!= null) {
154 description
.style
.display
= 'none';
157 thumb
.style
.display
= 'none';
161 button
.title
= document
.getElementById('translation-fold').innerHTML
;
162 if (description
!= null) {
163 description
.style
.display
= 'block';
166 thumb
.style
.display
= 'block';
169 button
.classList
.toggle('fa-chevron-down');
170 button
.classList
.toggle('fa-chevron-up');
174 * Confirmation message before deletion.
176 var deleteLinks
= document
.querySelectorAll('.confirm-delete');
177 [].forEach
.call(deleteLinks
, function(deleteLink
) {
178 deleteLink
.addEventListener('click', function(event
) {
179 if(! confirm(document
.getElementById('translation-delete-link').innerHTML
)) {
180 event
.preventDefault();
188 var closeLinks
= document
.querySelectorAll('.pure-alert-close');
189 [].forEach
.call(closeLinks
, function(closeLink
) {
190 closeLink
.addEventListener('click', function(event
) {
191 var alert
= getParentByClass(event
.target
, 'pure-alert-closable');
192 alert
.style
.display
= 'none';
197 * New version dismiss.
198 * Hide the message for one week using localStorage.
200 var newVersionDismiss
= document
.getElementById('new-version-dismiss');
201 var newVersionMessage
= document
.querySelector('.new-version-message');
202 if (newVersionMessage
!= null
203 && localStorage
.getItem('newVersionDismiss') != null
204 && parseInt(localStorage
.getItem('newVersionDismiss')) + 7*24*60*60*1000 > (new Date()).getTime()
206 newVersionMessage
.style
.display
= 'none';
208 if (newVersionDismiss
!= null) {
209 newVersionDismiss
.addEventListener('click', function () {
210 localStorage
.setItem('newVersionDismiss', (new Date()).getTime());
214 var hiddenReturnurl
= document
.getElementsByName('returnurl');
215 if (hiddenReturnurl
!= null) {
216 hiddenReturnurl
.value
= window
.location
.href
;
220 * Autofocus text fields
222 var autofocusElements
= document
.querySelectorAll('.autofocus');
223 var breakLoop
= false;
224 [].forEach
.call(autofocusElements
, function(autofocusElement
) {
225 if (autofocusElement
.value
== '' && ! breakLoop
) {
226 autofocusElement
.focus();
232 * Handle sub menus/forms
234 var openers
= document
.getElementsByClassName('subheader-opener');
235 if (openers
!= null) {
236 [].forEach
.call(openers
, function(opener
) {
237 opener
.addEventListener('click', function(event
) {
238 event
.preventDefault();
240 var id
= opener
.getAttribute('data-open-id');
241 var sub
= document
.getElementById(id
);
244 [].forEach
.call(document
.getElementsByClassName('subheader-form'), function (element
) {
245 if (element
!= sub
) {
246 removeClass(element
, 'open')
250 sub
.classList
.toggle('open');
256 function removeClass(element
, classname
) {
257 element
.className
= element
.className
.replace(new RegExp('(?:^|\\s)'+ classname
+ '(?:\\s|$)'), ' ');
261 * Remove CSS target padding (for fixed bar)
263 if (location
.hash
!= '') {
264 var anchor
= document
.getElementById(location
.hash
.substr(1));
265 if (anchor
!= null) {
266 var padsize
= anchor
.clientHeight
;
267 this.window
.scroll(0, this.window
.scrollY
- padsize
);
268 anchor
.style
.paddingTop
= 0;
275 var description
= document
.getElementById('lf_description');
276 var observe = function (element
, event
, handler
) {
277 element
.addEventListener(event
, handler
, false);
281 /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
282 var scrollTop
= window
.pageYOffset
||
283 (document
.documentElement
|| document
.body
.parentNode
|| document
.body
).scrollTop
;
285 description
.style
.height
= 'auto';
286 description
.style
.height
= description
.scrollHeight
+10+'px';
288 window
.scrollTo(0, scrollTop
);
290 /* 0-timeout to get the already changed text */
291 function delayedResize () {
292 window
.setTimeout(resize
, 0);
294 observe(description
, 'change', resize
);
295 observe(description
, 'cut', delayedResize
);
296 observe(description
, 'paste', delayedResize
);
297 observe(description
, 'drop', delayedResize
);
298 observe(description
, 'keydown', delayedResize
);
303 if (description
!= null) {
305 // Submit editlink form with CTRL + Enter in the text area.
306 description
.addEventListener('keydown', function (event
) {
307 if (event
.ctrlKey
&& event
.keyCode
=== 13) {
308 document
.getElementById('button-save-edit').click();
314 * Awesomplete trigger.
316 var tags
= document
.getElementById('lf_tags');
318 awesompleteUniqueTag('#lf_tags');
324 var picwall
= document
.getElementById('picwall_container');
325 if (picwall
!= null) {
326 var bLazy
= new Blazy();
332 var bookmarkletLinks
= document
.querySelectorAll('.bookmarklet-link');
333 var bkmMessage
= document
.getElementById('bookmarklet-alert');
334 [].forEach
.call(bookmarkletLinks
, function(link
) {
335 link
.addEventListener('click', function(event
) {
336 event
.preventDefault();
337 alert(bkmMessage
.value
);
344 var ffButton
= document
.getElementById('ff-social-button');
345 if (ffButton
!= null) {
346 ffButton
.addEventListener('click', function(event
) {
347 activateFirefoxSocial(event
.target
);
354 var orderPA
= document
.querySelectorAll('.order');
355 [].forEach
.call(orderPA
, function(link
) {
356 link
.addEventListener('click', function(event
) {
357 event
.preventDefault();
358 if (event
.target
.classList
.contains('order-up')) {
359 return orderUp(event
.target
.parentNode
.parentNode
.getAttribute('data-order'));
360 } else if (event
.target
.classList
.contains('order-down')) {
361 return orderDown(event
.target
.parentNode
.parentNode
.getAttribute('data-order'));
366 var continent
= document
.getElementById('continent');
367 var city
= document
.getElementById('city');
368 if (continent
!= null && city
!= null) {
369 continent
.addEventListener('change', function (event
) {
370 hideTimezoneCities(city
, continent
.options
[continent
.selectedIndex
].value
, true);
372 hideTimezoneCities(city
, continent
.options
[continent
.selectedIndex
].value
, false);
378 var linkCheckboxes
= document
.querySelectorAll('.delete-checkbox');
379 var bar
= document
.getElementById('actions');
380 [].forEach
.call(linkCheckboxes
, function(checkbox
) {
381 checkbox
.style
.display
= 'inline-block';
382 checkbox
.addEventListener('click', function(event
) {
384 var linkCheckedCheckboxes
= document
.querySelectorAll('.delete-checkbox:checked');
385 [].forEach
.call(linkCheckedCheckboxes
, function(checkbox
) {
388 if (count
== 0 && bar
.classList
.contains('open')) {
389 bar
.classList
.toggle('open');
390 } else if (count
> 0 && ! bar
.classList
.contains('open')) {
391 bar
.classList
.toggle('open');
396 var deleteButton
= document
.getElementById('actions-delete');
397 var token
= document
.querySelector('input[type="hidden"][name="token"]');
398 if (deleteButton
!= null && token
!= null) {
399 deleteButton
.addEventListener('click', function(event
) {
400 event
.preventDefault();
403 var linkCheckedCheckboxes
= document
.querySelectorAll('.delete-checkbox:checked');
404 [].forEach
.call(linkCheckedCheckboxes
, function(checkbox
) {
406 'id': checkbox
.value
,
407 'title': document
.querySelector('.linklist-item[data-id="'+ checkbox
.value
+'"] .linklist-link').innerHTML
411 var message
= 'Are you sure you want to delete '+ links
.length
+' links?\n';
412 message
+= 'This action is IRREVERSIBLE!\n\nTitles:\n';
414 links
.forEach(function(item
) {
415 message
+= ' - '+ item
['title'] +'\n';
416 ids
.push(item
['id']);
419 if (window
.confirm(message
)) {
420 window
.location
= '?delete_link&lf_linkdate='+ ids
.join('+') +'&token='+ token
.value
;
426 * Tag list operations
428 * TODO: support error code in the backend for AJAX requests
430 var tagList
= document
.querySelector('input[name="taglist"]');
431 var existingTags
= tagList
? tagList
.value
.split(' ') : [];
432 var awesomepletes
= [];
434 // Display/Hide rename form
435 var renameTagButtons
= document
.querySelectorAll('.rename-tag');
436 [].forEach
.call(renameTagButtons
, function(rename
) {
437 rename
.addEventListener('click', function(event
) {
438 event
.preventDefault();
439 var block
= findParent(event
.target
, 'div', {'class': 'tag-list-item'});
440 var form
= block
.querySelector('.rename-tag-form');
441 if (form
.style
.display
== 'none' || form
.style
.display
== '') {
442 form
.style
.display
= 'block';
444 form
.style
.display
= 'none';
446 block
.querySelector('input').focus();
450 // Rename a tag with an AJAX request
451 var renameTagSubmits
= document
.querySelectorAll('.validate-rename-tag');
452 [].forEach
.call(renameTagSubmits
, function(rename
) {
453 rename
.addEventListener('click', function(event
) {
454 event
.preventDefault();
455 var block
= findParent(event
.target
, 'div', {'class': 'tag-list-item'});
456 var input
= block
.querySelector('.rename-tag-input');
457 var totag
= input
.value
.replace('/"/g', '\\"');
458 if (totag
.trim() == '') {
461 var fromtag
= block
.getAttribute('data-tag');
462 var token
= document
.getElementById('token').value
;
464 xhr
= new XMLHttpRequest();
465 xhr
.open('POST', '?do=changetag');
466 xhr
.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
467 xhr
.onload = function() {
468 if (xhr
.status
!== 200) {
469 alert('An error occurred. Return code: '+ xhr
.status
);
472 block
.setAttribute('data-tag', totag
);
473 input
.setAttribute('name', totag
);
474 input
.setAttribute('value', totag
);
475 findParent(input
, 'div', {'class': 'rename-tag-form'}).style
.display
= 'none';
476 block
.querySelector('a.tag-link').innerHTML
= htmlEntities(totag
);
477 block
.querySelector('a.tag-link').setAttribute('href', '?searchtags='+ encodeURIComponent(totag
));
478 block
.querySelector('a.rename-tag').setAttribute('href', '?do=changetag&fromtag='+ encodeURIComponent(totag
));
480 // Refresh awesomplete values
481 for (var key
in existingTags
) {
482 if (existingTags
[key
] == fromtag
) {
483 existingTags
[key
] = totag
;
486 awesomepletes
= updateAwesompleteList('.rename-tag-input', existingTags
, awesomepletes
);
489 xhr
.send('renametag=1&fromtag='+ encodeURIComponent(fromtag
) +'&totag='+ encodeURIComponent(totag
) +'&token='+ token
);
494 // Validate input with enter key
495 var renameTagInputs
= document
.querySelectorAll('.rename-tag-input');
496 [].forEach
.call(renameTagInputs
, function(rename
) {
498 rename
.addEventListener('keypress', function(event
) {
499 if (event
.keyCode
=== 13) { // enter
500 findParent(event
.target
, 'div', {'class': 'tag-list-item'}).querySelector('.validate-rename-tag').click();
505 // Delete a tag with an AJAX query (alert popup confirmation)
506 var deleteTagButtons
= document
.querySelectorAll('.delete-tag');
507 [].forEach
.call(deleteTagButtons
, function(rename
) {
508 rename
.style
.display
= 'inline';
509 rename
.addEventListener('click', function(event
) {
510 event
.preventDefault();
511 var block
= findParent(event
.target
, 'div', {'class': 'tag-list-item'});
512 var tag
= block
.getAttribute('data-tag');
513 var token
= document
.getElementById('token').value
;
515 if (confirm('Are you sure you want to delete the tag "'+ tag
+'"?')) {
516 xhr
= new XMLHttpRequest();
517 xhr
.open('POST', '?do=changetag');
518 xhr
.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
519 xhr
.onload = function() {
522 xhr
.send(encodeURI('deletetag=1&fromtag='+ tag
+'&token='+ token
));
528 updateAwesompleteList('.rename-tag-input', existingTags
, awesomepletes
);
532 * Find a parent element according to its tag and its attributes
534 * @param element Element where to start the search
535 * @param tagName Expected parent tag name
536 * @param attributes Associative array of expected attributes (name=>value).
538 * @returns Found element or null.
540 function findParent(element
, tagName
, attributes
)
543 if (element
.tagName
.toLowerCase() == tagName
) {
545 for (var key
in attributes
) {
546 if (! element
.hasAttribute(key
)
547 || (attributes
[key
] != '' && element
.getAttribute(key
).indexOf(attributes
[key
]) == -1)
558 element
= element
.parentElement
;
564 * Ajax request to refresh the CSRF token.
566 function refreshToken()
568 var xhr
= new XMLHttpRequest();
569 xhr
.open('GET', '?do=token');
570 xhr
.onload = function() {
571 var token
= document
.getElementById('token');
572 token
.setAttribute('value', xhr
.responseText
);
578 * Update awesomplete list of tag for all elements matching the given selector
580 * @param selector CSS selector
581 * @param tags Array of tags
582 * @param instances List of existing awesomplete instances
584 function updateAwesompleteList(selector
, tags
, instances
)
586 // First load: create Awesomplete instances
587 if (instances
.length
== 0) {
588 var elements
= document
.querySelectorAll(selector
);
589 [].forEach
.call(elements
, function (element
) {
590 instances
.push(new Awesomplete(
596 // Update awesomplete tag list
597 for (var key
in instances
) {
598 instances
[key
].list
= tags
;
605 * html_entities in JS
607 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
609 function htmlEntities(str
)
611 return str
.replace(/[\u00A0-\u9999<>\&]/gim, function(i
) {
612 return '&#'+i
.charCodeAt(0)+';';
616 function activateFirefoxSocial(node
) {
617 var loc
= location
.href
;
618 var baseURL
= loc
.substring(0, loc
.lastIndexOf("/") + 1);
619 var title
= document
.title
;
621 // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
624 description: document
.getElementById('translation-delete-link').innerHTML
,
628 iconURL: baseURL
+ "/images/favicon.ico",
629 icon32URL: baseURL
+ "/images/favicon.ico",
630 icon64URL: baseURL
+ "/images/favicon.ico",
632 shareURL: baseURL
+ "?post=%{url}&title=%{title}&description=%{text}&source=firefoxsocialapi",
635 node
.setAttribute("data-service", JSON
.stringify(data
));
637 var activate
= new CustomEvent("ActivateSocialFeature");
638 node
.dispatchEvent(activate
);
642 * Add the class 'hidden' to city options not attached to the current selected continent.
644 * @param cities List of <option> elements
645 * @param currentContinent Current selected continent
646 * @param reset Set to true to reset the selected value
648 function hideTimezoneCities(cities
, currentContinent
) {
653 [].forEach
.call(cities
, function (option
) {
654 if (option
.getAttribute('data-continent') != currentContinent
) {
655 option
.className
= 'hidden';
657 option
.className
= '';
658 if (reset
=== true && first
=== true) {
659 option
.setAttribute('selected', 'selected');