Angular Component Modifier

Permite modificar propiedades de componentes en Angular en un servidor de desarrollo con funcionalidad para guardar y restaurar valores y navegar a componentes padre

当前为 2025-03-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Angular Component Modifier
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3
  5. // @description Permite modificar propiedades de componentes en Angular en un servidor de desarrollo con funcionalidad para guardar y restaurar valores y navegar a componentes padre
  6. // @author Blas Santomé Ocampo
  7. // @match http://localhost:*/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. "use strict";
  14.  
  15. const IGNORED_PROPERTIES = ["__ngContext__"];
  16.  
  17. const STORAGE_KEY = "angularModifier_savedStates";
  18. let savedStates = {};
  19.  
  20. let currentElement = null;
  21.  
  22. try {
  23. const storedStates = localStorage.getItem(STORAGE_KEY);
  24. if (storedStates) {
  25. savedStates = JSON.parse(storedStates);
  26. }
  27. } catch (err) {
  28. console.warn("[Angular Modifier] Error al cargar estados guardados:", err);
  29. }
  30. console.log(
  31. "[Angular Modifier] UserScript cargado. Usa OPTION (⌥) + Click en un componente app-*."
  32. );
  33.  
  34. document.addEventListener(
  35. "click",
  36. function (event) {
  37. if (!event.altKey) return;
  38. event.preventDefault();
  39.  
  40. let ng = window.ng;
  41. if (!ng) {
  42. alert(
  43. "⚠️ Angular DevTools no está disponible. Asegúrate de estar en un servidor de desarrollo."
  44. );
  45. return;
  46. }
  47.  
  48. let el = event.target;
  49. let component = null;
  50. let componentName = "Componente Desconocido";
  51. let componentId = "";
  52.  
  53. while (el) {
  54. component = ng.getComponent(el);
  55. if (component && el.tagName.toLowerCase().startsWith("app-")) {
  56. componentName = el.tagName.toLowerCase();
  57. componentId = generateComponentId(el, componentName);
  58. currentElement = el;
  59. break;
  60. }
  61. el = el.parentElement;
  62. }
  63.  
  64. if (!component) {
  65. alert(
  66. "⚠️ No se encontró un componente Angular válido (app-*) en la jerarquía."
  67. );
  68. return;
  69. }
  70.  
  71. console.log(
  72. `[Angular Modifier] Componente seleccionado: ${componentName} (ID: ${componentId})`,
  73. component
  74. );
  75.  
  76. showComponentEditor(component, componentName, componentId);
  77. },
  78. true
  79. );
  80.  
  81. function generateComponentId(element, componentName) {
  82. let path = [];
  83. let current = element;
  84. while (current && current !== document.body) {
  85. let index = 0;
  86. let sibling = current;
  87. while ((sibling = sibling.previousElementSibling)) {
  88. index++;
  89. }
  90. path.unshift(index);
  91. current = current.parentElement;
  92. }
  93. return `${componentName}_${path.join("_")}`;
  94. }
  95.  
  96. function navigateToParentComponent(currentEl) {
  97. let ng = window.ng;
  98. if (!ng) {
  99. alert(
  100. "⚠️ Angular DevTools no está disponible. Asegúrate de estar en un servidor de desarrollo."
  101. );
  102. return false;
  103. }
  104.  
  105. if (!currentEl) {
  106. alert("⚠️ No hay ningún componente seleccionado actualmente.");
  107. return false;
  108. }
  109.  
  110. let parentEl = currentEl.parentElement;
  111. let found = false;
  112.  
  113. while (parentEl) {
  114. if (
  115. parentEl.tagName &&
  116. parentEl.tagName.toLowerCase().startsWith("app-") &&
  117. ng.getComponent(parentEl)
  118. ) {
  119. currentElement = parentEl;
  120. const component = ng.getComponent(parentEl);
  121. const componentName = parentEl.tagName.toLowerCase();
  122. const componentId = generateComponentId(parentEl, componentName);
  123.  
  124. console.log(
  125. `[Angular Modifier] Navegando al componente padre: ${componentName} (ID: ${componentId})`,
  126. component
  127. );
  128.  
  129. const existingModal = document.querySelector(".angular-modifier-modal");
  130. if (existingModal) {
  131. document.body.removeChild(existingModal);
  132. }
  133.  
  134. showComponentEditor(component, componentName, componentId);
  135. found = true;
  136. break;
  137. }
  138. parentEl = parentEl.parentElement;
  139. }
  140.  
  141. if (!found) {
  142. alert("⚠️ No se encontró un componente padre que comience con 'app-'.");
  143. }
  144.  
  145. return found;
  146. }
  147.  
  148. function showComponentEditor(component, componentName, componentId) {
  149. let modal = document.createElement("div");
  150. modal.className = "angular-modifier-modal";
  151. modal.style.position = "fixed";
  152. modal.style.top = "50%";
  153. modal.style.left = "50%";
  154. modal.style.transform = "translate(-50%, -50%)";
  155. modal.style.background = "white";
  156. modal.style.padding = "20px";
  157. modal.style.boxShadow = "0px 0px 10px rgba(0,0,0,0.2)";
  158. modal.style.zIndex = "10000";
  159. modal.style.borderRadius = "8px";
  160. modal.style.width = "400px";
  161. modal.style.maxHeight = "500px";
  162. modal.style.overflowY = "auto";
  163.  
  164. let title = document.createElement("h3");
  165. title.innerText = componentName;
  166. title.style.marginTop = "0";
  167. modal.appendChild(title);
  168.  
  169. let form = document.createElement("form");
  170.  
  171. let formGroups = {};
  172. let editableProps = {};
  173.  
  174. Object.keys(component).forEach((prop) => {
  175. if (IGNORED_PROPERTIES.includes(prop)) return;
  176.  
  177. let value = component[prop];
  178.  
  179. if (typeof value === "function") return;
  180.  
  181. if (
  182. value &&
  183. typeof value === "object" &&
  184. value.constructor.name === "FormGroup"
  185. ) {
  186. formGroups[prop] = value;
  187. appendFormGroupFields(form, value, prop);
  188. return;
  189. }
  190.  
  191. if (value !== null && typeof value === "object") return;
  192.  
  193. let input = appendEditableField(form, component, prop, value);
  194. if (input) {
  195. editableProps[prop] = {
  196. type: typeof value,
  197. input: input,
  198. };
  199. }
  200. });
  201.  
  202. modal.appendChild(form);
  203.  
  204. let parentComponentButton = document.createElement("button");
  205. parentComponentButton.innerText = "Ir al Componente Padre";
  206. parentComponentButton.style.marginTop = "15px";
  207. parentComponentButton.style.width = "100%";
  208. parentComponentButton.style.padding = "8px";
  209. parentComponentButton.style.background = "#ffc107";
  210. parentComponentButton.style.color = "black";
  211. parentComponentButton.style.border = "none";
  212. parentComponentButton.style.borderRadius = "5px";
  213. parentComponentButton.style.cursor = "pointer";
  214. parentComponentButton.style.fontWeight = "bold";
  215.  
  216. parentComponentButton.addEventListener("click", (e) => {
  217. e.preventDefault();
  218. navigateToParentComponent(currentElement);
  219. });
  220.  
  221. modal.appendChild(parentComponentButton);
  222.  
  223. let stateManagementDiv = document.createElement("div");
  224. stateManagementDiv.style.marginTop = "15px";
  225. stateManagementDiv.style.borderTop = "1px solid #eee";
  226. stateManagementDiv.style.paddingTop = "10px";
  227.  
  228. let saveStateButton = document.createElement("button");
  229. saveStateButton.innerText = "Guardar Estado Actual";
  230. saveStateButton.style.padding = "5px 10px";
  231. saveStateButton.style.marginRight = "10px";
  232. saveStateButton.style.background = "#28a745";
  233. saveStateButton.style.color = "white";
  234. saveStateButton.style.border = "none";
  235. saveStateButton.style.borderRadius = "5px";
  236. saveStateButton.style.cursor = "pointer";
  237. saveStateButton.addEventListener("click", (e) => {
  238. e.preventDefault();
  239. saveCurrentState(component, componentId, formGroups, editableProps);
  240. });
  241. stateManagementDiv.appendChild(saveStateButton);
  242.  
  243. let restoreStateButton = document.createElement("button");
  244. restoreStateButton.innerText = "Restaurar Estado";
  245. restoreStateButton.style.padding = "5px 10px";
  246. restoreStateButton.style.background = "#007bff";
  247. restoreStateButton.style.color = "white";
  248. restoreStateButton.style.border = "none";
  249. restoreStateButton.style.borderRadius = "5px";
  250. restoreStateButton.style.cursor = "pointer";
  251.  
  252. if (!savedStates[componentId]) {
  253. restoreStateButton.disabled = true;
  254. restoreStateButton.style.opacity = "0.5";
  255. restoreStateButton.style.cursor = "not-allowed";
  256. }
  257.  
  258. restoreStateButton.addEventListener("click", (e) => {
  259. e.preventDefault();
  260. restoreSavedState(component, componentId, formGroups, editableProps);
  261. });
  262. stateManagementDiv.appendChild(restoreStateButton);
  263.  
  264. modal.appendChild(stateManagementDiv);
  265.  
  266. let fileLabel = document.createElement("label");
  267. fileLabel.innerText = "Cargar JSON:";
  268. fileLabel.style.display = "block";
  269. fileLabel.style.marginTop = "15px";
  270. modal.appendChild(fileLabel);
  271.  
  272. let fileInput = document.createElement("input");
  273. fileInput.type = "file";
  274. fileInput.accept = "application/json";
  275. fileInput.style.marginTop = "5px";
  276. fileInput.style.width = "100%";
  277. fileInput.addEventListener("change", (event) =>
  278. handleFileUpload(event, formGroups)
  279. );
  280. modal.appendChild(fileInput);
  281.  
  282. let exportButton = document.createElement("button");
  283. exportButton.innerText = "Exportar a JSON";
  284. exportButton.style.marginTop = "10px";
  285. exportButton.style.width = "100%";
  286. exportButton.style.padding = "5px";
  287. exportButton.style.background = "#17a2b8";
  288. exportButton.style.color = "white";
  289. exportButton.style.border = "none";
  290. exportButton.style.borderRadius = "5px";
  291. exportButton.style.cursor = "pointer";
  292. exportButton.addEventListener("click", (e) => {
  293. e.preventDefault();
  294. exportToJson(component, formGroups);
  295. });
  296. modal.appendChild(exportButton);
  297.  
  298. let closeButton = document.createElement("button");
  299. closeButton.innerText = "Cerrar";
  300. closeButton.style.marginTop = "10px";
  301. closeButton.style.width = "100%";
  302. closeButton.style.padding = "5px";
  303. closeButton.style.background = "#d9534f";
  304. closeButton.style.color = "white";
  305. closeButton.style.border = "none";
  306. closeButton.style.borderRadius = "5px";
  307. closeButton.style.cursor = "pointer";
  308.  
  309. closeButton.addEventListener("click", () => {
  310. document.body.removeChild(modal);
  311. });
  312.  
  313. modal.appendChild(closeButton);
  314. document.body.appendChild(modal);
  315. }
  316.  
  317. function appendEditableField(form, component, prop, value) {
  318. let label = document.createElement("label");
  319. label.innerText = prop;
  320. label.style.display = "block";
  321. label.style.marginTop = "5px";
  322.  
  323. let input = document.createElement("input");
  324. input.style.width = "100%";
  325. input.style.marginTop = "2px";
  326. input.dataset.propName = prop;
  327.  
  328. if (typeof value === "boolean") {
  329. input.type = "checkbox";
  330. input.checked = value;
  331. } else if (typeof value === "number") {
  332. input.type = "number";
  333. input.value = value;
  334. } else if (typeof value === "string") {
  335. input.type = "text";
  336. input.value = value;
  337. } else {
  338. return null;
  339. }
  340.  
  341. input.addEventListener("change", () => {
  342. try {
  343. if (input.type === "checkbox") {
  344. component[prop] = input.checked;
  345. } else if (input.type === "number") {
  346. component[prop] = parseFloat(input.value);
  347. } else {
  348. component[prop] = input.value;
  349. }
  350. if (typeof ng.applyChanges === "function") {
  351. ng.applyChanges(component);
  352. console.log(`[Angular Modifier] Se aplicaron cambios en ${prop}`);
  353. }
  354. } catch (err) {
  355. alert(`⚠️ Error al actualizar '${prop}': ${err.message}`);
  356. }
  357. });
  358.  
  359. form.appendChild(label);
  360. form.appendChild(input);
  361. return input;
  362. }
  363.  
  364. function appendFormGroupFields(form, formGroup, formGroupName) {
  365. let formGroupTitle = document.createElement("h4");
  366. formGroupTitle.innerText = `Formulario: ${formGroupName}`;
  367. formGroupTitle.style.marginTop = "10px";
  368. formGroupTitle.style.color = "#007bff";
  369. form.appendChild(formGroupTitle);
  370.  
  371. if (formGroup.controls) {
  372. Object.keys(formGroup.controls).forEach((controlName) => {
  373. try {
  374. const control = formGroup.controls[controlName];
  375. const currentValue = control.value;
  376.  
  377. let controlLabel = document.createElement("label");
  378. controlLabel.innerText = controlName;
  379. controlLabel.style.display = "block";
  380. controlLabel.style.marginTop = "5px";
  381. controlLabel.style.marginLeft = "10px";
  382.  
  383. let controlInput = document.createElement("input");
  384. controlInput.style.width = "95%";
  385. controlInput.style.marginTop = "2px";
  386. controlInput.style.marginLeft = "10px";
  387. controlInput.dataset.formGroup = formGroupName;
  388. controlInput.dataset.controlName = controlName;
  389.  
  390. if (typeof currentValue === "boolean") {
  391. controlInput.type = "checkbox";
  392. controlInput.checked = currentValue;
  393. } else if (typeof currentValue === "number") {
  394. controlInput.type = "number";
  395. controlInput.value = currentValue;
  396. } else {
  397. controlInput.type = "text";
  398. controlInput.value =
  399. currentValue !== null && currentValue !== undefined
  400. ? currentValue
  401. : "";
  402. }
  403.  
  404. controlInput.addEventListener("change", () => {
  405. try {
  406. let newValue;
  407. if (controlInput.type === "checkbox") {
  408. newValue = controlInput.checked;
  409. } else if (controlInput.type === "number") {
  410. newValue = parseFloat(controlInput.value);
  411. } else {
  412. newValue = controlInput.value;
  413. }
  414.  
  415. control.setValue(newValue);
  416. console.log(
  417. `[Angular Modifier] Actualizado control '${controlName}' en FormGroup '${formGroupName}'`
  418. );
  419. } catch (err) {
  420. alert(
  421. `⚠️ Error al actualizar control '${controlName}': ${err.message}`
  422. );
  423. }
  424. });
  425.  
  426. form.appendChild(controlLabel);
  427. form.appendChild(controlInput);
  428. } catch (err) {
  429. console.warn(
  430. `[Angular Modifier] Error al mostrar control '${controlName}':`,
  431. err
  432. );
  433. }
  434. });
  435. }
  436. }
  437.  
  438. function handleFileUpload(event, formGroups) {
  439. let file = event.target.files[0];
  440. if (!file) return;
  441.  
  442. let reader = new FileReader();
  443. reader.onload = function (event) {
  444. try {
  445. let jsonData = JSON.parse(event.target.result);
  446. applyJsonToForm(jsonData, formGroups);
  447. } catch (err) {
  448. alert("⚠️ Error al cargar JSON: " + err.message);
  449. }
  450. };
  451. reader.readAsText(file);
  452. }
  453.  
  454. function applyJsonToForm(jsonData, formGroups) {
  455. if (jsonData.properties) {
  456. Object.keys(jsonData.properties).forEach((prop) => {
  457. if (IGNORED_PROPERTIES.includes(prop)) return;
  458.  
  459. try {
  460. const inputElement = document.querySelector(
  461. `input[data-prop-name="${prop}"]`
  462. );
  463. if (inputElement) {
  464. if (inputElement.type === "checkbox") {
  465. inputElement.checked = jsonData.properties[prop];
  466. } else {
  467. inputElement.value = jsonData.properties[prop];
  468. }
  469. inputElement.dispatchEvent(new Event("change"));
  470. }
  471. } catch (err) {
  472. console.warn(
  473. `[Angular Modifier] Error al aplicar propiedad '${prop}':`,
  474. err
  475. );
  476. }
  477. });
  478. }
  479.  
  480. if (jsonData.formGroups) {
  481. Object.keys(jsonData.formGroups).forEach((groupName) => {
  482. if (formGroups[groupName]) {
  483. let formGroup = formGroups[groupName];
  484. const groupData = jsonData.formGroups[groupName];
  485.  
  486. Object.keys(groupData).forEach((controlName) => {
  487. if (formGroup.controls[controlName]) {
  488. try {
  489. formGroup.controls[controlName].setValue(
  490. groupData[controlName]
  491. );
  492.  
  493. const controlInput = document.querySelector(
  494. `input[data-form-group="${groupName}"][data-control-name="${controlName}"]`
  495. );
  496. if (controlInput) {
  497. if (controlInput.type === "checkbox") {
  498. controlInput.checked = groupData[controlName];
  499. } else {
  500. controlInput.value = groupData[controlName];
  501. }
  502. }
  503.  
  504. console.log(
  505. `[Angular Modifier] Campo '${controlName}' de '${groupName}' actualizado`
  506. );
  507. } catch (err) {
  508. console.warn(
  509. `[Angular Modifier] Error al actualizar control '${controlName}':`,
  510. err
  511. );
  512. }
  513. }
  514. });
  515. }
  516. });
  517. }
  518. }
  519.  
  520. function exportToJson(component, formGroups) {
  521. let exportData = {
  522. properties: {},
  523. formGroups: {},
  524. };
  525.  
  526. Object.keys(component).forEach((prop) => {
  527. if (IGNORED_PROPERTIES.includes(prop)) return;
  528.  
  529. let value = component[prop];
  530. if (
  531. typeof value !== "function" &&
  532. value !== null &&
  533. typeof value !== "object"
  534. ) {
  535. exportData.properties[prop] = value;
  536. }
  537. });
  538.  
  539. Object.keys(formGroups).forEach((groupName) => {
  540. const formGroup = formGroups[groupName];
  541. exportData.formGroups[groupName] = {};
  542.  
  543. if (formGroup.controls) {
  544. Object.keys(formGroup.controls).forEach((controlName) => {
  545. try {
  546. exportData.formGroups[groupName][controlName] =
  547. formGroup.controls[controlName].value;
  548. } catch (err) {
  549. console.warn(
  550. `[Angular Modifier] Error al exportar control '${controlName}':`,
  551. err
  552. );
  553. }
  554. });
  555. }
  556. });
  557.  
  558. const jsonString = JSON.stringify(exportData, null, 2);
  559. const blob = new Blob([jsonString], { type: "application/json" });
  560. const url = URL.createObjectURL(blob);
  561.  
  562. const a = document.createElement("a");
  563. a.href = url;
  564. a.download = `angular-component-${Date.now()}.json`;
  565. document.body.appendChild(a);
  566. a.click();
  567. document.body.removeChild(a);
  568. URL.revokeObjectURL(url);
  569. }
  570.  
  571. function saveCurrentState(component, componentId, formGroups, editableProps) {
  572. let state = {
  573. properties: {},
  574. formGroups: {},
  575. };
  576.  
  577. Object.keys(editableProps).forEach((prop) => {
  578. if (IGNORED_PROPERTIES.includes(prop)) return;
  579.  
  580. const input = editableProps[prop].input;
  581. if (input.type === "checkbox") {
  582. state.properties[prop] = input.checked;
  583. } else if (input.type === "number") {
  584. state.properties[prop] = parseFloat(input.value);
  585. } else {
  586. state.properties[prop] = input.value;
  587. }
  588. });
  589.  
  590. Object.keys(formGroups).forEach((groupName) => {
  591. const formGroup = formGroups[groupName];
  592. state.formGroups[groupName] = {};
  593.  
  594. if (formGroup.controls) {
  595. Object.keys(formGroup.controls).forEach((controlName) => {
  596. try {
  597. state.formGroups[groupName][controlName] =
  598. formGroup.controls[controlName].value;
  599. } catch (err) {
  600. console.warn(
  601. `[Angular Modifier] Error al guardar control '${controlName}':`,
  602. err
  603. );
  604. }
  605. });
  606. }
  607. });
  608.  
  609. savedStates[componentId] = state;
  610.  
  611. try {
  612. localStorage.setItem(STORAGE_KEY, JSON.stringify(savedStates));
  613. alert(`✅ Estado guardado correctamente para ${componentId}`);
  614. } catch (err) {
  615. console.error("[Angular Modifier] Error al guardar estado:", err);
  616. alert("⚠️ Error al guardar estado: " + err.message);
  617. }
  618. }
  619.  
  620. function restoreSavedState(
  621. component,
  622. componentId,
  623. formGroups,
  624. editableProps
  625. ) {
  626. const savedState = savedStates[componentId];
  627. if (!savedState) {
  628. alert("⚠️ No hay estado guardado para este componente");
  629. return;
  630. }
  631.  
  632. if (savedState.properties) {
  633. Object.keys(savedState.properties).forEach((prop) => {
  634. if (IGNORED_PROPERTIES.includes(prop)) return;
  635.  
  636. if (editableProps[prop]) {
  637. const input = editableProps[prop].input;
  638. const value = savedState.properties[prop];
  639.  
  640. if (input.type === "checkbox") {
  641. input.checked = value;
  642. } else {
  643. input.value = value;
  644. }
  645.  
  646. try {
  647. component[prop] = value;
  648. } catch (err) {
  649. console.warn(
  650. `[Angular Modifier] Error al restaurar propiedad '${prop}':`,
  651. err
  652. );
  653. }
  654. }
  655. });
  656. }
  657.  
  658. if (savedState.formGroups) {
  659. Object.keys(savedState.formGroups).forEach((groupName) => {
  660. if (formGroups[groupName]) {
  661. const formGroup = formGroups[groupName];
  662. const groupData = savedState.formGroups[groupName];
  663.  
  664. Object.keys(groupData).forEach((controlName) => {
  665. if (formGroup.controls[controlName]) {
  666. try {
  667. formGroup.controls[controlName].setValue(
  668. groupData[controlName]
  669. );
  670.  
  671. const controlInput = document.querySelector(
  672. `input[data-form-group="${groupName}"][data-control-name="${controlName}"]`
  673. );
  674. if (controlInput) {
  675. if (controlInput.type === "checkbox") {
  676. controlInput.checked = groupData[controlName];
  677. } else {
  678. controlInput.value = groupData[controlName];
  679. }
  680. }
  681. } catch (err) {
  682. console.warn(
  683. `[Angular Modifier] Error al restaurar control '${controlName}':`,
  684. err
  685. );
  686. }
  687. }
  688. });
  689. }
  690. });
  691. }
  692.  
  693. if (typeof ng.applyChanges === "function") {
  694. ng.applyChanges(component);
  695. console.log(`[Angular Modifier] Se restauró el estado del componente`);
  696. }
  697.  
  698. alert(`✅ Estado restaurado correctamente para ${componentId}`);
  699. }
  700. })();