Pick-A-Brick Color Line

Shows elements' color below their ID in search results

  1. // ==UserScript==
  2. // @name Pick-A-Brick Color Line
  3. // @name:en Pick-A-Brick Color Line
  4. // @description Shows elements' color below their ID in search results
  5. // @namespace Violentmonkey Scripts
  6. // @match https://www.lego.com/en-us/pick-and-build/pick-a-brick*
  7. // @grant none
  8. // @version 1.3
  9. // @author The0x539
  10. // @run-at document-start
  11. // @license AGPL-3.0
  12. // ==/UserScript==
  13. /* jshint esversion: 11 */
  14.  
  15. let initialEnrichmentDone = false;
  16. const ELEMENT_SELECTOR = 'li[class^=ElementsList_leaf_]';
  17. const elementColors = {};
  18.  
  19. // Modify a PaB search result to include the color name below the element/design ID line.
  20. function enrich(element) {
  21. const idElem = element.querySelector('p[data-test=pab-item-elementId]');
  22. const [elemId, partId] = idElem.innerHTML.split(' ')[1].split('/');
  23. const elemColor = elementColors[elemId];
  24. if (!elemColor) {
  25. return;
  26. }
  27. idElem.insertAdjacentHTML('afterend', `<p class="elem-color ds-body-xs-regular">${elemColor}</p>`);
  28. }
  29.  
  30. // Intercept HTTP requests to a LEGO GraphQL endpoint that tells us the colors of the element IDs currently on the page.
  31. // Store data from the response in the `elementColors` dictionary, which maps element IDs to color names.
  32. const actualFetch = window.fetch;
  33. window.fetch = async (...args) => {
  34. const response = await actualFetch(...args);
  35. if (args[0] !== 'https://www.lego.com/api/graphql/PickABrickQuery') {
  36. return response;
  37. }
  38.  
  39. const body = await response.json();
  40.  
  41. try {
  42. for (const element of body.data.searchElements.results) {
  43. elementColors[element.id] = element.facets.color.name;
  44. }
  45.  
  46. // The elements shown on initial page load are included in the initial HTML response,
  47. // so we can't add the color line until the GraphQL request is made.
  48. if (!initialEnrichmentDone) {
  49. document.querySelectorAll(ELEMENT_SELECTOR).forEach(enrich);
  50. initialEnrichmentDone = true;
  51. }
  52. } catch {}
  53.  
  54. // Reconstruct the response because we already consumed the body of the original response object.
  55. const { status, statusText, headers } = response;
  56. const options = { status, statusText, headers };
  57. return new Response(JSON.stringify(body), options);
  58. }
  59.  
  60. const observeOptions = { childList: true, subtree: true };
  61.  
  62. // Searches performed after initial page load mutate the DOM after making the GraphQL request,
  63. // so we need to catch that mutation in order to add color based on data intercepted from the response.
  64. function onUpdateSearchResults(records, observer) {
  65. for (const record of records) {
  66. for (const node of record.addedNodes) {
  67. if (node.matches(ELEMENT_SELECTOR)) {
  68. enrich(node);
  69. } else {
  70. for (const leaf of node.querySelectorAll(ELEMENT_SELECTOR)) {
  71. enrich(leaf);
  72. }
  73. }
  74. }
  75. }
  76. }
  77.  
  78. function onUpdatePage(records, observer) {
  79. for (const record of records) {
  80. for (const node of record.addedNodes) {
  81. const pabResultsWrapper = node.parentNode?.querySelector('#pab-results-wrapper');
  82. if (!pabResultsWrapper) continue;
  83. observer.disconnect(); // we found the element, so stop looking for it
  84. new MutationObserver(onUpdateSearchResults).observe(pabResultsWrapper, observeOptions);
  85. return;
  86. }
  87. }
  88. }
  89.  
  90. document.addEventListener('readystatechange', (event) => {
  91. // This script needs to run at document-start in order to monkey-patch window.fetch,
  92. // but that's too early for us to perform these two DOM operations.
  93. if (document.readyState === 'interactive') {
  94. document.head.insertAdjacentHTML('beforeend', `
  95. <style>
  96. .elem-color {
  97. color: var(--ds-color-text-subdued);
  98. margin: 0;
  99. }
  100. </style>
  101. `);
  102. new MutationObserver(onUpdatePage).observe(document.body, observeOptions);
  103. }
  104. });