TogglLibrary

Library for Toggl-Button scripts used on platforms like drupal, github, youtrack and others.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/2670/247800/TogglLibrary.js

  1. /*------------------------------------------------------------------------
  2. * JavaScript Library for Toggl-Button for GreaseMonkey
  3. *
  4. * (c) Jürgen Haas, PARAGON Executive Services GmbH
  5. * Version: 1.9
  6. *
  7. * @see https://gitlab.paragon-es.de/toggl-button/core
  8. *------------------------------------------------------------------------
  9. */
  10.  
  11. function TogglButtonGM(selector, renderer) {
  12.  
  13. var
  14. $activeApiUrl = null,
  15. $apiUrl = "https://www.toggl.com/api/v7",
  16. $newApiUrl = "https://www.toggl.com/api/v8",
  17. $legacyApiUrl = "https://new.toggl.com/api/v8",
  18. $triedAlternative = false,
  19. $addedDynamicListener = false,
  20. $api_token = null,
  21. $default_wid = null,
  22. $clientMap = {},
  23. $projectMap = {},
  24. $instances = {};
  25.  
  26. init(selector, renderer);
  27.  
  28. function init(selector, renderer, apiUrl) {
  29. var timeNow = new Date().getTime(),
  30. timeAuth = GM_getValue('_authenticated', 0);
  31. apiUrl = apiUrl || $newApiUrl;
  32. $api_token = GM_getValue('_api_token', false);
  33. if ($api_token && (timeNow - timeAuth) < (6*60*60*1000)) {
  34. $activeApiUrl = GM_getValue('_api_url', $newApiUrl);
  35. $default_wid = GM_getValue('_default_wid', 0);
  36. $clientMap = JSON.parse(GM_getValue('_clientMap', {}));
  37. $projectMap = JSON.parse(GM_getValue('_projectMap', {}));
  38. if ($activeApiUrl == $legacyApiUrl) {
  39. // See issue #22.
  40. $activeApiUrl = $newApiUrl;
  41. GM_setValue('_api_url', $activeApiUrl);
  42. }
  43. render(selector, renderer);
  44. return;
  45. }
  46.  
  47. var headers = {};
  48. if ($api_token) {
  49. headers = {
  50. "Authorization": "Basic " + btoa($api_token + ':api_token')
  51. };
  52. }
  53. $activeApiUrl = apiUrl;
  54. GM_xmlhttpRequest({
  55. method: "GET",
  56. url: apiUrl + "/me?with_related_data=true",
  57. headers: headers,
  58. onload: function(result) {
  59. if (result.status === 200) {
  60. var resp = JSON.parse(result.responseText);
  61. $clientMap[0] = 'No Client';
  62. if (resp.data.clients) {
  63. resp.data.clients.forEach(function (client) {
  64. $clientMap[client.id] = client.name;
  65. });
  66. }
  67. if (resp.data.projects) {
  68. resp.data.projects.forEach(function (project) {
  69. if ($clientMap[project.cid] == undefined) {
  70. project.cid = 0;
  71. }
  72. if (project.active) {
  73. $projectMap[project.id] = {
  74. id: project.id,
  75. cid: project.cid,
  76. name: project.name,
  77. billable: project.billable
  78. };
  79. }
  80. });
  81. }
  82. GM_setValue('_authenticated', new Date().getTime());
  83. GM_setValue('_api_token', resp.data.api_token);
  84. GM_setValue('_api_url', $activeApiUrl);
  85. GM_setValue('_default_wid', resp.data.default_wid);
  86. GM_setValue('_clientMap', JSON.stringify($clientMap));
  87. GM_setValue('_projectMap', JSON.stringify($projectMap));
  88. $api_token = resp.data.api_token;
  89. $default_wid = resp.data.default_wid;
  90. render(selector, renderer);
  91. } else if (!$triedAlternative) {
  92. $triedAlternative = true;
  93. if (apiUrl === $apiUrl) {
  94. init(selector, renderer, $newApiUrl);
  95. } else if (apiUrl === $newApiUrl) {
  96. init(selector, renderer, $apiUrl);
  97. }
  98. } else if ($api_token) {
  99. // Delete the API token and try again
  100. GM_setValue('_api_token', false);
  101. $triedAlternative = false;
  102. init(selector, renderer, $newApiUrl);
  103. } else {
  104. var wrapper = document.createElement('div'),
  105. content = createTag('div', 'content'),
  106. link = createLink('login', 'a', 'https://new.toggl.com/', 'Login');
  107. GM_addStyle(GM_getResourceText('togglStyle'));
  108. link.setAttribute('target', '_blank');
  109. wrapper.setAttribute('id', 'toggl-button-auth-failed');
  110. content.appendChild(document.createTextNode('Authorization to your Toggl account failed!'));
  111. content.appendChild(link);
  112. wrapper.appendChild(content);
  113. document.querySelector('body').appendChild(wrapper);
  114. }
  115. }
  116. });
  117. }
  118.  
  119. function render(selector, renderer) {
  120. if (selector == null) {
  121. return;
  122. }
  123. var i, len, elems = document.querySelectorAll(selector + ':not(.toggl)');
  124. for (i = 0, len = elems.length; i < len; i += 1) {
  125. elems[i].classList.add('toggl');
  126. $instances[i] = new TogglButtonGMInstance(renderer(elems[i]));
  127. }
  128.  
  129. if (!$addedDynamicListener) {
  130. $addedDynamicListener = true;
  131.  
  132. document.addEventListener('TogglButtonGMUpdateStatus', function() {
  133. GM_xmlhttpRequest({
  134. method: "GET",
  135. url: $activeApiUrl + "/time_entries/current",
  136. headers: {
  137. "Authorization": "Basic " + btoa($api_token + ':api_token')
  138. },
  139. onload: function (result) {
  140. if (result.status === 200) {
  141. var resp = JSON.parse(result.responseText),
  142. data = resp.data || false;
  143. for (i in $instances) {
  144. $instances[i].checkCurrentLinkStatus(data);
  145. }
  146. }
  147. }
  148. });
  149. });
  150.  
  151. window.addEventListener('focus', function() {
  152. document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus'));
  153. });
  154.  
  155. if (selector !== 'body') {
  156. document.body.addEventListener('DOMSubtreeModified', function () {
  157. setTimeout(function () {
  158. render(selector, renderer);
  159. }, 1000);
  160. });
  161. }
  162. }
  163. }
  164.  
  165. this.clickLinks = function() {
  166. for (i in $instances) {
  167. $instances[i].clickLink();
  168. }
  169. };
  170.  
  171. this.getCurrentTimeEntry = function(callback) {
  172. GM_xmlhttpRequest({
  173. method: "GET",
  174. url: $activeApiUrl + "/time_entries/current",
  175. headers: {
  176. "Authorization": "Basic " + btoa($api_token + ':api_token')
  177. },
  178. onload: function (result) {
  179. if (result.status === 200) {
  180. var resp = JSON.parse(result.responseText),
  181. data = resp.data || false;
  182. if (data) {
  183. callback(data.id, true);
  184. }
  185. }
  186. }
  187. });
  188. };
  189.  
  190. this.stopTimeEntry = function(entryId, asCallback) {
  191. if (entryId == null) {
  192. if (asCallback) {
  193. return;
  194. }
  195. this.getCurrentTimeEntry(this.stopTimeEntry);
  196. return;
  197. }
  198. GM_xmlhttpRequest({
  199. method: "PUT",
  200. url: $activeApiUrl + "/time_entries/" + entryId + "/stop",
  201. headers: {
  202. "Authorization": "Basic " + btoa($api_token + ':api_token')
  203. },
  204. onload: function () {
  205. document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus'));
  206. }
  207. });
  208. };
  209.  
  210. function TogglButtonGMInstance(params) {
  211.  
  212. var
  213. $curEntryId = null,
  214. $isStarted = false,
  215. $link = null,
  216. $generalInfo = null,
  217. $buttonTypeMinimal = false,
  218. $projectSelector = window.location.host,
  219. $projectId = null,
  220. $projectSelected = false,
  221. $projectSelectElem = null,
  222. $stopCallback = null,
  223. $tags = params.tags || [];
  224.  
  225. this.checkCurrentLinkStatus = function (data) {
  226. var started, updateRequired = false;
  227. if (!data) {
  228. if ($isStarted) {
  229. updateRequired = true;
  230. started = false;
  231. }
  232. } else {
  233. if ($generalInfo != null) {
  234. if (!$isStarted || ($curEntryId != null && $curEntryId != data.id)) {
  235. $curEntryId = data.id;
  236. $isStarted = false;
  237. }
  238. }
  239. if ($curEntryId == data.id) {
  240. if (!$isStarted) {
  241. updateRequired = true;
  242. started = true;
  243. }
  244. } else {
  245. if ($isStarted) {
  246. updateRequired = true;
  247. started = false;
  248. }
  249. }
  250. }
  251. if (updateRequired) {
  252. if (!started) {
  253. $curEntryId = null;
  254. }
  255. if ($link != null) {
  256. updateLink(started);
  257. }
  258. if ($generalInfo != null) {
  259. if (data) {
  260. var projectName = 'No project',
  261. clientName = 'No client';
  262. if (data.pid !== undefined) {
  263. if ($projectMap[data.pid] == undefined) {
  264. GM_setValue('_authenticated', 0);
  265. window.location.reload();
  266. return;
  267. }
  268. projectName = $projectMap[data.pid].name;
  269. clientName = $clientMap[$projectMap[data.pid].cid];
  270. }
  271. var content = createTag('div', 'content'),
  272. contentClient = createTag('div', 'client'),
  273. contentProject = createTag('div', 'project'),
  274. contentDescription = createTag('div', 'description');
  275. contentClient.innerHTML = clientName;
  276. contentProject.innerHTML = projectName;
  277. contentDescription.innerHTML = data.description;
  278. content.appendChild(contentClient);
  279. content.appendChild(contentProject);
  280. content.appendChild(contentDescription);
  281. while ($generalInfo.firstChild) {
  282. $generalInfo.removeChild($generalInfo.firstChild);
  283. }
  284. $generalInfo.appendChild(content);
  285. }
  286. updateGeneralInfo(started);
  287. }
  288. }
  289. };
  290.  
  291. this.clickLink = function (data) {
  292. $link.dispatchEvent(new CustomEvent('click'));
  293. };
  294.  
  295. createTimerLink(params);
  296.  
  297. function createTimerLink(params) {
  298. GM_addStyle(GM_getResourceText('togglStyle'));
  299. if (params.generalMode !== undefined && params.generalMode) {
  300. $generalInfo = document.createElement('div');
  301. $generalInfo.id = 'toggl-button-gi-wrapper';
  302. $generalInfo.addEventListener('click', function (e) {
  303. e.preventDefault();
  304. $generalInfo.classList.toggle('collapsed');
  305. });
  306. document.querySelector('body').appendChild($generalInfo);
  307. document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus'));
  308. return;
  309. }
  310. if (params.projectIds !== undefined) {
  311. $projectSelector += '-' + params.projectIds.join('-');
  312. }
  313. if (params.stopCallback !== undefined) {
  314. $stopCallback = params.stopCallback;
  315. }
  316. updateProjectId();
  317. $link = createLink('toggl-button');
  318. $link.classList.add(params.className);
  319.  
  320. if (params.buttonType === 'minimal') {
  321. $link.classList.add('min');
  322. $link.removeChild($link.firstChild);
  323. $buttonTypeMinimal = true;
  324. }
  325.  
  326. $link.addEventListener('click', function (e) {
  327. var opts = '';
  328. e.preventDefault();
  329. if ($isStarted) {
  330. stopTimeEntry();
  331. } else {
  332. var billable = false;
  333. if ($projectId != undefined && $projectId > 0) {
  334. billable = $projectMap[$projectId].billable;
  335. }
  336. opts = {
  337. $projectId: $projectId || null,
  338. billable: billable,
  339. description: invokeIfFunction(params.description),
  340. createdWith: 'TogglButtonGM - ' + params.className
  341. };
  342. createTimeEntry(opts);
  343. }
  344. return false;
  345. });
  346.  
  347. // new button created - reset state
  348. $isStarted = false;
  349.  
  350. // check if our link is the current time entry and set the state if it is
  351. checkCurrentTimeEntry({
  352. $projectId: $projectId,
  353. description: invokeIfFunction(params.description)
  354. });
  355.  
  356. document.querySelector('body').classList.add('toggl-button-available');
  357. if (params.targetSelectors == undefined) {
  358. var wrapper,
  359. existingWrapper = document.querySelectorAll('#toggl-button-wrapper'),
  360. content = createTag('div', 'content');
  361. content.appendChild($link);
  362. content.appendChild(createProjectSelect());
  363. if (existingWrapper.length > 0) {
  364. wrapper = existingWrapper[0];
  365. while (wrapper.firstChild) {
  366. wrapper.removeChild(wrapper.firstChild);
  367. }
  368. wrapper.appendChild(content);
  369. }
  370. else {
  371. wrapper = document.createElement('div');
  372. wrapper.id = 'toggl-button-wrapper';
  373. wrapper.appendChild(content);
  374. document.querySelector('body').appendChild(wrapper);
  375. }
  376. } else {
  377. var elem = params.targetSelectors.context || document;
  378. if (params.targetSelectors.link != undefined) {
  379. elem.querySelector(params.targetSelectors.link).appendChild($link);
  380. }
  381. if (params.targetSelectors.projectSelect != undefined) {
  382. elem.querySelector(params.targetSelectors.projectSelect).appendChild(createProjectSelect());
  383. }
  384. }
  385.  
  386. return $link;
  387. }
  388.  
  389. function createTimeEntry(timeEntry) {
  390. var start = new Date();
  391. GM_xmlhttpRequest({
  392. method: "POST",
  393. url: $activeApiUrl + "/time_entries",
  394. headers: {
  395. "Authorization": "Basic " + btoa($api_token + ':api_token')
  396. },
  397. data: JSON.stringify({
  398. time_entry: {
  399. start: start.toISOString(),
  400. description: timeEntry.description,
  401. wid: $default_wid,
  402. pid: timeEntry.$projectId || null,
  403. billable: timeEntry.billable || false,
  404. duration: -(start.getTime() / 1000),
  405. tags: $tags,
  406. created_with: timeEntry.createdWith || 'TogglButtonGM'
  407. }
  408. }),
  409. onload: function (res) {
  410. var responseData, entryId;
  411. responseData = JSON.parse(res.responseText);
  412. entryId = responseData && responseData.data && responseData.data.id;
  413. $curEntryId = entryId;
  414. document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus'));
  415. }
  416. });
  417. }
  418.  
  419. function checkCurrentTimeEntry(params) {
  420. GM_xmlhttpRequest({
  421. method: "GET",
  422. url: $activeApiUrl + "/time_entries/current",
  423. headers: {
  424. "Authorization": "Basic " + btoa($api_token + ':api_token')
  425. },
  426. onload: function (result) {
  427. if (result.status === 200) {
  428. var resp = JSON.parse(result.responseText);
  429. if (resp == null) {
  430. return;
  431. }
  432. if (params.description === resp.data.description) {
  433. $curEntryId = resp.data.id;
  434. updateLink(true);
  435. }
  436. }
  437. }
  438. });
  439. }
  440.  
  441. function stopTimeEntry(entryId) {
  442. entryId = entryId || $curEntryId;
  443. if (!entryId) {
  444. return;
  445. }
  446. GM_xmlhttpRequest({
  447. method: "PUT",
  448. url: $activeApiUrl + "/time_entries/" + entryId + "/stop",
  449. headers: {
  450. "Authorization": "Basic " + btoa($api_token + ':api_token')
  451. },
  452. onload: function (result) {
  453. $curEntryId = null;
  454. document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus'));
  455. if (result.status === 200) {
  456. var resp = JSON.parse(result.responseText),
  457. data = resp.data || false;
  458. if (data) {
  459. if ($stopCallback !== undefined && $stopCallback !== null) {
  460. var currentdate = new Date();
  461. $stopCallback((currentdate.getTime() - (data.duration * 1000)), data.duration);
  462. }
  463. }
  464. }
  465. }
  466. });
  467. }
  468.  
  469. function createTag(name, className, innerHTML) {
  470. var tag = document.createElement(name);
  471. tag.className = className;
  472.  
  473. if (innerHTML) {
  474. tag.innerHTML = innerHTML;
  475. }
  476.  
  477. return tag;
  478. }
  479.  
  480. function createLink(className, tagName, linkHref, linkText) {
  481. // Param defaults
  482. tagName = tagName || 'a';
  483. linkHref = linkHref || '#';
  484. linkText = linkText || 'Start timer';
  485.  
  486. var link = createTag(tagName, className);
  487.  
  488. if (tagName === 'a') {
  489. link.setAttribute('href', linkHref);
  490. }
  491.  
  492. link.appendChild(document.createTextNode(linkText));
  493. return link;
  494. }
  495.  
  496. function updateGeneralInfo(started) {
  497. if (started) {
  498. $generalInfo.classList.add('active');
  499. } else {
  500. $generalInfo.classList.remove('active');
  501. }
  502. $isStarted = started;
  503. }
  504.  
  505. function updateLink(started) {
  506. var linkText, color = '';
  507.  
  508. if (started) {
  509. document.querySelector('body').classList.add('toggl-button-active');
  510. $link.classList.add('active');
  511. color = '#1ab351';
  512. linkText = 'Stop timer';
  513. } else {
  514. document.querySelector('body').classList.remove('toggl-button-active');
  515. $link.classList.remove('active');
  516. linkText = 'Start timer';
  517. }
  518. $isStarted = started;
  519.  
  520. $link.setAttribute('style', 'color:'+color+';');
  521. if (!$buttonTypeMinimal) {
  522. $link.innerHTML = linkText;
  523. }
  524.  
  525. $projectSelectElem.disabled = $isStarted;
  526. }
  527.  
  528. function updateProjectId(id) {
  529. id = id || GM_getValue($projectSelector, 0);
  530.  
  531. $projectSelected = (id != 0);
  532.  
  533. if (id <= 0) {
  534. $projectId = null;
  535. }
  536. else {
  537. $projectId = id;
  538. }
  539.  
  540. if ($projectSelectElem != undefined) {
  541. $projectSelectElem.value = id;
  542. $projectSelectElem.disabled = $isStarted;
  543. }
  544.  
  545. GM_setValue($projectSelector, id);
  546.  
  547. if ($link != undefined) {
  548. if ($projectSelected) {
  549. $link.classList.remove('hidden');
  550. }
  551. else {
  552. $link.classList.add('hidden');
  553. }
  554. }
  555. }
  556.  
  557. function invokeIfFunction(trial) {
  558. if (trial instanceof Function) {
  559. return trial();
  560. }
  561. return trial;
  562. }
  563.  
  564. function createProjectSelect() {
  565. var pid,
  566. wrapper = createTag('div', 'toggl-button-project-select'),
  567. noneOptionAdded = false,
  568. noneOption = document.createElement('option'),
  569. emptyOption = document.createElement('option'),
  570. resetOption = document.createElement('option');
  571.  
  572. $projectSelectElem = createTag('select');
  573.  
  574. // None Option to indicate that a project should be selected first
  575. if (!$projectSelected) {
  576. noneOption.setAttribute('value', '0');
  577. noneOption.text = '- First select a project -';
  578. $projectSelectElem.appendChild(noneOption);
  579. noneOptionAdded = true;
  580. }
  581.  
  582. // Empty Option for tasks with no project
  583. emptyOption.setAttribute('value', '-1');
  584. emptyOption.text = 'No Project';
  585. $projectSelectElem.appendChild(emptyOption);
  586.  
  587. var optgroup, project, clientMap = [];
  588. for (pid in $projectMap) {
  589. //noinspection JSUnfilteredForInLoop
  590. project = $projectMap[pid];
  591. if (clientMap[project.cid] == undefined) {
  592. optgroup = createTag('optgroup');
  593. optgroup.label = $clientMap[project.cid];
  594. clientMap[project.cid] = optgroup;
  595. $projectSelectElem.appendChild(optgroup);
  596. } else {
  597. optgroup = clientMap[project.cid];
  598. }
  599. var option = document.createElement('option');
  600. option.setAttribute('value', project.id);
  601. option.text = project.name;
  602. optgroup.appendChild(option);
  603. }
  604.  
  605. // Reset Option to reload settings and projects from Toggl
  606. resetOption.setAttribute('value', 'RESET');
  607. resetOption.text = 'Reload settings';
  608. $projectSelectElem.appendChild(resetOption);
  609.  
  610. $projectSelectElem.addEventListener('change', function () {
  611. if ($projectSelectElem.value == 'RESET') {
  612. GM_setValue('_authenticated', 0);
  613. window.location.reload();
  614. return;
  615. }
  616.  
  617. if (noneOptionAdded) {
  618. $projectSelectElem.removeChild(noneOption);
  619. noneOptionAdded = false;
  620. }
  621.  
  622. updateProjectId($projectSelectElem.value);
  623.  
  624. });
  625.  
  626. updateProjectId($projectId);
  627.  
  628. wrapper.appendChild($projectSelectElem);
  629. return wrapper;
  630. }
  631. }
  632.  
  633. }