]> git.immae.eu Git - github/shaarli/Shaarli.git/blame - tpl/default/js/shaarli.js
Fix jumpy textarea with long content in post edit
[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 () {
9c46b347
A
278 /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
279 var scrollTop = window.pageYOffset ||
280 (document.documentElement || document.body.parentNode || document.body).scrollTop;
281
70401690
A
282 description.style.height = 'auto';
283 description.style.height = description.scrollHeight+10+'px';
9c46b347
A
284
285 window.scrollTo(0, scrollTop);
70401690
A
286 }
287 /* 0-timeout to get the already changed text */
288 function delayedResize () {
289 window.setTimeout(resize, 0);
290 }
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);
296
297 resize();
298 }
2dd698fd 299
70401690
A
300 if (description != null) {
301 init();
2dd698fd
A
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();
306 }
307 });
70401690 308 }
b9b41d25 309
b9b41d25
A
310 /**
311 * Awesomplete trigger.
312 */
313 var tags = document.getElementById('lf_tags');
314 if (tags != null) {
315 awesompleteUniqueTag('#lf_tags');
316 }
317
318 /**
319 * bLazy trigger
320 */
321 var picwall = document.getElementById('picwall_container');
322 if (picwall != null) {
323 var bLazy = new Blazy();
324 }
325
326 /**
327 * Bookmarklet alert
328 */
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);
335 });
336 });
337
338 /**
339 * Firefox Social
340 */
341 var ffButton = document.getElementById('ff-social-button');
342 if (ffButton != null) {
343 ffButton.addEventListener('click', function(event) {
344 activateFirefoxSocial(event.target);
345 });
346 }
347
348 /**
349 * Plugin admin order
350 */
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'));
359 }
360 });
361 });
a0737313
A
362
363 var continent = document.getElementById('continent');
364 var city = document.getElementById('city');
365 if (continent != null && city != null) {
29a837f3 366 continent.addEventListener('change', function (event) {
a0737313
A
367 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
368 });
369 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
370 }
29a837f3
A
371
372 /**
373 * Bulk actions
29a837f3 374 */
63836498
A
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) {
380 var count = 0;
381 var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
382 [].forEach.call(linkCheckedCheckboxes, function(checkbox) {
383 count++;
29a837f3 384 });
63836498
A
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');
389 }
390 });
391 });
29a837f3 392
63836498
A
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();
29a837f3 398
63836498
A
399 var links = [];
400 var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
401 [].forEach.call(linkCheckedCheckboxes, function(checkbox) {
402 links.push({
403 'id': checkbox.value,
404 'title': document.querySelector('.linklist-item[data-id="'+ checkbox.value +'"] .linklist-link').innerHTML
405 });
406 });
29a837f3 407
63836498
A
408 var message = 'Are you sure you want to delete '+ links.length +' links?\n';
409 message += 'This action is IRREVERSIBLE!\n\nTitles:\n';
a74f52a8 410 var ids = [];
63836498
A
411 links.forEach(function(item) {
412 message += ' - '+ item['title'] +'\n';
a74f52a8 413 ids.push(item['id']);
29a837f3 414 });
63836498
A
415
416 if (window.confirm(message)) {
a74f52a8 417 window.location = '?delete_link&lf_linkdate='+ ids.join('+') +'&token='+ token.value;
63836498
A
418 }
419 });
29a837f3 420 }
aa4797ba
A
421
422 /**
423 * Tag list operations
424 *
425 * TODO: support error code in the backend for AJAX requests
426 */
9bf82f4f
LC
427 var tagList = document.querySelector('input[name="taglist"]');
428 var existingTags = tagList ? tagList.value.split(' ') : [];
82e3bb5f
A
429 var awesomepletes = [];
430
aa4797ba
A
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');
82e3bb5f
A
438 if (form.style.display == 'none' || form.style.display == '') {
439 form.style.display = 'block';
440 } else {
441 form.style.display = 'none';
442 }
443 block.querySelector('input').focus();
aa4797ba
A
444 });
445 });
446
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() == '') {
456 return;
457 }
458 var fromtag = block.getAttribute('data-tag');
459 var token = document.getElementById('token').value;
460
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);
467 location.reload();
468 } else {
469 block.setAttribute('data-tag', totag);
470 input.setAttribute('name', totag);
471 input.setAttribute('value', totag);
82e3bb5f 472 findParent(input, 'div', {'class': 'rename-tag-form'}).style.display = 'none';
aa4797ba
A
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));
82e3bb5f
A
476
477 // Refresh awesomplete values
478 for (var key in existingTags) {
479 if (existingTags[key] == fromtag) {
480 existingTags[key] = totag;
481 }
482 }
483 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
aa4797ba
A
484 }
485 };
486 xhr.send('renametag=1&fromtag='+ encodeURIComponent(fromtag) +'&totag='+ encodeURIComponent(totag) +'&token='+ token);
487 refreshToken();
488 });
489 });
490
491 // Validate input with enter key
492 var renameTagInputs = document.querySelectorAll('.rename-tag-input');
493 [].forEach.call(renameTagInputs, function(rename) {
82e3bb5f 494
aa4797ba
A
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();
498 }
499 });
500 });
501
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;
511
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() {
517 block.remove();
518 };
519 xhr.send(encodeURI('deletetag=1&fromtag='+ tag +'&token='+ token));
520 refreshToken();
521 }
522 });
523 });
82e3bb5f 524
9bf82f4f 525 updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
402b0346 526};
b9b41d25 527
82e3bb5f
A
528/**
529 * Find a parent element according to its tag and its attributes
530 *
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).
534 *
535 * @returns Found element or null.
536 */
aa4797ba
A
537function findParent(element, tagName, attributes)
538{
539 while (element) {
540 if (element.tagName.toLowerCase() == tagName) {
541 var match = true;
542 for (var key in attributes) {
543 if (! element.hasAttribute(key)
544 || (attributes[key] != '' && element.getAttribute(key).indexOf(attributes[key]) == -1)
545 ) {
546 match = false;
547 break;
548 }
549 }
550
551 if (match) {
552 return element;
553 }
554 }
555 element = element.parentElement;
556 }
557 return null;
558}
559
82e3bb5f
A
560/**
561 * Ajax request to refresh the CSRF token.
562 */
aa4797ba
A
563function refreshToken()
564{
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);
570 };
571 xhr.send();
572}
573
82e3bb5f
A
574/**
575 * Update awesomplete list of tag for all elements matching the given selector
576 *
577 * @param selector CSS selector
578 * @param tags Array of tags
579 * @param instances List of existing awesomplete instances
580 */
581function updateAwesompleteList(selector, tags, instances)
582{
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(
588 element,
589 {'list': tags}
590 ));
591 });
592 } else {
593 // Update awesomplete tag list
594 for (var key in instances) {
595 instances[key].list = tags;
596 }
597 }
598 return instances;
599}
600
aa4797ba
A
601/**
602 * html_entities in JS
603 *
604 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
605 */
606function htmlEntities(str)
607{
608 return str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
609 return '&#'+i.charCodeAt(0)+';';
610 });
611}
612
b9b41d25
A
613function activateFirefoxSocial(node) {
614 var loc = location.href;
8eb6bac1 615 var baseURL = loc.substring(0, loc.lastIndexOf("/") + 1);
206c45bd 616 var title = document.title;
b9b41d25
A
617
618 // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
619 var data = {
206c45bd 620 name: title,
b9b41d25
A
621 description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.",
622 author: "Shaarli",
623 version: "1.0.0",
624
625 iconURL: baseURL + "/images/favicon.ico",
626 icon32URL: baseURL + "/images/favicon.ico",
627 icon64URL: baseURL + "/images/favicon.ico",
628
8eb6bac1 629 shareURL: baseURL + "?post=%{url}&title=%{title}&description=%{text}&source=firefoxsocialapi",
b9b41d25
A
630 homepageURL: baseURL
631 };
632 node.setAttribute("data-service", JSON.stringify(data));
633
634 var activate = new CustomEvent("ActivateSocialFeature");
635 node.dispatchEvent(activate);
636}
a0737313
A
637
638/**
639 * Add the class 'hidden' to city options not attached to the current selected continent.
640 *
641 * @param cities List of <option> elements
642 * @param currentContinent Current selected continent
643 * @param reset Set to true to reset the selected value
644 */
aa4797ba 645function hideTimezoneCities(cities, currentContinent) {
a0737313 646 var first = true;
aa4797ba
A
647 if (reset == null) {
648 reset = false;
649 }
29a837f3 650 [].forEach.call(cities, function (option) {
a0737313
A
651 if (option.getAttribute('data-continent') != currentContinent) {
652 option.className = 'hidden';
653 } else {
654 option.className = '';
655 if (reset === true && first === true) {
656 option.setAttribute('selected', 'selected');
657 first = false;
658 }
659 }
660 });
661}