]>
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');
145 function toggleFold(button
, description
, thumb
)
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';
154 thumb
.style
.display
= 'none';
158 button
.title
= 'Fold';
159 if (description
!= null) {
160 description
.style
.display
= 'block';
163 thumb
.style
.display
= 'block';
166 button
.classList
.toggle('fa-chevron-down');
167 button
.classList
.toggle('fa-chevron-up');
171 * Confirmation message before deletion.
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();
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';
194 * New version dismiss.
195 * Hide the message for one week using localStorage.
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()
203 newVersionMessage
.style
.display
= 'none';
205 if (newVersionDismiss
!= null) {
206 newVersionDismiss
.addEventListener('click', function () {
207 localStorage
.setItem('newVersionDismiss', (new Date()).getTime());
211 var hiddenReturnurl
= document
.getElementsByName('returnurl');
212 if (hiddenReturnurl
!= null) {
213 hiddenReturnurl
.value
= window
.location
.href
;
217 * Autofocus text fields
219 var autofocusElements
= document
.querySelectorAll('.autofocus');
220 var breakLoop
= false;
221 [].forEach
.call(autofocusElements
, function(autofocusElement
) {
222 if (autofocusElement
.value
== '' && ! breakLoop
) {
223 autofocusElement
.focus();
229 * Handle sub menus/forms
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();
237 var id
= opener
.getAttribute('data-open-id');
238 var sub
= document
.getElementById(id
);
241 [].forEach
.call(document
.getElementsByClassName('subheader-form'), function (element
) {
242 if (element
!= sub
) {
243 removeClass(element
, 'open')
247 sub
.classList
.toggle('open');
253 function removeClass(element
, classname
) {
254 element
.className
= element
.className
.replace(new RegExp('(?:^|\\s)'+ classname
+ '(?:\\s|$)'), ' ');
258 * Remove CSS target padding (for fixed bar)
260 if (location
.hash
!= '') {
261 var anchor
= document
.getElementById(location
.hash
.substr(1));
262 if (anchor
!= null) {
263 var padsize
= anchor
.clientHeight
;
264 this.window
.scroll(0, this.window
.scrollY
- padsize
);
265 anchor
.style
.paddingTop
= 0;
272 var description
= document
.getElementById('lf_description');
273 var observe = function (element
, event
, handler
) {
274 element
.addEventListener(event
, handler
, false);
278 /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
279 var scrollTop
= window
.pageYOffset
||
280 (document
.documentElement
|| document
.body
.parentNode
|| document
.body
).scrollTop
;
282 description
.style
.height
= 'auto';
283 description
.style
.height
= description
.scrollHeight
+10+'px';
285 window
.scrollTo(0, scrollTop
);
287 /* 0-timeout to get the already changed text */
288 function delayedResize () {
289 window
.setTimeout(resize
, 0);
291 observe(description
, 'change', resize
);
292 observe(description
, 'cut', delayedResize
);
293 observe(description
, 'paste', delayedResize
);
294 observe(description
, 'drop', delayedResize
);
295 observe(description
, 'keydown', delayedResize
);
300 if (description
!= null) {
302 // Submit editlink form with CTRL + Enter in the text area.
303 description
.addEventListener('keydown', function (event
) {
304 if (event
.ctrlKey
&& event
.keyCode
=== 13) {
305 document
.getElementById('button-save-edit').click();
311 * Awesomplete trigger.
313 var tags
= document
.getElementById('lf_tags');
315 awesompleteUniqueTag('#lf_tags');
321 var picwall
= document
.getElementById('picwall_container');
322 if (picwall
!= null) {
323 var bLazy
= new Blazy();
329 var bookmarkletLinks
= document
.querySelectorAll('.bookmarklet-link');
330 var bkmMessage
= document
.getElementById('bookmarklet-alert');
331 [].forEach
.call(bookmarkletLinks
, function(link
) {
332 link
.addEventListener('click', function(event
) {
333 event
.preventDefault();
334 alert(bkmMessage
.value
);
341 var ffButton
= document
.getElementById('ff-social-button');
342 if (ffButton
!= null) {
343 ffButton
.addEventListener('click', function(event
) {
344 activateFirefoxSocial(event
.target
);
351 var orderPA
= document
.querySelectorAll('.order');
352 [].forEach
.call(orderPA
, function(link
) {
353 link
.addEventListener('click', function(event
) {
354 event
.preventDefault();
355 if (event
.target
.classList
.contains('order-up')) {
356 return orderUp(event
.target
.parentNode
.parentNode
.getAttribute('data-order'));
357 } else if (event
.target
.classList
.contains('order-down')) {
358 return orderDown(event
.target
.parentNode
.parentNode
.getAttribute('data-order'));
363 var continent
= document
.getElementById('continent');
364 var city
= document
.getElementById('city');
365 if (continent
!= null && city
!= null) {
366 continent
.addEventListener('change', function (event
) {
367 hideTimezoneCities(city
, continent
.options
[continent
.selectedIndex
].value
, true);
369 hideTimezoneCities(city
, continent
.options
[continent
.selectedIndex
].value
, false);
375 var linkCheckboxes
= document
.querySelectorAll('.delete-checkbox');
376 var bar
= document
.getElementById('actions');
377 [].forEach
.call(linkCheckboxes
, function(checkbox
) {
378 checkbox
.style
.display
= 'block';
379 checkbox
.addEventListener('click', function(event
) {
381 var linkCheckedCheckboxes
= document
.querySelectorAll('.delete-checkbox:checked');
382 [].forEach
.call(linkCheckedCheckboxes
, function(checkbox
) {
385 if (count
== 0 && bar
.classList
.contains('open')) {
386 bar
.classList
.toggle('open');
387 } else if (count
> 0 && ! bar
.classList
.contains('open')) {
388 bar
.classList
.toggle('open');
393 var deleteButton
= document
.getElementById('actions-delete');
394 var token
= document
.querySelector('input[type="hidden"][name="token"]');
395 if (deleteButton
!= null && token
!= null) {
396 deleteButton
.addEventListener('click', function(event
) {
397 event
.preventDefault();
400 var linkCheckedCheckboxes
= document
.querySelectorAll('.delete-checkbox:checked');
401 [].forEach
.call(linkCheckedCheckboxes
, function(checkbox
) {
403 'id': checkbox
.value
,
404 'title': document
.querySelector('.linklist-item[data-id="'+ checkbox
.value
+'"] .linklist-link').innerHTML
408 var message
= 'Are you sure you want to delete '+ links
.length
+' links?\n';
409 message
+= 'This action is IRREVERSIBLE!\n\nTitles:\n';
411 links
.forEach(function(item
) {
412 message
+= ' - '+ item
['title'] +'\n';
413 ids
.push(item
['id']);
416 if (window
.confirm(message
)) {
417 window
.location
= '?delete_link&lf_linkdate='+ ids
.join('+') +'&token='+ token
.value
;
423 * Tag list operations
425 * TODO: support error code in the backend for AJAX requests
427 var tagList
= document
.querySelector('input[name="taglist"]');
428 var existingTags
= tagList
? tagList
.value
.split(' ') : [];
429 var awesomepletes
= [];
431 // Display/Hide rename form
432 var renameTagButtons
= document
.querySelectorAll('.rename-tag');
433 [].forEach
.call(renameTagButtons
, function(rename
) {
434 rename
.addEventListener('click', function(event
) {
435 event
.preventDefault();
436 var block
= findParent(event
.target
, 'div', {'class': 'tag-list-item'});
437 var form
= block
.querySelector('.rename-tag-form');
438 if (form
.style
.display
== 'none' || form
.style
.display
== '') {
439 form
.style
.display
= 'block';
441 form
.style
.display
= 'none';
443 block
.querySelector('input').focus();
447 // Rename a tag with an AJAX request
448 var renameTagSubmits
= document
.querySelectorAll('.validate-rename-tag');
449 [].forEach
.call(renameTagSubmits
, function(rename
) {
450 rename
.addEventListener('click', function(event
) {
451 event
.preventDefault();
452 var block
= findParent(event
.target
, 'div', {'class': 'tag-list-item'});
453 var input
= block
.querySelector('.rename-tag-input');
454 var totag
= input
.value
.replace('/"/g', '\\"');
455 if (totag
.trim() == '') {
458 var fromtag
= block
.getAttribute('data-tag');
459 var token
= document
.getElementById('token').value
;
461 xhr
= new XMLHttpRequest();
462 xhr
.open('POST', '?do=changetag');
463 xhr
.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
464 xhr
.onload = function() {
465 if (xhr
.status
!== 200) {
466 alert('An error occurred. Return code: '+ xhr
.status
);
469 block
.setAttribute('data-tag', totag
);
470 input
.setAttribute('name', totag
);
471 input
.setAttribute('value', totag
);
472 findParent(input
, 'div', {'class': 'rename-tag-form'}).style
.display
= 'none';
473 block
.querySelector('a.tag-link').innerHTML
= htmlEntities(totag
);
474 block
.querySelector('a.tag-link').setAttribute('href', '?searchtags='+ encodeURIComponent(totag
));
475 block
.querySelector('a.rename-tag').setAttribute('href', '?do=changetag&fromtag='+ encodeURIComponent(totag
));
477 // Refresh awesomplete values
478 for (var key
in existingTags
) {
479 if (existingTags
[key
] == fromtag
) {
480 existingTags
[key
] = totag
;
483 awesomepletes
= updateAwesompleteList('.rename-tag-input', existingTags
, awesomepletes
);
486 xhr
.send('renametag=1&fromtag='+ encodeURIComponent(fromtag
) +'&totag='+ encodeURIComponent(totag
) +'&token='+ token
);
491 // Validate input with enter key
492 var renameTagInputs
= document
.querySelectorAll('.rename-tag-input');
493 [].forEach
.call(renameTagInputs
, function(rename
) {
495 rename
.addEventListener('keypress', function(event
) {
496 if (event
.keyCode
=== 13) { // enter
497 findParent(event
.target
, 'div', {'class': 'tag-list-item'}).querySelector('.validate-rename-tag').click();
502 // Delete a tag with an AJAX query (alert popup confirmation)
503 var deleteTagButtons
= document
.querySelectorAll('.delete-tag');
504 [].forEach
.call(deleteTagButtons
, function(rename
) {
505 rename
.style
.display
= 'inline';
506 rename
.addEventListener('click', function(event
) {
507 event
.preventDefault();
508 var block
= findParent(event
.target
, 'div', {'class': 'tag-list-item'});
509 var tag
= block
.getAttribute('data-tag');
510 var token
= document
.getElementById('token').value
;
512 if (confirm('Are you sure you want to delete the tag "'+ tag
+'"?')) {
513 xhr
= new XMLHttpRequest();
514 xhr
.open('POST', '?do=changetag');
515 xhr
.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
516 xhr
.onload = function() {
519 xhr
.send(encodeURI('deletetag=1&fromtag='+ tag
+'&token='+ token
));
525 updateAwesompleteList('.rename-tag-input', existingTags
, awesomepletes
);
529 * Find a parent element according to its tag and its attributes
531 * @param element Element where to start the search
532 * @param tagName Expected parent tag name
533 * @param attributes Associative array of expected attributes (name=>value).
535 * @returns Found element or null.
537 function findParent(element
, tagName
, attributes
)
540 if (element
.tagName
.toLowerCase() == tagName
) {
542 for (var key
in attributes
) {
543 if (! element
.hasAttribute(key
)
544 || (attributes
[key
] != '' && element
.getAttribute(key
).indexOf(attributes
[key
]) == -1)
555 element
= element
.parentElement
;
561 * Ajax request to refresh the CSRF token.
563 function refreshToken()
565 var xhr
= new XMLHttpRequest();
566 xhr
.open('GET', '?do=token');
567 xhr
.onload = function() {
568 var token
= document
.getElementById('token');
569 token
.setAttribute('value', xhr
.responseText
);
575 * Update awesomplete list of tag for all elements matching the given selector
577 * @param selector CSS selector
578 * @param tags Array of tags
579 * @param instances List of existing awesomplete instances
581 function updateAwesompleteList(selector
, tags
, instances
)
583 // First load: create Awesomplete instances
584 if (instances
.length
== 0) {
585 var elements
= document
.querySelectorAll(selector
);
586 [].forEach
.call(elements
, function (element
) {
587 instances
.push(new Awesomplete(
593 // Update awesomplete tag list
594 for (var key
in instances
) {
595 instances
[key
].list
= tags
;
602 * html_entities in JS
604 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
606 function htmlEntities(str
)
608 return str
.replace(/[\u00A0-\u9999<>\&]/gim, function(i
) {
609 return '&#'+i
.charCodeAt(0)+';';
613 function activateFirefoxSocial(node
) {
614 var loc
= location
.href
;
615 var baseURL
= loc
.substring(0, loc
.lastIndexOf("/") + 1);
616 var title
= document
.title
;
618 // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
621 description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.",
625 iconURL: baseURL
+ "/images/favicon.ico",
626 icon32URL: baseURL
+ "/images/favicon.ico",
627 icon64URL: baseURL
+ "/images/favicon.ico",
629 shareURL: baseURL
+ "?post=%{url}&title=%{title}&description=%{text}&source=firefoxsocialapi",
632 node
.setAttribute("data-service", JSON
.stringify(data
));
634 var activate
= new CustomEvent("ActivateSocialFeature");
635 node
.dispatchEvent(activate
);
639 * Add the class 'hidden' to city options not attached to the current selected continent.
641 * @param cities List of <option> elements
642 * @param currentContinent Current selected continent
643 * @param reset Set to true to reset the selected value
645 function hideTimezoneCities(cities
, currentContinent
) {
650 [].forEach
.call(cities
, function (option
) {
651 if (option
.getAttribute('data-continent') != currentContinent
) {
652 option
.className
= 'hidden';
654 option
.className
= '';
655 if (reset
=== true && first
=== true) {
656 option
.setAttribute('selected', 'selected');