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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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}`);
  }
})();