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 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Angular Component Modifier
// @namespace    http://tampermonkey.net/
// @version      1.3
// @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
// @author       Blas Santomé Ocampo
// @match        http://localhost:*/*
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  "use strict";

  const IGNORED_PROPERTIES = ["__ngContext__"];

  const STORAGE_KEY = "angularModifier_savedStates";
  let savedStates = {};

  let currentElement = null;

  try {
    const storedStates = localStorage.getItem(STORAGE_KEY);
    if (storedStates) {
      savedStates = JSON.parse(storedStates);
    }
  } catch (err) {
    console.warn("[Angular Modifier] Error al cargar estados guardados:", err);
  }
  console.log(
    "[Angular Modifier] UserScript cargado. Usa OPTION (⌥) + Click en un componente app-*."
  );

  document.addEventListener(
    "click",
    function (event) {
      if (!event.altKey) return;
      event.preventDefault();

      let ng = window.ng;
      if (!ng) {
        alert(
          "⚠️ Angular DevTools no está disponible. Asegúrate de estar en un servidor de desarrollo."
        );
        return;
      }

      let el = event.target;
      let component = null;
      let componentName = "Componente Desconocido";
      let componentId = "";

      while (el) {
        component = ng.getComponent(el);
        if (component && el.tagName.toLowerCase().startsWith("app-")) {
          componentName = el.tagName.toLowerCase();
          componentId = generateComponentId(el, componentName);
          currentElement = el;
          break;
        }
        el = el.parentElement;
      }

      if (!component) {
        alert(
          "⚠️ No se encontró un componente Angular válido (app-*) en la jerarquía."
        );
        return;
      }

      console.log(
        `[Angular Modifier] Componente seleccionado: ${componentName} (ID: ${componentId})`,
        component
      );

      showComponentEditor(component, componentName, componentId);
    },
    true
  );

  function generateComponentId(element, componentName) {
    let path = [];
    let current = element;
    while (current && current !== document.body) {
      let index = 0;
      let sibling = current;
      while ((sibling = sibling.previousElementSibling)) {
        index++;
      }
      path.unshift(index);
      current = current.parentElement;
    }
    return `${componentName}_${path.join("_")}`;
  }

  function navigateToParentComponent(currentEl) {
    let ng = window.ng;
    if (!ng) {
      alert(
        "⚠️ Angular DevTools no está disponible. Asegúrate de estar en un servidor de desarrollo."
      );
      return false;
    }

    if (!currentEl) {
      alert("⚠️ No hay ningún componente seleccionado actualmente.");
      return false;
    }

    let parentEl = currentEl.parentElement;
    let found = false;

    while (parentEl) {
      if (
        parentEl.tagName &&
        parentEl.tagName.toLowerCase().startsWith("app-") &&
        ng.getComponent(parentEl)
      ) {
        currentElement = parentEl;
        const component = ng.getComponent(parentEl);
        const componentName = parentEl.tagName.toLowerCase();
        const componentId = generateComponentId(parentEl, componentName);

        console.log(
          `[Angular Modifier] Navegando al componente padre: ${componentName} (ID: ${componentId})`,
          component
        );

        const existingModal = document.querySelector(".angular-modifier-modal");
        if (existingModal) {
          document.body.removeChild(existingModal);
        }

        showComponentEditor(component, componentName, componentId);
        found = true;
        break;
      }
      parentEl = parentEl.parentElement;
    }

    if (!found) {
      alert("⚠️ No se encontró un componente padre que comience con 'app-'.");
    }

    return found;
  }

  function showComponentEditor(component, componentName, componentId) {
    let modal = document.createElement("div");
    modal.className = "angular-modifier-modal";
    modal.style.position = "fixed";
    modal.style.top = "50%";
    modal.style.left = "50%";
    modal.style.transform = "translate(-50%, -50%)";
    modal.style.background = "white";
    modal.style.padding = "20px";
    modal.style.boxShadow = "0px 0px 10px rgba(0,0,0,0.2)";
    modal.style.zIndex = "10000";
    modal.style.borderRadius = "8px";
    modal.style.width = "400px";
    modal.style.maxHeight = "500px";
    modal.style.overflowY = "auto";

    let title = document.createElement("h3");
    title.innerText = componentName;
    title.style.marginTop = "0";
    modal.appendChild(title);

    let form = document.createElement("form");

    let formGroups = {};
    let editableProps = {};

    Object.keys(component).forEach((prop) => {
      if (IGNORED_PROPERTIES.includes(prop)) return;

      let value = component[prop];

      if (typeof value === "function") return;

      if (
        value &&
        typeof value === "object" &&
        value.constructor.name === "FormGroup"
      ) {
        formGroups[prop] = value;
        appendFormGroupFields(form, value, prop);
        return;
      }

      if (value !== null && typeof value === "object") return;

      let input = appendEditableField(form, component, prop, value);
      if (input) {
        editableProps[prop] = {
          type: typeof value,
          input: input,
        };
      }
    });

    modal.appendChild(form);

    let parentComponentButton = document.createElement("button");
    parentComponentButton.innerText = "Ir al Componente Padre";
    parentComponentButton.style.marginTop = "15px";
    parentComponentButton.style.width = "100%";
    parentComponentButton.style.padding = "8px";
    parentComponentButton.style.background = "#ffc107";
    parentComponentButton.style.color = "black";
    parentComponentButton.style.border = "none";
    parentComponentButton.style.borderRadius = "5px";
    parentComponentButton.style.cursor = "pointer";
    parentComponentButton.style.fontWeight = "bold";

    parentComponentButton.addEventListener("click", (e) => {
      e.preventDefault();
      navigateToParentComponent(currentElement);
    });

    modal.appendChild(parentComponentButton);

    let stateManagementDiv = document.createElement("div");
    stateManagementDiv.style.marginTop = "15px";
    stateManagementDiv.style.borderTop = "1px solid #eee";
    stateManagementDiv.style.paddingTop = "10px";

    let saveStateButton = document.createElement("button");
    saveStateButton.innerText = "Guardar Estado Actual";
    saveStateButton.style.padding = "5px 10px";
    saveStateButton.style.marginRight = "10px";
    saveStateButton.style.background = "#28a745";
    saveStateButton.style.color = "white";
    saveStateButton.style.border = "none";
    saveStateButton.style.borderRadius = "5px";
    saveStateButton.style.cursor = "pointer";
    saveStateButton.addEventListener("click", (e) => {
      e.preventDefault();
      saveCurrentState(component, componentId, formGroups, editableProps);
    });
    stateManagementDiv.appendChild(saveStateButton);

    let restoreStateButton = document.createElement("button");
    restoreStateButton.innerText = "Restaurar Estado";
    restoreStateButton.style.padding = "5px 10px";
    restoreStateButton.style.background = "#007bff";
    restoreStateButton.style.color = "white";
    restoreStateButton.style.border = "none";
    restoreStateButton.style.borderRadius = "5px";
    restoreStateButton.style.cursor = "pointer";

    if (!savedStates[componentId]) {
      restoreStateButton.disabled = true;
      restoreStateButton.style.opacity = "0.5";
      restoreStateButton.style.cursor = "not-allowed";
    }

    restoreStateButton.addEventListener("click", (e) => {
      e.preventDefault();
      restoreSavedState(component, componentId, formGroups, editableProps);
    });
    stateManagementDiv.appendChild(restoreStateButton);

    modal.appendChild(stateManagementDiv);

    let fileLabel = document.createElement("label");
    fileLabel.innerText = "Cargar JSON:";
    fileLabel.style.display = "block";
    fileLabel.style.marginTop = "15px";
    modal.appendChild(fileLabel);

    let fileInput = document.createElement("input");
    fileInput.type = "file";
    fileInput.accept = "application/json";
    fileInput.style.marginTop = "5px";
    fileInput.style.width = "100%";
    fileInput.addEventListener("change", (event) =>
      handleFileUpload(event, formGroups)
    );
    modal.appendChild(fileInput);

    let exportButton = document.createElement("button");
    exportButton.innerText = "Exportar a JSON";
    exportButton.style.marginTop = "10px";
    exportButton.style.width = "100%";
    exportButton.style.padding = "5px";
    exportButton.style.background = "#17a2b8";
    exportButton.style.color = "white";
    exportButton.style.border = "none";
    exportButton.style.borderRadius = "5px";
    exportButton.style.cursor = "pointer";
    exportButton.addEventListener("click", (e) => {
      e.preventDefault();
      exportToJson(component, formGroups);
    });
    modal.appendChild(exportButton);

    let closeButton = document.createElement("button");
    closeButton.innerText = "Cerrar";
    closeButton.style.marginTop = "10px";
    closeButton.style.width = "100%";
    closeButton.style.padding = "5px";
    closeButton.style.background = "#d9534f";
    closeButton.style.color = "white";
    closeButton.style.border = "none";
    closeButton.style.borderRadius = "5px";
    closeButton.style.cursor = "pointer";

    closeButton.addEventListener("click", () => {
      document.body.removeChild(modal);
    });

    modal.appendChild(closeButton);
    document.body.appendChild(modal);
  }

  function appendEditableField(form, component, prop, value) {
    let label = document.createElement("label");
    label.innerText = prop;
    label.style.display = "block";
    label.style.marginTop = "5px";

    let input = document.createElement("input");
    input.style.width = "100%";
    input.style.marginTop = "2px";
    input.dataset.propName = prop;

    if (typeof value === "boolean") {
      input.type = "checkbox";
      input.checked = value;
    } else if (typeof value === "number") {
      input.type = "number";
      input.value = value;
    } else if (typeof value === "string") {
      input.type = "text";
      input.value = value;
    } else {
      return null;
    }

    input.addEventListener("change", () => {
      try {
        if (input.type === "checkbox") {
          component[prop] = input.checked;
        } else if (input.type === "number") {
          component[prop] = parseFloat(input.value);
        } else {
          component[prop] = input.value;
        }
        if (typeof ng.applyChanges === "function") {
          ng.applyChanges(component);
          console.log(`[Angular Modifier] Se aplicaron cambios en ${prop}`);
        }
      } catch (err) {
        alert(`⚠️ Error al actualizar '${prop}': ${err.message}`);
      }
    });

    form.appendChild(label);
    form.appendChild(input);
    return input;
  }

  function appendFormGroupFields(form, formGroup, formGroupName) {
    let formGroupTitle = document.createElement("h4");
    formGroupTitle.innerText = `Formulario: ${formGroupName}`;
    formGroupTitle.style.marginTop = "10px";
    formGroupTitle.style.color = "#007bff";
    form.appendChild(formGroupTitle);

    if (formGroup.controls) {
      Object.keys(formGroup.controls).forEach((controlName) => {
        try {
          const control = formGroup.controls[controlName];
          const currentValue = control.value;

          let controlLabel = document.createElement("label");
          controlLabel.innerText = controlName;
          controlLabel.style.display = "block";
          controlLabel.style.marginTop = "5px";
          controlLabel.style.marginLeft = "10px";

          let controlInput = document.createElement("input");
          controlInput.style.width = "95%";
          controlInput.style.marginTop = "2px";
          controlInput.style.marginLeft = "10px";
          controlInput.dataset.formGroup = formGroupName;
          controlInput.dataset.controlName = controlName;

          if (typeof currentValue === "boolean") {
            controlInput.type = "checkbox";
            controlInput.checked = currentValue;
          } else if (typeof currentValue === "number") {
            controlInput.type = "number";
            controlInput.value = currentValue;
          } else {
            controlInput.type = "text";
            controlInput.value =
              currentValue !== null && currentValue !== undefined
                ? currentValue
                : "";
          }

          controlInput.addEventListener("change", () => {
            try {
              let newValue;
              if (controlInput.type === "checkbox") {
                newValue = controlInput.checked;
              } else if (controlInput.type === "number") {
                newValue = parseFloat(controlInput.value);
              } else {
                newValue = controlInput.value;
              }

              control.setValue(newValue);
              console.log(
                `[Angular Modifier] Actualizado control '${controlName}' en FormGroup '${formGroupName}'`
              );
            } catch (err) {
              alert(
                `⚠️ Error al actualizar control '${controlName}': ${err.message}`
              );
            }
          });

          form.appendChild(controlLabel);
          form.appendChild(controlInput);
        } catch (err) {
          console.warn(
            `[Angular Modifier] Error al mostrar control '${controlName}':`,
            err
          );
        }
      });
    }
  }

  function handleFileUpload(event, formGroups) {
    let file = event.target.files[0];
    if (!file) return;

    let reader = new FileReader();
    reader.onload = function (event) {
      try {
        let jsonData = JSON.parse(event.target.result);
        applyJsonToForm(jsonData, formGroups);
      } catch (err) {
        alert("⚠️ Error al cargar JSON: " + err.message);
      }
    };
    reader.readAsText(file);
  }

  function applyJsonToForm(jsonData, formGroups) {
    if (jsonData.properties) {
      Object.keys(jsonData.properties).forEach((prop) => {
        if (IGNORED_PROPERTIES.includes(prop)) return;

        try {
          const inputElement = document.querySelector(
            `input[data-prop-name="${prop}"]`
          );
          if (inputElement) {
            if (inputElement.type === "checkbox") {
              inputElement.checked = jsonData.properties[prop];
            } else {
              inputElement.value = jsonData.properties[prop];
            }
            inputElement.dispatchEvent(new Event("change"));
          }
        } catch (err) {
          console.warn(
            `[Angular Modifier] Error al aplicar propiedad '${prop}':`,
            err
          );
        }
      });
    }

    if (jsonData.formGroups) {
      Object.keys(jsonData.formGroups).forEach((groupName) => {
        if (formGroups[groupName]) {
          let formGroup = formGroups[groupName];
          const groupData = jsonData.formGroups[groupName];

          Object.keys(groupData).forEach((controlName) => {
            if (formGroup.controls[controlName]) {
              try {
                formGroup.controls[controlName].setValue(
                  groupData[controlName]
                );

                const controlInput = document.querySelector(
                  `input[data-form-group="${groupName}"][data-control-name="${controlName}"]`
                );
                if (controlInput) {
                  if (controlInput.type === "checkbox") {
                    controlInput.checked = groupData[controlName];
                  } else {
                    controlInput.value = groupData[controlName];
                  }
                }

                console.log(
                  `[Angular Modifier] Campo '${controlName}' de '${groupName}' actualizado`
                );
              } catch (err) {
                console.warn(
                  `[Angular Modifier] Error al actualizar control '${controlName}':`,
                  err
                );
              }
            }
          });
        }
      });
    }
  }

  function exportToJson(component, formGroups) {
    let exportData = {
      properties: {},
      formGroups: {},
    };

    Object.keys(component).forEach((prop) => {
      if (IGNORED_PROPERTIES.includes(prop)) return;

      let value = component[prop];
      if (
        typeof value !== "function" &&
        value !== null &&
        typeof value !== "object"
      ) {
        exportData.properties[prop] = value;
      }
    });

    Object.keys(formGroups).forEach((groupName) => {
      const formGroup = formGroups[groupName];
      exportData.formGroups[groupName] = {};

      if (formGroup.controls) {
        Object.keys(formGroup.controls).forEach((controlName) => {
          try {
            exportData.formGroups[groupName][controlName] =
              formGroup.controls[controlName].value;
          } catch (err) {
            console.warn(
              `[Angular Modifier] Error al exportar control '${controlName}':`,
              err
            );
          }
        });
      }
    });

    const jsonString = JSON.stringify(exportData, null, 2);
    const blob = new Blob([jsonString], { type: "application/json" });
    const url = URL.createObjectURL(blob);

    const a = document.createElement("a");
    a.href = url;
    a.download = `angular-component-${Date.now()}.json`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  function saveCurrentState(component, componentId, formGroups, editableProps) {
    let state = {
      properties: {},
      formGroups: {},
    };

    Object.keys(editableProps).forEach((prop) => {
      if (IGNORED_PROPERTIES.includes(prop)) return;

      const input = editableProps[prop].input;
      if (input.type === "checkbox") {
        state.properties[prop] = input.checked;
      } else if (input.type === "number") {
        state.properties[prop] = parseFloat(input.value);
      } else {
        state.properties[prop] = input.value;
      }
    });

    Object.keys(formGroups).forEach((groupName) => {
      const formGroup = formGroups[groupName];
      state.formGroups[groupName] = {};

      if (formGroup.controls) {
        Object.keys(formGroup.controls).forEach((controlName) => {
          try {
            state.formGroups[groupName][controlName] =
              formGroup.controls[controlName].value;
          } catch (err) {
            console.warn(
              `[Angular Modifier] Error al guardar control '${controlName}':`,
              err
            );
          }
        });
      }
    });

    savedStates[componentId] = state;

    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(savedStates));
      alert(`✅ Estado guardado correctamente para ${componentId}`);
    } catch (err) {
      console.error("[Angular Modifier] Error al guardar estado:", err);
      alert("⚠️ Error al guardar estado: " + err.message);
    }
  }

  function restoreSavedState(
    component,
    componentId,
    formGroups,
    editableProps
  ) {
    const savedState = savedStates[componentId];
    if (!savedState) {
      alert("⚠️ No hay estado guardado para este componente");
      return;
    }

    if (savedState.properties) {
      Object.keys(savedState.properties).forEach((prop) => {
        if (IGNORED_PROPERTIES.includes(prop)) return;

        if (editableProps[prop]) {
          const input = editableProps[prop].input;
          const value = savedState.properties[prop];

          if (input.type === "checkbox") {
            input.checked = value;
          } else {
            input.value = value;
          }

          try {
            component[prop] = value;
          } catch (err) {
            console.warn(
              `[Angular Modifier] Error al restaurar propiedad '${prop}':`,
              err
            );
          }
        }
      });
    }

    if (savedState.formGroups) {
      Object.keys(savedState.formGroups).forEach((groupName) => {
        if (formGroups[groupName]) {
          const formGroup = formGroups[groupName];
          const groupData = savedState.formGroups[groupName];

          Object.keys(groupData).forEach((controlName) => {
            if (formGroup.controls[controlName]) {
              try {
                formGroup.controls[controlName].setValue(
                  groupData[controlName]
                );

                const controlInput = document.querySelector(
                  `input[data-form-group="${groupName}"][data-control-name="${controlName}"]`
                );
                if (controlInput) {
                  if (controlInput.type === "checkbox") {
                    controlInput.checked = groupData[controlName];
                  } else {
                    controlInput.value = groupData[controlName];
                  }
                }
              } catch (err) {
                console.warn(
                  `[Angular Modifier] Error al restaurar control '${controlName}':`,
                  err
                );
              }
            }
          });
        }
      });
    }

    if (typeof ng.applyChanges === "function") {
      ng.applyChanges(component);
      console.log(`[Angular Modifier] Se restauró el estado del componente`);
    }

    alert(`✅ Estado restaurado correctamente para ${componentId}`);
  }
})();