]>
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 description
.style
.height
= 'auto';
279 description
.style
.height
= description
.scrollHeight
+10+'px';
281 /* 0-timeout to get the already changed text */
282 function delayedResize () {
283 window
.setTimeout(resize
, 0);
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
);
294 if (description
!= null) {
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();
305 * Awesomplete trigger.
307 var tags
= document
.getElementById('lf_tags');
309 awesompleteUniqueTag('#lf_tags');
315 var picwall
= document
.getElementById('picwall_container');
316 if (picwall
!= null) {
317 var bLazy
= new Blazy();
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
);
335 var ffButton
= document
.getElementById('ff-social-button');
336 if (ffButton
!= null) {
337 ffButton
.addEventListener('click', function(event
) {
338 activateFirefoxSocial(event
.target
);
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'));
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);
363 hideTimezoneCities(city
, continent
.options
[continent
.selectedIndex
].value
, false);
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
) {
375 var linkCheckedCheckboxes
= document
.querySelectorAll('.delete-checkbox:checked');
376 [].forEach
.call(linkCheckedCheckboxes
, function(checkbox
) {
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');
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();
394 var linkCheckedCheckboxes
= document
.querySelectorAll('.delete-checkbox:checked');
395 [].forEach
.call(linkCheckedCheckboxes
, function(checkbox
) {
397 'id': checkbox
.value
,
398 'title': document
.querySelector('.linklist-item[data-id="'+ checkbox
.value
+'"] .linklist-link').innerHTML
402 var message
= 'Are you sure you want to delete '+ links
.length
+' links?\n';
403 message
+= 'This action is IRREVERSIBLE!\n\nTitles:\n';
405 links
.forEach(function(item
) {
406 message
+= ' - '+ item
['title'] +'\n';
407 ids
.push(item
['id']);
410 if (window
.confirm(message
)) {
411 window
.location
= '?delete_link&lf_linkdate='+ ids
.join('+') +'&token='+ token
.value
;
417 * Tag list operations
419 * TODO: support error code in the backend for AJAX requests
421 var tagList
= document
.querySelector('input[name="taglist"]');
422 var existingTags
= tagList
? tagList
.value
.split(' ') : [];
423 var awesomepletes
= [];
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');
432 if (form
.style
.display
== 'none' || form
.style
.display
== '') {
433 form
.style
.display
= 'block';
435 form
.style
.display
= 'none';
437 block
.querySelector('input').focus();
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() == '') {
452 var fromtag
= block
.getAttribute('data-tag');
453 var token
= document
.getElementById('token').value
;
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
);
463 block
.setAttribute('data-tag', totag
);
464 input
.setAttribute('name', totag
);
465 input
.setAttribute('value', totag
);
466 findParent(input
, 'div', {'class': 'rename-tag-form'}).style
.display
= 'none';
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
));
471 // Refresh awesomplete values
472 for (var key
in existingTags
) {
473 if (existingTags
[key
] == fromtag
) {
474 existingTags
[key
] = totag
;
477 awesomepletes
= updateAwesompleteList('.rename-tag-input', existingTags
, awesomepletes
);
480 xhr
.send('renametag=1&fromtag='+ encodeURIComponent(fromtag
) +'&totag='+ encodeURIComponent(totag
) +'&token='+ token
);
485 // Validate input with enter key
486 var renameTagInputs
= document
.querySelectorAll('.rename-tag-input');
487 [].forEach
.call(renameTagInputs
, function(rename
) {
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();
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
;
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() {
513 xhr
.send(encodeURI('deletetag=1&fromtag='+ tag
+'&token='+ token
));
519 updateAwesompleteList('.rename-tag-input', existingTags
, awesomepletes
);
523 * Find a parent element according to its tag and its attributes
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).
529 * @returns Found element or null.
531 function findParent(element
, tagName
, attributes
)
534 if (element
.tagName
.toLowerCase() == tagName
) {
536 for (var key
in attributes
) {
537 if (! element
.hasAttribute(key
)
538 || (attributes
[key
] != '' && element
.getAttribute(key
).indexOf(attributes
[key
]) == -1)
549 element
= element
.parentElement
;
555 * Ajax request to refresh the CSRF token.
557 function refreshToken()
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
);
569 * Update awesomplete list of tag for all elements matching the given selector
571 * @param selector CSS selector
572 * @param tags Array of tags
573 * @param instances List of existing awesomplete instances
575 function updateAwesompleteList(selector
, tags
, instances
)
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(
587 // Update awesomplete tag list
588 for (var key
in instances
) {
589 instances
[key
].list
= tags
;
596 * html_entities in JS
598 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
600 function htmlEntities(str
)
602 return str
.replace(/[\u00A0-\u9999<>\&]/gim, function(i
) {
603 return '&#'+i
.charCodeAt(0)+';';
607 function activateFirefoxSocial(node
) {
608 var loc
= location
.href
;
609 var baseURL
= loc
.substring(0, loc
.lastIndexOf("/") + 1);
610 var title
= document
.title
;
612 // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
615 description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.",
619 iconURL: baseURL
+ "/images/favicon.ico",
620 icon32URL: baseURL
+ "/images/favicon.ico",
621 icon64URL: baseURL
+ "/images/favicon.ico",
623 shareURL: baseURL
+ "?post=%{url}&title=%{title}&description=%{text}&source=firefoxsocialapi",
626 node
.setAttribute("data-service", JSON
.stringify(data
));
628 var activate
= new CustomEvent("ActivateSocialFeature");
629 node
.dispatchEvent(activate
);
633 * Add the class 'hidden' to city options not attached to the current selected continent.
635 * @param cities List of <option> elements
636 * @param currentContinent Current selected continent
637 * @param reset Set to true to reset the selected value
639 function hideTimezoneCities(cities
, currentContinent
) {
644 [].forEach
.call(cities
, function (option
) {
645 if (option
.getAttribute('data-continent') != currentContinent
) {
646 option
.className
= 'hidden';
648 option
.className
= '';
649 if (reset
=== true && first
=== true) {
650 option
.setAttribute('selected', 'selected');