TogglLibrary

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

目前为 2016-06-22 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/2670/133048/TogglLibrary.js

  1. /*------------------------------------------------------------------------
  2. * JavaScript Library for Toggl-Button for GreaseMonkey
  3. *
  4. * (c) Jürgen Haas, PARAGON Executive Services GmbH
  5. * Version: 1.7
  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. if (data) {
  144. for (i in $instances) {
  145. $instances[i].checkCurrentLinkStatus(data);
  146. }
  147. }
  148. }
  149. }
  150. });
  151. });
  152.  
  153. window.addEventListener('focus', function() {
  154. document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus'));
  155. });
  156.  
  157. if (selector !== 'body') {
  158. document.body.addEventListener('DOMSubtreeModified', function () {
  159. setTimeout(function () {
  160. render(selector, renderer);
  161. }, 1000);
  162. });
  163. }
  164. }
  165. }
  166.  
  167. this.clickLinks = function() {
  168. for (i in $instances) {
  169. $instances[i].clickLink();
  170. }
  171. };
  172.  
  173. this.getCurrentTimeEntry = function(callback) {
  174. GM_xmlhttpRequest({
  175. method: "GET",
  176. url: $activeApiUrl + "/time_entries/current",
  177. headers: {
  178. "Authorization": "Basic " + btoa($api_token + ':api_token')
  179. },
  180. onload: function (result) {
  181. if (result.status === 200) {
  182. var resp = JSON.parse(result.responseText),
  183. data = resp.data || false;
  184. if (data) {
  185. callback(data.id, true);
  186. }
  187. }
  188. }
  189. });
  190. };
  191.  
  192. this.stopTimeEntry = function(entryId, asCallback) {
  193. if (entryId == null) {
  194. if (asCallback) {
  195. return;
  196. }
  197. this.getCurrentTimeEntry(this.stopTimeEntry);
  198. return;
  199. }
  200. GM_xmlhttpRequest({
  201. method: "PUT",
  202. url: $activeApiUrl + "/time_entries/" + entryId + "/stop",
  203. headers: {
  204. "Authorization": "Basic " + btoa($api_token + ':api_token')
  205. },
  206. onload: function () {
  207. document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus'));
  208. }
  209. });
  210. };
  211.  
  212. function TogglButtonGMInstance(params) {
  213.  
  214. var
  215. $curEntryId = null,
  216. $isStarted = false,
  217. $link = null,
  218. $generalInfo = null,
  219. $buttonTypeMinimal = false,
  220. $projectSelector = window.location.host,
  221. $projectId = null,
  222. $projectSelected = false,
  223. $projectSelectElem = null,
  224. $stopCallback = null;
  225.  
  226. this.checkCurrentLinkStatus = function (data) {
  227. var started, updateRequired = false;
  228. if (!data) {
  229. if ($isStarted) {
  230. updateRequired = true;
  231. started = false;
  232. }
  233. } else {
  234. if ($generalInfo != null) {
  235. if (!$isStarted || ($curEntryId != null && $curEntryId != data.id)) {
  236. $curEntryId = data.id;
  237. $isStarted = false;
  238. }
  239. }
  240. if ($curEntryId == data.id) {
  241. if (!$isStarted) {
  242. updateRequired = true;
  243. started = true;
  244. }
  245. } else {
  246. if ($isStarted) {
  247. updateRequired = true;
  248. started = false;
  249. }
  250. }
  251. }
  252. if (updateRequired) {
  253. if (!started) {
  254. $curEntryId = null;
  255. }
  256. if ($link != null) {
  257. updateLink(started);
  258. }
  259. if ($generalInfo != null) {
  260. if (data) {
  261. var projectName = 'No project',
  262. clientName = 'No client';
  263. if (data.pid !== undefined) {
  264. if ($projectMap[data.pid] == undefined) {
  265. GM_setValue('_authenticated', 0);
  266. window.location.reload();
  267. return;
  268. }
  269. projectName = $projectMap[data.pid].name;
  270. clientName = $clientMap[$projectMap[data.pid].cid];
  271. }
  272. var content = createTag('div', 'content'),
  273. contentClient = createTag('div', 'client'),
  274. contentProject = createTag('div', 'project'),
  275. contentDescription = createTag('div', 'description');
  276. contentClient.innerHTML = clientName;
  277. contentProject.innerHTML = projectName;
  278. contentDescription.innerHTML = data.description;
  279. content.appendChild(contentClient);
  280. content.appendChild(contentProject);
  281. content.appendChild(contentDescription);
  282. while ($generalInfo.firstChild) {
  283. $generalInfo.removeChild($generalInfo.firstChild);
  284. }
  285. $generalInfo.appendChild(content);
  286. }
  287. updateGeneralInfo(started);
  288. }
  289. }
  290. };
  291.  
  292. this.clickLink = function (data) {
  293. $link.dispatchEvent(new CustomEvent('click'));
  294. };
  295.  
  296. createTimerLink(params);
  297.  
  298. function createTimerLink(params) {
  299. GM_addStyle(GM_getResourceText('togglStyle'));
  300. if (params.generalMode !== undefined && params.generalMode) {
  301. $generalInfo = document.createElement('div');
  302. $generalInfo.id = 'toggl-button-gi-wrapper';
  303. $generalInfo.addEventListener('click', function (e) {
  304. e.preventDefault();
  305. $generalInfo.classList.toggle('collapsed');
  306. });
  307. document.querySelector('body').appendChild($generalInfo);
  308. document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus'));
  309. return;
  310. }
  311. if (params.projectIds !== undefined) {
  312. $projectSelector += '-' + params.projectIds.join('-');
  313. }
  314. if (params.stopCallback !== undefined) {
  315. $stopCallback = params.stopCallback;
  316. }
  317. updateProjectId();
  318. $link = createLink('toggl-button');
  319. $link.classList.add(params.className);
  320.  
  321. if (params.buttonType === 'minimal') {
  322. $link.classList.add('min');
  323. $link.removeChild($link.firstChild);
  324. $buttonTypeMinimal = true;
  325. }
  326.  
  327. $link.addEventListener('click', function (e) {
  328. var opts = '';
  329. e.preventDefault();
  330. if ($isStarted) {
  331. stopTimeEntry();
  332. } else {
  333. var billable = false;
  334. if ($projectId != undefined && $projectId > 0) {
  335. billable = $projectMap[$projectId].billable;
  336. }
  337. opts = {
  338. $projectId: $projectId || null,
  339. billable: billable,
  340. description: invokeIfFunction(params.description),
  341. createdWith: 'TogglButtonGM - ' + params.className
  342. };
  343. createTimeEntry(opts);
  344. }
  345. return false;
  346. });
  347.  
  348. // new button created - reset state
  349. $isStarted = false;
  350.  
  351. // check if our link is the current time entry and set the state if it is
  352. checkCurrentTimeEntry({
  353. $projectId: $projectId,
  354. description: invokeIfFunction(params.description)
  355. });
  356.  
  357. document.querySelector('body').classList.add('toggl-button-available');
  358. if (params.targetSelectors == undefined) {
  359. var wrapper,
  360. existingWrapper = document.querySelectorAll('#toggl-button-wrapper'),
  361. content = createTag('div', 'content');
  362. content.appendChild($link);
  363. content.appendChild(createProjectSelect());
  364. if (existingWrapper.length > 0) {
  365. wrapper = existingWrapper[0];
  366. while (wrapper.firstChild) {
  367. wrapper.removeChild(wrapper.firstChild);
  368. }
  369. wrapper.appendChild(content);
  370. }
  371. else {
  372. wrapper = document.createElement('div');
  373. wrapper.id = 'toggl-button-wrapper';
  374. wrapper.appendChild(content);
  375. document.querySelector('body').appendChild(wrapper);
  376. }
  377. } else {
  378. var elem = params.targetSelectors.context || document;
  379. if (params.targetSelectors.link != undefined) {
  380. elem.querySelector(params.targetSelectors.link).appendChild($link);
  381. }
  382. if (params.targetSelectors.projectSelect != undefined) {
  383. elem.querySelector(params.targetSelectors.projectSelect).appendChild(createProjectSelect());
  384. }
  385. }
  386.  
  387. return $link;
  388. }
  389.  
  390. function createTimeEntry(timeEntry) {
  391. var start = new Date();
  392. GM_xmlhttpRequest({
  393. method: "POST",
  394. url: $activeApiUrl + "/time_entries",
  395. headers: {
  396. "Authorization": "Basic " + btoa($api_token + ':api_token')
  397. },
  398. data: JSON.stringify({
  399. time_entry: {
  400. start: start.toISOString(),
  401. description: timeEntry.description,
  402. wid: $default_wid,
  403. pid: timeEntry.$projectId || null,
  404. billable: timeEntry.billable || false,
  405. duration: -(start.getTime() / 1000),
  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. document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus'));
  454. if (result.status === 200) {
  455. var resp = JSON.parse(result.responseText),
  456. data = resp.data || false;
  457. if (data) {
  458. if ($stopCallback !== undefined) {
  459. var currentdate = new Date();
  460. $stopCallback((currentdate.getTime() - (data.duration * 1000)), data.duration);
  461. }
  462. }
  463. }
  464. }
  465. });
  466. }
  467.  
  468. function createTag(name, className, innerHTML) {
  469. var tag = document.createElement(name);
  470. tag.className = className;
  471.  
  472. if (innerHTML) {
  473. tag.innerHTML = innerHTML;
  474. }
  475.  
  476. return tag;
  477. }
  478.  
  479. function createLink(className, tagName, linkHref, linkText) {
  480. // Param defaults
  481. tagName = tagName || 'a';
  482. linkHref = linkHref || '#';
  483. linkText = linkText || 'Start timer';
  484.  
  485. var link = createTag(tagName, className);
  486.  
  487. if (tagName === 'a') {
  488. link.setAttribute('href', linkHref);
  489. }
  490.  
  491. link.appendChild(document.createTextNode(linkText));
  492. return link;
  493. }
  494.  
  495. function updateGeneralInfo(started) {
  496. if (started) {
  497. $generalInfo.classList.add('active');
  498. } else {
  499. $generalInfo.classList.remove('active');
  500. }
  501. $isStarted = started;
  502. }
  503.  
  504. function updateLink(started) {
  505. var linkText, color = '';
  506.  
  507. if (started) {
  508. document.querySelector('body').classList.add('toggl-button-active');
  509. $link.classList.add('active');
  510. color = '#1ab351';
  511. linkText = 'Stop timer';
  512. } else {
  513. document.querySelector('body').classList.remove('toggl-button-active');
  514. $link.classList.remove('active');
  515. linkText = 'Start timer';
  516. }
  517. $isStarted = started;
  518.  
  519. $link.setAttribute('style', 'color:'+color+';');
  520. if (!$buttonTypeMinimal) {
  521. $link.innerHTML = linkText;
  522. }
  523.  
  524. $projectSelectElem.disabled = $isStarted;
  525. }
  526.  
  527. function updateProjectId(id) {
  528. id = id || GM_getValue($projectSelector, 0);
  529.  
  530. $projectSelected = (id != 0);
  531.  
  532. if (id <= 0) {
  533. $projectId = null;
  534. }
  535. else {
  536. $projectId = id;
  537. }
  538.  
  539. if ($projectSelectElem != undefined) {
  540. $projectSelectElem.value = id;
  541. $projectSelectElem.disabled = $isStarted;
  542. }
  543.  
  544. GM_setValue($projectSelector, id);
  545.  
  546. if ($link != undefined) {
  547. if ($projectSelected) {
  548. $link.classList.remove('hidden');
  549. }
  550. else {
  551. $link.classList.add('hidden');
  552. }
  553. }
  554. }
  555.  
  556. function invokeIfFunction(trial) {
  557. if (trial instanceof Function) {
  558. return trial();
  559. }
  560. return trial;
  561. }
  562.  
  563. function createProjectSelect() {
  564. var pid,
  565. wrapper = createTag('div', 'toggl-button-project-select'),
  566. noneOptionAdded = false,
  567. noneOption = document.createElement('option'),
  568. emptyOption = document.createElement('option'),
  569. resetOption = document.createElement('option');
  570.  
  571. $projectSelectElem = createTag('select');
  572.  
  573. // None Option to indicate that a project should be selected first
  574. if (!$projectSelected) {
  575. noneOption.setAttribute('value', '0');
  576. noneOption.text = '- First select a project -';
  577. $projectSelectElem.appendChild(noneOption);
  578. noneOptionAdded = true;
  579. }
  580.  
  581. // Empty Option for tasks with no project
  582. emptyOption.setAttribute('value', '-1');
  583. emptyOption.text = 'No Project';
  584. $projectSelectElem.appendChild(emptyOption);
  585.  
  586. var optgroup, project, clientMap = [];
  587. for (pid in $projectMap) {
  588. //noinspection JSUnfilteredForInLoop
  589. project = $projectMap[pid];
  590. if (clientMap[project.cid] == undefined) {
  591. optgroup = createTag('optgroup');
  592. optgroup.label = $clientMap[project.cid];
  593. clientMap[project.cid] = optgroup;
  594. $projectSelectElem.appendChild(optgroup);
  595. } else {
  596. optgroup = clientMap[project.cid];
  597. }
  598. var option = document.createElement('option');
  599. option.setAttribute('value', project.id);
  600. option.text = project.name;
  601. optgroup.appendChild(option);
  602. }
  603.  
  604. // Reset Option to reload settings and projects from Toggl
  605. resetOption.setAttribute('value', 'RESET');
  606. resetOption.text = 'Reload settings';
  607. $projectSelectElem.appendChild(resetOption);
  608.  
  609. $projectSelectElem.addEventListener('change', function () {
  610. if ($projectSelectElem.value == 'RESET') {
  611. GM_setValue('_authenticated', 0);
  612. window.location.reload();
  613. return;
  614. }
  615.  
  616. if (noneOptionAdded) {
  617. $projectSelectElem.removeChild(noneOption);
  618. noneOptionAdded = false;
  619. }
  620.  
  621. updateProjectId($projectSelectElem.value);
  622.  
  623. });
  624.  
  625. updateProjectId($projectId);
  626.  
  627. wrapper.appendChild($projectSelectElem);
  628. return wrapper;
  629. }
  630. }
  631.  
  632. }