github-stars-tagger

为github的star增加标签管理功能

  1. // ==UserScript==
  2. // github-stars-tagger
  3. // 需要在页面https://github.com/stars中管理标签, 所以在导航栏上增加了 /stars 的链接
  4. // 代码来自: https://github.com/artisologic/github-stars-tagger
  5. // 存储改为了 localStorage
  6. //
  7. // @name github-stars-tagger
  8. // @namespace http://tampermonkey.net/
  9. // @version 0.1
  10. // @description 为github的star增加标签管理功能
  11. // @author You
  12. // @match https://github.com/*
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. // libs/EventEmitter.js
  17. ((window) => {
  18.  
  19. 'use strict';
  20.  
  21.  
  22. /**
  23. * @class EventEmitter
  24. */
  25. class EventEmitter {
  26.  
  27. constructor() {
  28. this._listeners = [];
  29. }
  30.  
  31. on(eventName, callback) {
  32. this._listeners.push({
  33. name: eventName,
  34. callback: callback
  35. });
  36.  
  37. return this;
  38. }
  39.  
  40. off(eventName, callback) {
  41. this._listeners.forEach((listener, index) => {
  42. if (listener.name === eventName && listener.callback === callback) {
  43. this._listeners.splice(index, 1);
  44. }
  45. });
  46.  
  47. return this;
  48. }
  49.  
  50. emit(eventName, data) {
  51. this._listeners
  52. .filter(listener => listener.name === eventName)
  53. .forEach(listener => listener.callback(data, this, eventName));
  54.  
  55. return this;
  56. }
  57.  
  58. }
  59.  
  60.  
  61. window.GSM = window.GSM || {};
  62. GSM.EventEmitter = EventEmitter;
  63. })(window);
  64.  
  65. // libs/Model.js
  66. ((window) => {
  67.  
  68. 'use strict';
  69.  
  70.  
  71. /**
  72. * @class Model
  73. */
  74. class Model extends GSM.EventEmitter {
  75.  
  76. constructor(data) {
  77. super();
  78.  
  79. this.data = data;
  80. }
  81.  
  82. }
  83.  
  84.  
  85. window.GSM = window.GSM || {};
  86. GSM.Model = Model;
  87. })(window);
  88.  
  89. // libs/TagsStore.js
  90. ((window) => {
  91.  
  92. 'use strict';
  93.  
  94. const KEY = 'github-stars-tagger';
  95.  
  96. /**
  97. * @class TagsStore
  98. */
  99. class TagsStore {
  100.  
  101. constructor() {
  102.  
  103. }
  104.  
  105. get(key) {
  106. // console.log('get', key);
  107. const promise = new Promise((resolve, reject) => {
  108. var items = localStorage.getItem(KEY);
  109. var data = {};
  110. if (items) {
  111. data = JSON.parse(items);
  112. }
  113. if (key === undefined) {
  114. resolve(data);
  115. } else if (typeof data[key] !== 'undefined') {
  116. resolve(data[key]);
  117. } else {
  118. reject('TagsStore.get('+key+')获取失败');
  119. }
  120. });
  121.  
  122. promise.catch(error => {
  123. GSM.utils.track('Sync', 'get', 'error', error);
  124. });
  125.  
  126. return promise;
  127. }
  128.  
  129. set(key, value) {
  130. // console.log('set', key, value);
  131. var items = localStorage.getItem(KEY);
  132. var data = {};
  133. if (items) {
  134. data = JSON.parse(items);
  135. }
  136. data[key] = value;
  137. const promise = new Promise((resolve, reject) => {
  138. if (localStorage.setItem(KEY, JSON.stringify(data))) {
  139. resolve();
  140. } else {
  141. reject();
  142. }
  143. });
  144.  
  145. promise.catch(error => {
  146. GSM.utils.track('Sync', 'set', 'error', error);
  147. });
  148.  
  149. return promise;
  150. }
  151.  
  152. remove(key) {
  153. // console.log('remove', key);
  154. const promise = new Promise((resolve, reject) => {
  155. if (localStorage.setItem(key, null)) {
  156. resolve();
  157. } else {
  158. reject('remove error');
  159. }
  160. });
  161.  
  162. promise.catch(error => {
  163. GSM.utils.track('Sync', 'remove', 'error', error);
  164. });
  165.  
  166. return promise;
  167. }
  168.  
  169. clear() {
  170. // console.log('clear');
  171. const promise = new Promise((resolve, reject) => {
  172. });
  173.  
  174. promise.catch(error => {
  175. GSM.utils.track('Sync', 'clear', 'error', error);
  176. });
  177.  
  178. return promise;
  179. }
  180.  
  181. }
  182.  
  183.  
  184. window.GSM = window.GSM || {};
  185. GSM.TagsStore = TagsStore;
  186. })(window);
  187.  
  188. // libs/utils.js
  189. ((window) => {
  190.  
  191. 'use strict';
  192.  
  193.  
  194. const utils = {
  195.  
  196. insertAfter(newNode, referenceNode) {
  197. referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  198. },
  199.  
  200. unique(array) {
  201. const hash = {};
  202. const res = [];
  203.  
  204. for (let i = 0; i < array.length; i++) {
  205. const item = array[i];
  206.  
  207. if (!hash[item]) {
  208. hash[item] = true;
  209. res.push(item);
  210. }
  211. }
  212.  
  213. return res;
  214. },
  215.  
  216. message(command, data) {
  217. // chrome.runtime.sendMessage({ command, data });
  218. },
  219.  
  220. track(category, action, label, value) {
  221. utils.message('trackEvent', { category, action, label, value });
  222. }
  223.  
  224. };
  225.  
  226.  
  227. window.GSM = window.GSM || {};
  228. GSM.utils = utils;
  229. })(window);
  230.  
  231. // libs/View.js
  232. ((window) => {
  233.  
  234. 'use strict';
  235.  
  236.  
  237. /**
  238. * @class View
  239. */
  240. class View extends GSM.EventEmitter {
  241.  
  242. constructor() {
  243. super();
  244.  
  245. this.refs = {
  246. root: this.createRootElement()
  247. };
  248.  
  249. this.handlers = {};
  250. }
  251.  
  252. static getRootClass() {
  253. // override this method
  254. return '';
  255. }
  256.  
  257. createRootElement() {
  258. const rootElem = document.createElement('div');
  259. rootElem.classList.add(this.constructor.getRootClass());
  260.  
  261. return rootElem;
  262. }
  263.  
  264. render() {
  265. // override this method
  266. }
  267.  
  268. getElement(selector) {
  269. if (typeof selector === 'undefined') {
  270. return this.refs.root;
  271. } else {
  272. return this.refs.root.querySelector(selector);
  273. }
  274. }
  275.  
  276. injectInto(parentElem) {
  277. parentElem.appendChild(this.getElement());
  278. }
  279.  
  280. injectAfter(siblingElem) {
  281. GSM.utils.insertAfter(this.getElement(), siblingElem);
  282. }
  283. }
  284.  
  285.  
  286. window.GSM = window.GSM || {};
  287. GSM.View = View;
  288. })(window);
  289.  
  290. // models/Tags.js
  291. ((window) => {
  292.  
  293. 'use strict';
  294.  
  295.  
  296. /**
  297. * @class Tags
  298. */
  299. class Tags extends GSM.Model {
  300.  
  301. constructor(data) {
  302. super(data);
  303. }
  304.  
  305. getTagsForRepo(repoId) {
  306. return this.data[repoId] || [];
  307. }
  308.  
  309. setTagsForRepo(repoId, unserializedTags) {
  310. const serializedTags = unserializedTags.split(',')
  311. .map(tag => tag.trim())
  312. .filter(tag => tag !== '');
  313.  
  314. const hasNoTags = serializedTags.length === 0;
  315. const repoChangeEventName = 'change:' + repoId;
  316.  
  317. if (hasNoTags) {
  318. delete this.data[repoId];
  319. const changeData = { key: repoId, deleted: true };
  320. this.emit('change', changeData);
  321. this.emit(repoChangeEventName, changeData);
  322. } else {
  323. const newTags = GSM.utils.unique(serializedTags);
  324. const changeData = { key: repoId, value: newTags };
  325. this.data[repoId] = newTags;
  326. this.emit('change', changeData);
  327. this.emit(repoChangeEventName, changeData);
  328. }
  329. }
  330.  
  331. getDeserializedTagsForRepo(repoId) {
  332. return this.getTagsForRepo(repoId).join(', ');
  333. }
  334.  
  335. byTag() {
  336. const pivotedData = {};
  337.  
  338. for (const repoId in this.data) {
  339. const tags = this.getTagsForRepo(repoId);
  340. tags.forEach(tag => {
  341. if (!(tag in pivotedData)) { pivotedData[tag] = []; }
  342. pivotedData[tag].push(repoId);
  343. });
  344. }
  345.  
  346. return pivotedData;
  347. }
  348.  
  349. byTagSortedByUse() {
  350. const modelByTag = this.byTag();
  351.  
  352. return Object.keys(modelByTag)
  353. .map(tag => createTagObject(tag))
  354. .sort(byMostUsed);
  355.  
  356.  
  357. function createTagObject(tag) {
  358. return {
  359. name: tag,
  360. repos: modelByTag[tag]
  361. };
  362. }
  363.  
  364. function byMostUsed(tagObject1, tagObject2) {
  365. const diff = tagObject2.repos.length - tagObject1.repos.length;
  366. // default to alphanumerical sort
  367. if (diff === 0) { return tagObject2.name < tagObject1.name ? 1 : -1; }
  368. return diff;
  369. }
  370. }
  371.  
  372. }
  373.  
  374.  
  375. window.GSM = window.GSM || {};
  376. GSM.Tags = Tags;
  377. })(window);
  378.  
  379. // views/TagLineView.js
  380. ((window) => {
  381.  
  382. 'use strict';
  383.  
  384.  
  385. /**
  386. * @class TagLineView
  387. */
  388. class TagLineView extends GSM.View {
  389.  
  390. constructor(model, repoId) {
  391. super();
  392.  
  393. this.model = model;
  394. this.repoId = repoId;
  395. }
  396.  
  397. static getRootClass() {
  398. return 'GsmTagLine';
  399. }
  400.  
  401. createRootElement() {
  402. const rootElem = document.createElement('p');
  403. rootElem.classList.add(TagLineView.getRootClass(), 'f6', 'text-gray', 'mt-2');
  404.  
  405. return rootElem;
  406. }
  407.  
  408. render() {
  409. if (this.rendered) {
  410. this.removeEvents();
  411. }
  412.  
  413. const tags = this.model.getDeserializedTagsForRepo(this.repoId);
  414. const noTagsModifierClass = 'GsmTagLine--noTags';
  415.  
  416. this.getElement().classList.toggle(noTagsModifierClass, !tags);
  417. this.getElement().innerHTML = `
  418. <svg class="octicon octicon-tag GsmTagLine-icon" viewBox="0 0 14 16" version="1.1" width="14" height="16" aria-hidden="true">
  419. <path fill-rule="evenodd" d="M7.73 1.73C7.26 1.26 6.62 1 5.96 1H3.5C2.13 1 1 2.13 1 3.5v2.47c0 .66.27 1.3.73 1.77l6.06 6.06c.39.39 1.02.39 1.41 0l4.59-4.59a.996.996 0 0 0 0-1.41L7.73 1.73zM2.38 7.09c-.31-.3-.47-.7-.47-1.13V3.5c0-.88.72-1.59 1.59-1.59h2.47c.42 0 .83.16 1.13.47l6.14 6.13-4.73 4.73-6.13-6.15zM3.01 3h2v2H3V3h.01z"></path>
  420. </svg>
  421. <span class="GsmTagLine-tags">${ tags }</span>
  422. <span class="GsmTagLine-separator"> </span>
  423. <button class="GsmTagLine-editButton" type="button" title="Click to edit">Edit</button>
  424. <input class="GsmTagLine-tagsInput form-control input-sm" type="text" value="${ tags }" placeholder="Enter comma-separated tags" spellcheck="false" autocomplete="off" />
  425. `;
  426.  
  427. this.refs.editButton = this.getElement('.GsmTagLine-editButton');
  428. this.refs.tagsInput = this.getElement('.GsmTagLine-tagsInput');
  429.  
  430. this.addEvents();
  431. this.rendered = true;
  432. }
  433.  
  434. addEvents() {
  435. this.handlers = {
  436. modelChange: (changeData, target, eventName) => this.onModelChanged(changeData, target, eventName),
  437. editButtonClick: event => this.onEditButtonClicked(event),
  438. tagsInputKeydown: event => this.onTagsInputKeydowned(event),
  439. tagsInputBlur: event => this.onTagsInputBlurred(event)
  440. };
  441.  
  442. this.model.on('change:' + this.repoId, this.handlers.modelChange);
  443. this.refs.editButton.addEventListener('click', this.handlers.editButtonClick);
  444. this.refs.tagsInput.addEventListener('keydown', this.handlers.tagsInputKeydown);
  445. this.refs.tagsInput.addEventListener('blur', this.handlers.tagsInputBlur);
  446. }
  447.  
  448. removeEvents() {
  449. this.model.off('change:' + this.repoId, this.handlers.modelChange);
  450. this.refs.editButton.removeEventListener('click', this.handlers.editButtonClick);
  451. this.refs.tagsInput.removeEventListener('keydown', this.handlers.tagsInputKeydown);
  452. this.refs.tagsInput.removeEventListener('blur', this.handlers.tagsInputBlur);
  453.  
  454. this.handlers = {};
  455. }
  456.  
  457. onModelChanged() {
  458. this.render();
  459. }
  460.  
  461. onEditButtonClicked() {
  462. this.enterEditMode();
  463. GSM.utils.track('TagLine', 'edit');
  464. }
  465.  
  466. onTagsInputKeydowned(event) {
  467. const ENTER = 13;
  468. const ESCAPE = 27;
  469.  
  470. if (event.keyCode === ESCAPE) {
  471. this.exitEditMode();
  472. GSM.utils.track('TagLine', 'escape');
  473. } else if (event.keyCode === ENTER) {
  474. const newTags = event.currentTarget.value;
  475. this.exitEditMode(newTags);
  476. GSM.utils.track('TagLine', 'save');
  477. }
  478. }
  479.  
  480. onTagsInputBlurred() {
  481. this.exitEditMode();
  482. GSM.utils.track('TagLine', 'blur');
  483. }
  484.  
  485. enterEditMode() {
  486. this.getElement().classList.add('-is-editing');
  487.  
  488. // help entering next tag
  489. if (this.refs.tagsInput.value !== '') { this.refs.tagsInput.value += ', '; }
  490.  
  491. // focus at the end of input
  492. this.refs.tagsInput.focus();
  493. const length = this.refs.tagsInput.value.length;
  494. this.refs.tagsInput.setSelectionRange(length, length);
  495. }
  496.  
  497. exitEditMode(newTags) {
  498. if (typeof newTags === 'undefined') {
  499. this.render();
  500. } else {
  501. this.model.setTagsForRepo(this.repoId, newTags);
  502. }
  503.  
  504. this.getElement().classList.remove('-is-editing');
  505. }
  506.  
  507. }
  508.  
  509.  
  510. window.GSM = window.GSM || {};
  511. GSM.TagLineView = TagLineView;
  512. })(window);
  513.  
  514. // views/TagSidebarView.js
  515. ((window) => {
  516.  
  517. 'use strict';
  518.  
  519.  
  520. /**
  521. * @class TagSidebarView
  522. */
  523. class TagSidebarView extends GSM.View {
  524.  
  525. constructor(model) {
  526. super();
  527.  
  528. this.model = model;
  529. }
  530.  
  531. static getRootClass() {
  532. return 'GsmTagSidebar';
  533. }
  534.  
  535. render() {
  536. if (this.rendered) {
  537. this.removeEvents();
  538. }
  539.  
  540. const sortedTags = this.model.byTagSortedByUse();
  541. const tagsCount = sortedTags.length;
  542. const tagsCountIndicator = tagsCount ? `<span class="count">${ tagsCount }</span>` : '';
  543.  
  544. this.getElement().innerHTML = `
  545. <h3 class="h4 mb-2">
  546. Filter by tags
  547. ${ tagsCountIndicator }
  548. </h3>
  549. <ul class="filter-list small GsmTagSidebar-tagList">
  550. ${ this.renderTags(sortedTags) }
  551. </ul>
  552. <hr />
  553. `;
  554.  
  555. this.addEvents();
  556. this.rendered = true;
  557. }
  558.  
  559. renderTags(sortedTags) {
  560. if (sortedTags.length === 0) {
  561. return `<span class="filter-item GsmTagSidebar-noTagsMessage">No tags</span>`;
  562. }
  563. return sortedTags.map(tagModel => this.renderTag(tagModel)).join('');
  564. }
  565.  
  566. renderTag(tagModel) {
  567. return `
  568. <li>
  569. <label class="GsmTagSidebar-label">
  570. <span class="filter-item">
  571. ${ tagModel.name }
  572. <span class="count">${ tagModel.repos.length }</span>
  573. </span>
  574. <input class="GsmTagSidebar-checkbox" type="checkbox" />
  575. <ul class="GsmRepoList">
  576. ${ this.renderTagRepos(tagModel) }
  577. </ul>
  578. </label>
  579. </li>
  580. `;
  581. }
  582.  
  583. renderTagRepos(tagModel) {
  584. return tagModel.repos.map(tagModel => this.renderTagRepo(tagModel)).join('');
  585. }
  586.  
  587. renderTagRepo(repoId) {
  588. return `
  589. <li class="GsmRepoList-item css-truncate">
  590. <a class="css-truncate-target" href="/${ repoId }">${ repoId }</a>
  591. </li>
  592. `;
  593. }
  594.  
  595. addEvents() {
  596. this.handlers = {
  597. modelChange: (changeData, target, eventName) => this.onModelChanged(changeData, target, eventName),
  598. click: (event) => this.onClicked(event)
  599. };
  600.  
  601. this.model.on('change', this.handlers.modelChange);
  602. this.getElement().addEventListener('click', this.handlers.click, false);
  603. }
  604.  
  605. removeEvents() {
  606. this.model.off('change', this.handlers.modelChange);
  607. this.getElement().removeEventListener('click', this.handlers.click, false);
  608.  
  609. this.handlers = {};
  610. }
  611.  
  612. onModelChanged() {
  613. this.render();
  614. }
  615.  
  616. onClicked(event) {
  617. if (event.target && event.target.classList.contains('filter-item')) {
  618. GSM.utils.track('TagSidebar', 'click', 'tag');
  619. }
  620. }
  621.  
  622. }
  623.  
  624.  
  625. window.GSM = window.GSM || {};
  626. GSM.TagSidebarView = TagSidebarView;
  627. })(window);
  628.  
  629. githubStarsTaggerInit();
  630.  
  631. // main.js
  632.  
  633. function githubStarsTaggerInit() {
  634. 'use strict';
  635.  
  636. addStarPageBtn();
  637. const tagsStore = new GSM.TagsStore();
  638. if (isStarPage(location.href)) {
  639. addStyle();
  640. tagsStore.get()
  641. .then(createModel)
  642. .then(initViews)
  643. .then(initSync);
  644. }
  645.  
  646. function addStarPageBtn() {
  647. var navs = document.querySelector('ul.flex-items-center.text-bold');
  648. if (navs) {
  649. navs.innerHTML += '<li><a href="/stars" class="js-selected-navigation-item HeaderNavlink px-2">Stars</a></li>';
  650. }
  651. }
  652.  
  653. function isStarPage(path) {
  654. return path === '/stars' || path === '/stars/' || Boolean(path.match(/\/stars/));
  655. }
  656.  
  657. function createModel(data) {
  658. return new GSM.Tags(data);
  659. }
  660.  
  661. function initViews(tagsModel) {
  662. initTagLines(tagsModel);
  663. initTagSidebar(tagsModel);
  664.  
  665. return tagsModel;
  666.  
  667.  
  668. function initTagLines(model) {
  669. const repoItemSelector = '.repo-list > li';
  670.  
  671. // on page load
  672. addTagLines();
  673.  
  674. // when sorting, filtering, paginating was used
  675. addAjaxPageRefreshEventListener(onAjaxPageRefreshed);
  676.  
  677.  
  678. function onAjaxPageRefreshed(newPath) {
  679. removeTagLines();
  680. const shouldAddTagLines = isCurrentPathSupported(newPath);
  681. if (shouldAddTagLines) {
  682. addTagLines();
  683. }
  684. }
  685.  
  686. function addTagLines() {
  687. const starredRepoElems = document.querySelectorAll(repoItemSelector);
  688. Array.from(starredRepoElems).forEach(starredRepoElem => addTagLine(starredRepoElem));
  689.  
  690. function addTagLine(starredRepoElem) {
  691. const repoId = starredRepoElem.querySelector('h3 a').getAttribute('href').substring(1);
  692. const view = new GSM.TagLineView(model, repoId);
  693. view.render();
  694. view.injectInto(starredRepoElem);
  695. }
  696. }
  697.  
  698. function removeTagLines() {
  699. const starredRepoElems = document.querySelectorAll(repoItemSelector);
  700. Array.from(starredRepoElems).forEach(starredRepoElem => removeTagLine(starredRepoElem));
  701.  
  702. function removeTagLine(starredRepoElem) {
  703. const oldTagLineElem = starredRepoElem.querySelector('.' + GSM.TagLineView.getRootClass());
  704. if (oldTagLineElem) { oldTagLineElem.remove(); }
  705. }
  706. }
  707.  
  708. function isCurrentPathSupported(path) {
  709. return path === '/stars' || path === '/stars/' || Boolean(path.match(/\/stars\/?\?.+/));
  710. }
  711. }
  712.  
  713. function initTagSidebar(model) {
  714. const ajaxContentElem = document.querySelector('.explore-pjax-container');
  715.  
  716. // on page load
  717. addSidebar();
  718.  
  719. // when sorting, filtering, paginating was used
  720. addAjaxPageRefreshEventListener(onAjaxPageRefreshed);
  721.  
  722.  
  723. function onAjaxPageRefreshed(newPath) {
  724. removeSidebar();
  725. const shouldAddSidebar = isCurrentPathSupported(newPath);
  726. if (shouldAddSidebar) {
  727. addSidebar();
  728. }
  729. }
  730.  
  731. function addSidebar() {
  732. const firstSidebarSeparatorElem = ajaxContentElem.querySelector('.col-md-3.float-md-left.mt-3 hr:first-of-type');
  733. const view = new GSM.TagSidebarView(model);
  734. view.render();
  735. view.injectAfter(firstSidebarSeparatorElem);
  736. }
  737.  
  738. function removeSidebar() {
  739. const oldTagSidebarElem = ajaxContentElem.querySelector('.' + GSM.TagSidebarView.getRootClass());
  740. if (oldTagSidebarElem) { oldTagSidebarElem.remove(); }
  741. }
  742. }
  743.  
  744. function addAjaxPageRefreshEventListener(callback) {
  745. const ajaxContentElem = document.querySelector('.explore-pjax-container');
  746.  
  747. const observer = new MutationObserver(mutations => {
  748. mutations.forEach(mutation => {
  749. if (mutation.addedNodes.length > 0) {
  750. callback(document.location.pathname);
  751. }
  752. });
  753. });
  754.  
  755. const config = { childList: true };
  756. observer.observe(ajaxContentElem, config);
  757. }
  758.  
  759. }
  760.  
  761. function initSync(tagsModel) {
  762. tagsModel.on('change', onModelChanged);
  763.  
  764.  
  765. function onModelChanged(changeData) {
  766. if (changeData.deleted) {
  767. tagsStore.remove(changeData.key);
  768. } else {
  769. tagsStore.set(changeData.key, changeData.value);
  770. }
  771. }
  772. }
  773.  
  774. function addStyle() {
  775. var cssText = '<style>.GsmTagLine{position:relative}.GsmTagLine-icon{color:currentColor}.GsmTagLine--noTags .GsmTagLine-icon{opacity:.35}.GsmTagLine.-is-editing .GsmTagLine-icon{position:absolute;top:7px;left:7px}.GsmTagLine.-is-editing .GsmTagLine-tags{display:none}.GsmTagLine--noTags .GsmTagLine-tags{opacity:.35}.GsmTagLine-separator{opacity:.35}.GsmTagLine.-is-editing .GsmTagLine-separator{display:none}.GsmTagLine-editButton{padding:.5em;position:relative;margin:-0.5em;background-color:transparent;border:0;opacity:.35;outline:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.GsmTagLine-editButton:hover,.GsmTagLine-editButton:focus{opacity:1}.GsmTagLine.-is-editing .GsmTagLine-editButton{display:none}.GsmTagLine-tagsInput{display:none;width:400px;padding-left:25px !important}.GsmTagLine.-is-editing .GsmTagLine-tagsInput{display:inline-block} .GsmTagSidebar h3 .count{float:right;margin-right:10px}.GsmTagSidebar-noTagsMessage{margin-bottom:15px !important}.GsmTagSidebar-noTagsMessage:hover{background-color:transparent !important;cursor:auto}.GsmTagSidebar-tagList{max-height:19.2em;overflow:auto}.GsmTagSidebar-tagList>li:last-child{margin-bottom:15px}.GsmTagSidebar-tagList+hr{margin-top:0}.GsmTagSidebar-label{font-size:inherit;font-weight:inherit}.GsmTagSidebar-checkbox{display:none}.GsmRepoList{display:none;list-style:none}.GsmRepoList-item{display:block;padding:4px 10px;margin:0 0 2px;font-size:12px}.GsmRepoList-item a{display:block;max-width:210px !important}.GsmTagSidebar-checkbox:checked+.GsmRepoList{display:block}</style>';
  776. document.body.innerHTML += cssText;
  777. }
  778. }