shaarli.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  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. window.onload = function () {
  29. /**
  30. * Retrieve an element up in the tree from its class name.
  31. */
  32. function getParentByClass(el, className) {
  33. var p = el.parentNode;
  34. if (p == null || p.classList.contains(className)) {
  35. return p;
  36. }
  37. return getParentByClass(p, className);
  38. }
  39. /**
  40. * Handle responsive menu.
  41. * Source: http://purecss.io/layouts/tucked-menu-vertical/
  42. */
  43. (function (window, document) {
  44. var menu = document.getElementById('shaarli-menu'),
  45. WINDOW_CHANGE_EVENT = ('onorientationchange' in window) ? 'orientationchange':'resize';
  46. function toggleHorizontal() {
  47. [].forEach.call(
  48. document.getElementById('shaarli-menu').querySelectorAll('.menu-transform'),
  49. function(el){
  50. el.classList.toggle('pure-menu-horizontal');
  51. }
  52. );
  53. };
  54. function toggleMenu() {
  55. // set timeout so that the panel has a chance to roll up
  56. // before the menu switches states
  57. if (menu.classList.contains('open')) {
  58. setTimeout(toggleHorizontal, 500);
  59. }
  60. else {
  61. toggleHorizontal();
  62. }
  63. menu.classList.toggle('open');
  64. document.getElementById('menu-toggle').classList.toggle('x');
  65. };
  66. function closeMenu() {
  67. if (menu.classList.contains('open')) {
  68. toggleMenu();
  69. }
  70. }
  71. var menuToggle = document.getElementById('menu-toggle');
  72. if (menuToggle != null) {
  73. menuToggle.addEventListener('click', function (e) {
  74. toggleMenu();
  75. });
  76. }
  77. window.addEventListener(WINDOW_CHANGE_EVENT, closeMenu);
  78. })(this, this.document);
  79. /**
  80. * Fold/Expand shaares description and thumbnail.
  81. */
  82. var foldAllButtons = document.getElementsByClassName('fold-all');
  83. var foldButtons = document.getElementsByClassName('fold-button');
  84. [].forEach.call(foldButtons, function (foldButton) {
  85. // Retrieve description
  86. var description = null;
  87. var thumbnail = null;
  88. var linklistItem = getParentByClass(foldButton, 'linklist-item');
  89. if (linklistItem != null) {
  90. description = linklistItem.querySelector('.linklist-item-description');
  91. thumbnail = linklistItem.querySelector('.linklist-item-thumbnail');
  92. if (description != null || thumbnail != null) {
  93. foldButton.style.display = 'inline';
  94. }
  95. }
  96. foldButton.addEventListener('click', function (event) {
  97. event.preventDefault();
  98. toggleFold(event.target, description, thumbnail);
  99. });
  100. });
  101. if (foldAllButtons != null) {
  102. [].forEach.call(foldAllButtons, function (foldAllButton) {
  103. foldAllButton.addEventListener('click', function (event) {
  104. event.preventDefault();
  105. var state = foldAllButton.firstElementChild.getAttribute('class').indexOf('down') != -1 ? 'down' : 'up';
  106. [].forEach.call(foldButtons, function (foldButton) {
  107. if (foldButton.firstElementChild.classList.contains('fa-chevron-up') && state == 'down'
  108. || foldButton.firstElementChild.classList.contains('fa-chevron-down') && state == 'up'
  109. ) {
  110. return;
  111. }
  112. // Retrieve description
  113. var description = null;
  114. var thumbnail = null;
  115. var linklistItem = getParentByClass(foldButton, 'linklist-item');
  116. if (linklistItem != null) {
  117. description = linklistItem.querySelector('.linklist-item-description');
  118. thumbnail = linklistItem.querySelector('.linklist-item-thumbnail');
  119. if (description != null || thumbnail != null) {
  120. foldButton.style.display = 'inline';
  121. }
  122. }
  123. toggleFold(foldButton.firstElementChild, description, thumbnail);
  124. });
  125. foldAllButton.firstElementChild.classList.toggle('fa-chevron-down');
  126. foldAllButton.firstElementChild.classList.toggle('fa-chevron-up');
  127. });
  128. });
  129. }
  130. function toggleFold(button, description, thumb)
  131. {
  132. // Switch fold/expand - up = fold
  133. if (button.classList.contains('fa-chevron-up')) {
  134. button.title = 'Expand';
  135. if (description != null) {
  136. description.style.display = 'none';
  137. }
  138. if (thumb != null) {
  139. thumb.style.display = 'none';
  140. }
  141. }
  142. else {
  143. button.title = 'Fold';
  144. if (description != null) {
  145. description.style.display = 'block';
  146. }
  147. if (thumb != null) {
  148. thumb.style.display = 'block';
  149. }
  150. }
  151. button.classList.toggle('fa-chevron-down');
  152. button.classList.toggle('fa-chevron-up');
  153. }
  154. /**
  155. * Confirmation message before deletion.
  156. */
  157. var deleteLinks = document.querySelectorAll('.confirm-delete');
  158. [].forEach.call(deleteLinks, function(deleteLink) {
  159. deleteLink.addEventListener('click', function(event) {
  160. if(! confirm('Are you sure you want to delete this link ?')) {
  161. event.preventDefault();
  162. }
  163. });
  164. });
  165. /**
  166. * Close alerts
  167. */
  168. var closeLinks = document.querySelectorAll('.pure-alert-close');
  169. [].forEach.call(closeLinks, function(closeLink) {
  170. closeLink.addEventListener('click', function(event) {
  171. var alert = getParentByClass(event.target, 'pure-alert-closable');
  172. alert.style.display = 'none';
  173. });
  174. });
  175. /**
  176. * New version dismiss.
  177. * Hide the message for one week using localStorage.
  178. */
  179. var newVersionDismiss = document.getElementById('new-version-dismiss');
  180. var newVersionMessage = document.querySelector('.new-version-message');
  181. if (newVersionMessage != null
  182. && localStorage.getItem('newVersionDismiss') != null
  183. && parseInt(localStorage.getItem('newVersionDismiss')) + 7*24*60*60*1000 > (new Date()).getTime()
  184. ) {
  185. newVersionMessage.style.display = 'none';
  186. }
  187. if (newVersionDismiss != null) {
  188. newVersionDismiss.addEventListener('click', function () {
  189. localStorage.setItem('newVersionDismiss', (new Date()).getTime());
  190. });
  191. }
  192. var hiddenReturnurl = document.getElementsByName('returnurl');
  193. if (hiddenReturnurl != null) {
  194. hiddenReturnurl.value = window.location.href;
  195. }
  196. /**
  197. * Autofocus text fields
  198. */
  199. // ES6 syntax
  200. let autofocusElements = document.querySelectorAll('.autofocus');
  201. for (let autofocusElement of autofocusElements) {
  202. if (autofocusElement.value == '') {
  203. autofocusElement.focus();
  204. break;
  205. }
  206. }
  207. /**
  208. * Handle sub menus/forms
  209. */
  210. var openers = document.getElementsByClassName('subheader-opener');
  211. if (openers != null) {
  212. [].forEach.call(openers, function(opener) {
  213. opener.addEventListener('click', function(event) {
  214. event.preventDefault();
  215. var id = opener.getAttribute('data-open-id');
  216. var sub = document.getElementById(id);
  217. if (sub != null) {
  218. [].forEach.call(document.getElementsByClassName('subheader-form'), function (element) {
  219. if (element != sub) {
  220. removeClass(element, 'open')
  221. }
  222. });
  223. sub.classList.toggle('open');
  224. }
  225. });
  226. });
  227. }
  228. function removeClass(element, classname) {
  229. element.className = element.className.replace(new RegExp('(?:^|\\s)'+ classname + '(?:\\s|$)'), ' ');
  230. }
  231. /**
  232. * Remove CSS target padding (for fixed bar)
  233. */
  234. if (location.hash != '') {
  235. var anchor = document.getElementById(location.hash.substr(1));
  236. if (anchor != null) {
  237. var padsize = anchor.clientHeight;
  238. this.window.scroll(0, this.window.scrollY - padsize);
  239. anchor.style.paddingTop = 0;
  240. }
  241. }
  242. /**
  243. * Text area resizer
  244. */
  245. var description = document.getElementById('lf_description');
  246. var observe = function (element, event, handler) {
  247. element.addEventListener(event, handler, false);
  248. };
  249. function init () {
  250. function resize () {
  251. description.style.height = 'auto';
  252. description.style.height = description.scrollHeight+10+'px';
  253. }
  254. /* 0-timeout to get the already changed text */
  255. function delayedResize () {
  256. window.setTimeout(resize, 0);
  257. }
  258. observe(description, 'change', resize);
  259. observe(description, 'cut', delayedResize);
  260. observe(description, 'paste', delayedResize);
  261. observe(description, 'drop', delayedResize);
  262. observe(description, 'keydown', delayedResize);
  263. resize();
  264. }
  265. if (description != null) {
  266. init();
  267. // Submit editlink form with CTRL + Enter in the text area.
  268. description.addEventListener('keydown', function (event) {
  269. if (event.ctrlKey && event.keyCode === 13) {
  270. document.getElementById('button-save-edit').click();
  271. }
  272. });
  273. }
  274. /**
  275. * Awesomplete trigger.
  276. */
  277. var tags = document.getElementById('lf_tags');
  278. if (tags != null) {
  279. awesompleteUniqueTag('#lf_tags');
  280. }
  281. /**
  282. * bLazy trigger
  283. */
  284. var picwall = document.getElementById('picwall_container');
  285. if (picwall != null) {
  286. var bLazy = new Blazy();
  287. }
  288. /**
  289. * Bookmarklet alert
  290. */
  291. var bookmarkletLinks = document.querySelectorAll('.bookmarklet-link');
  292. var bkmMessage = document.getElementById('bookmarklet-alert');
  293. [].forEach.call(bookmarkletLinks, function(link) {
  294. link.addEventListener('click', function(event) {
  295. event.preventDefault();
  296. alert(bkmMessage.value);
  297. });
  298. });
  299. /**
  300. * Firefox Social
  301. */
  302. var ffButton = document.getElementById('ff-social-button');
  303. if (ffButton != null) {
  304. ffButton.addEventListener('click', function(event) {
  305. activateFirefoxSocial(event.target);
  306. });
  307. }
  308. /**
  309. * Plugin admin order
  310. */
  311. var orderPA = document.querySelectorAll('.order');
  312. [].forEach.call(orderPA, function(link) {
  313. link.addEventListener('click', function(event) {
  314. event.preventDefault();
  315. if (event.target.classList.contains('order-up')) {
  316. return orderUp(event.target.parentNode.parentNode.getAttribute('data-order'));
  317. } else if (event.target.classList.contains('order-down')) {
  318. return orderDown(event.target.parentNode.parentNode.getAttribute('data-order'));
  319. }
  320. });
  321. });
  322. var continent = document.getElementById('continent');
  323. var city = document.getElementById('city');
  324. if (continent != null && city != null) {
  325. continent.addEventListener('change', function(event) {
  326. hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
  327. });
  328. hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
  329. }
  330. };
  331. function activateFirefoxSocial(node) {
  332. var loc = location.href;
  333. var baseURL = loc.substring(0, loc.lastIndexOf("/"));
  334. // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
  335. var data = {
  336. name: "{$shaarlititle}",
  337. description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.",
  338. author: "Shaarli",
  339. version: "1.0.0",
  340. iconURL: baseURL + "/images/favicon.ico",
  341. icon32URL: baseURL + "/images/favicon.ico",
  342. icon64URL: baseURL + "/images/favicon.ico",
  343. shareURL: baseURL + "{noparse}?post=%{url}&title=%{title}&description=%{text}&source=firefoxsocialapi{/noparse}",
  344. homepageURL: baseURL
  345. };
  346. node.setAttribute("data-service", JSON.stringify(data));
  347. var activate = new CustomEvent("ActivateSocialFeature");
  348. node.dispatchEvent(activate);
  349. }
  350. /**
  351. * Add the class 'hidden' to city options not attached to the current selected continent.
  352. *
  353. * @param cities List of <option> elements
  354. * @param currentContinent Current selected continent
  355. * @param reset Set to true to reset the selected value
  356. */
  357. function hideTimezoneCities(cities, currentContinent, reset = false) {
  358. var first = true;
  359. [].forEach.call(cities, function(option) {
  360. if (option.getAttribute('data-continent') != currentContinent) {
  361. option.className = 'hidden';
  362. } else {
  363. option.className = '';
  364. if (reset === true && first === true) {
  365. option.setAttribute('selected', 'selected');
  366. first = false;
  367. }
  368. }
  369. });
  370. }