您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Módulo para gestionar listas de usuario en WME Place Normalizer. No funciona por sí solo.
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyforks.org/scripts/548862/1657865/WME%20PLN%20Module%20-%20Lists%20Handler.js
// ==UserScript== // @name WME PLN Module - Lists Handler // @version 9.0.0 // @description Módulo para gestionar listas de usuario en WME Place Normalizer. No funciona por sí solo. // @author mincho77 // @license MIT // @grant none // ==/UserScript== // Función para crear el gestor de palabras excluidas y lugares excluidos function createSpecialItemsManager(parentContainer) { // Evitar crear múltiples instancias const mainSection = document.createElement("div"); mainSection.id = "specialItemsManagerSection"; mainSection.style.marginTop = "20px"; mainSection.style.borderTop = "1px solid #ccc"; mainSection.style.paddingTop = "10px"; // --- Dropdown para seleccionar el tipo de gestión --- const typeSelectorWrapper = document.createElement("div"); typeSelectorWrapper.style.marginBottom = "15px"; typeSelectorWrapper.style.textAlign = "center"; const typeSelectorLabel = document.createElement("label"); typeSelectorLabel.textContent = "Gestionar:"; typeSelectorLabel.style.marginRight = "10px"; typeSelectorLabel.style.fontWeight = "bold"; typeSelectorWrapper.appendChild(typeSelectorLabel); const typeSelector = document.createElement("select"); typeSelector.id = "specialTypeSelector"; typeSelector.style.padding = "5px"; typeSelector.style.borderRadius = "4px"; typeSelector.style.fontSize = "13px"; const optionWords = document.createElement("option"); optionWords.value = "words"; optionWords.textContent = "Palabras Especiales"; typeSelector.appendChild(optionWords); const optionPlaces = document.createElement("option"); optionPlaces.value = "places"; optionPlaces.textContent = "Lugares Excluidos"; typeSelector.appendChild(optionPlaces); typeSelectorWrapper.appendChild(typeSelector); mainSection.appendChild(typeSelectorWrapper); // Añadir a mainSection // --- Contenedores para las dos vistas --- const wordsView = document.createElement("div"); wordsView.id = "specialWordsView"; wordsView.style.display = "block"; // Visible por defecto const placesView = document.createElement("div"); placesView.id = "excludedPlacesView"; placesView.style.display = "none"; // Oculto por defecto mainSection.appendChild(wordsView); // Añadir a mainSection mainSection.appendChild(placesView); // Añadir a mainSection // Título de la sección const wordsTitle = document.createElement("h4"); wordsTitle.textContent = "Gestión de Palabras Especiales"; wordsTitle.style.fontSize = "15px"; wordsTitle.style.marginBottom = "10px"; wordsView.appendChild(wordsTitle); // AÑADIDO A wordsView // Contenedor para los controles de añadir palabra const addWordsControlsContainer = document.createElement("div"); // Renombrado para claridad addWordsControlsContainer.style.display = "flex"; addWordsControlsContainer.style.gap = "8px"; addWordsControlsContainer.style.marginBottom = "8px"; addWordsControlsContainer.style.alignItems = "center"; // Alinear verticalmente // Input para añadir nueva palabra o frase const wordsInput = document.createElement("input"); // Renombrado para claridad wordsInput.type = "text"; wordsInput.placeholder = "Nueva palabra o frase"; wordsInput.style.flexGrow = "1"; wordsInput.style.padding = "6px"; wordsInput.style.border = "1px solid #ccc"; wordsInput.style.borderRadius = "3px"; addWordsControlsContainer.appendChild(wordsInput); // AÑADIDO A addWordsControlsContainer // Botón para añadir la palabra const addWordBtn = document.createElement("button"); // Renombrado para claridad addWordBtn.textContent = "Añadir"; addWordBtn.style.padding = "6px 10px"; addWordBtn.style.cursor = "pointer"; // Añadir tooltip al botón addWordBtn.addEventListener("click", function () { const newWord = wordsInput.value.trim(); // Usa wordsInput const validation = isValidExcludedWord(newWord); if (!validation.valid) { plnToast(validation.msg, 3000); return; } excludedWords.add(newWord); const firstCharNew = newWord.charAt(0).toLowerCase(); if (!excludedWordsMap.has(firstCharNew)) { excludedWordsMap.set(firstCharNew, new Set()); } excludedWordsMap.get(firstCharNew).add(newWord); wordsInput.value = ""; // Limpia wordsInput renderExcludedWordsList(document.getElementById("excludedWordsList")); saveExcludedWordsToLocalStorage(); }); addWordsControlsContainer.appendChild(addWordBtn); // AÑADIDO A addWordsControlsContainer wordsView.appendChild(addWordsControlsContainer); // AÑADIDO A wordsView // Contenedor para los botones de acción (Exportar/Limpiar para Palabras) const wordsActionButtonsContainer = document.createElement("div"); // Renombrado wordsActionButtonsContainer.style.display = "flex"; wordsActionButtonsContainer.style.gap = "8px"; wordsActionButtonsContainer.style.marginBottom = "10px"; const exportWordsBtn = document.createElement("button"); // Renombrado exportWordsBtn.textContent = "Exportar"; exportWordsBtn.title = "Exportar Lista a XML"; exportWordsBtn.style.padding = "6px 10px"; exportWordsBtn.style.cursor = "pointer"; exportWordsBtn.addEventListener("click", () => plnUiExportDataToXml("words")); // UI adapter wordsActionButtonsContainer.appendChild(exportWordsBtn); // AÑADIDO A wordsActionButtonsContainer const clearWordsBtn = document.createElement("button"); // Renombrado clearWordsBtn.textContent = "Limpiar"; clearWordsBtn.title = "Limpiar toda la lista"; clearWordsBtn.style.padding = "6px 10px"; clearWordsBtn.style.cursor = "pointer"; clearWordsBtn.addEventListener("click", function () { if (confirm("¿Estás seguro de que deseas eliminar TODAS las palabras de la lista?")) { excludedWords.clear(); excludedWordsMap.clear(); renderExcludedWordsList(document.getElementById("excludedWordsList")); saveExcludedWordsToLocalStorage(); } }); wordsActionButtonsContainer.appendChild(clearWordsBtn); // AÑADIDO A wordsActionButtonsContainer wordsView.appendChild(wordsActionButtonsContainer); // AÑADIDO A wordsView // Contenedor para la lista de palabras excluidas (buscador y UL) const wordsSearchInput = document.createElement("input"); // Renombrado wordsSearchInput.type = "text"; wordsSearchInput.placeholder = "Buscar en especiales..."; wordsSearchInput.style.display = "block"; wordsSearchInput.style.width = "calc(100% - 14px)"; wordsSearchInput.style.padding = "6px"; wordsSearchInput.style.border = "1px solid #ccc"; wordsSearchInput.style.borderRadius = "3px"; wordsSearchInput.style.marginBottom = "5px"; wordsSearchInput.addEventListener("input", () => { renderExcludedWordsList(document.getElementById("excludedWordsList"), wordsSearchInput.value.trim()); // Usa wordsSearchInput }); wordsView.appendChild(wordsSearchInput); // AÑADIDO A wordsView // UL para palabras excluidas const wordsListUL = document.createElement("ul"); // Renombrado wordsListUL.id = "excludedWordsList"; // Mantiene el ID original para compatibilidad con renderExcludedWordsList wordsListUL.style.maxHeight = "150px"; wordsListUL.style.overflowY = "auto"; wordsListUL.style.border = "1px solid #ddd"; wordsListUL.style.padding = "5px"; wordsListUL.style.margin = "0"; wordsListUL.style.background = "#fff"; wordsListUL.style.listStyle = "none"; wordsView.appendChild(wordsListUL); // AÑADIDO A wordsView // Drop Area para XML de palabras const wordsDropArea = document.createElement("div"); // Renombrado wordsDropArea.textContent = "Arrastra aquí el archivo XML de palabras especiales"; wordsDropArea.style.border = "2px dashed #ccc"; wordsDropArea.style.borderRadius = "4px"; wordsDropArea.style.padding = "15px"; wordsDropArea.style.marginTop = "10px"; wordsDropArea.style.textAlign = "center"; wordsDropArea.style.background = "#f9f9f9"; wordsDropArea.style.color = "#555"; wordsDropArea.addEventListener("dragover", (e) => { e.preventDefault(); wordsDropArea.style.background = "#e9e9e9"; wordsDropArea.style.borderColor = "#aaa"; }); wordsDropArea.addEventListener("dragleave", () => { wordsDropArea.style.background = "#f9f9f9"; wordsDropArea.style.borderColor = "#ccc"; }); wordsDropArea.addEventListener("drop", (e) => { e.preventDefault(); wordsDropArea.style.background = "#f9f9f9"; plnUiHandleXmlFileDrop(e.dataTransfer.files[0], "words"); // UI adapter }); wordsView.appendChild(wordsDropArea); // AÑADIDO A wordsView // Título de la sección const placesTitle = document.createElement("h4"); placesTitle.textContent = "Gestión de Lugares Excluidos"; placesTitle.style.fontSize = "15px"; placesTitle.style.marginBottom = "10px"; placesView.appendChild(placesTitle); // Controles de búsqueda y lista de lugares const placesSearchInput = document.createElement("input"); placesSearchInput.type = "text"; placesSearchInput.placeholder = "Buscar lugar excluido..."; placesSearchInput.style.display = "block"; placesSearchInput.style.width = "calc(100% - 14px)"; placesSearchInput.style.padding = "6px"; placesSearchInput.style.border = "1px solid #ccc"; placesSearchInput.style.borderRadius = "3px"; placesSearchInput.style.marginBottom = "5px"; placesSearchInput.addEventListener("input", () => { renderExcludedPlacesList(document.getElementById("excludedPlacesListUL"), placesSearchInput.value.trim()); }); placesView.appendChild(placesSearchInput); const placesListUL = document.createElement("ul"); placesListUL.id = "excludedPlacesListUL"; // Nuevo ID para la lista de Places placesListUL.style.maxHeight = "200px"; // Un poco más grande placesListUL.style.overflowY = "auto"; placesListUL.style.border = "1px solid #ddd"; placesListUL.style.padding = "5px"; placesListUL.style.margin = "0"; placesListUL.style.background = "#fff"; placesListUL.style.listStyle = "none"; placesView.appendChild(placesListUL); // Botones de acción para Lugares Excluidos const placesActionButtonsContainer = document.createElement("div"); placesActionButtonsContainer.style.display = "flex"; placesActionButtonsContainer.style.gap = "8px"; placesActionButtonsContainer.style.marginTop = "10px"; const exportPlacesBtn = document.createElement("button"); exportPlacesBtn.textContent = "Exportar"; exportPlacesBtn.title = "Exportar Lugares Excluidos a XML"; exportPlacesBtn.style.padding = "6px 10px"; exportPlacesBtn.style.cursor = "pointer"; exportPlacesBtn.addEventListener("click", () => plnUiExportDataToXml("places")); // UI adapter placesActionButtonsContainer.appendChild(exportPlacesBtn); const clearPlacesBtn = document.createElement("button"); clearPlacesBtn.textContent = "Limpiar"; clearPlacesBtn.title = "Limpiar lista de lugares excluidos"; clearPlacesBtn.style.padding = "6px 10px"; clearPlacesBtn.style.cursor = "pointer"; clearPlacesBtn.addEventListener("click", () => { if (confirm("¿Estás seguro de que deseas eliminar TODOS los lugares de la lista?")) { excludedPlaces.clear(); renderExcludedPlacesList(document.getElementById("excludedPlacesListUL")); saveExcludedPlacesToLocalStorage(); } }); placesActionButtonsContainer.appendChild(clearPlacesBtn); placesView.appendChild(placesActionButtonsContainer); // Drop Area para XML de Lugares Excluidos const placesDropArea = document.createElement("div"); placesDropArea.textContent = "Arrastra aquí el archivo XML de lugares excluidos"; placesDropArea.style.border = "2px dashed #ccc"; placesDropArea.style.borderRadius = "4px"; placesDropArea.style.padding = "15px"; placesDropArea.style.marginTop = "10px"; placesDropArea.style.textAlign = "center"; placesDropArea.style.background = "#f9f9f9"; placesDropArea.style.color = "#555"; placesDropArea.addEventListener("dragover", (e) => { e.preventDefault(); placesDropArea.style.background = "#e9e9e9"; placesDropArea.style.borderColor = "#aaa"; }); placesDropArea.addEventListener("dragleave", () => { placesDropArea.style.background = "#f9f9f9"; placesDropArea.style.borderColor = "#ccc"; }); placesDropArea.addEventListener("drop", (e) => { e.preventDefault(); placesDropArea.style.background = "#f9f9f9"; plnUiHandleXmlFileDrop(e.dataTransfer.files[0], "places"); // UI adapter }); placesView.appendChild(placesDropArea); // --- Lógica de alternancia del selector --- typeSelector.addEventListener("change", () => { if (typeSelector.value === "words") { wordsView.style.display = "block"; placesView.style.display = "none"; renderExcludedWordsList(document.getElementById("excludedWordsList"), wordsSearchInput.value.trim()); // Renderiza lista de palabras } else { wordsView.style.display = "none"; placesView.style.display = "block"; renderExcludedPlacesList(document.getElementById("excludedPlacesListUL"), placesSearchInput.value.trim()); // Renderiza lista de lugares } }); // --- Renderizado inicial de las listas al cargar --- renderExcludedWordsList(wordsListUL, ""); renderExcludedPlacesList(placesListUL, ""); parentContainer.appendChild(mainSection); }// createSpecialItemsManager // === Diccionario === // Función para crear el gestor de diccionario personalizado function createDictionaryManager(parentContainer) { // Evitar crear múltiples instancias const section = document.createElement("div"); section.id = "dictionaryManagerSection"; section.style.marginTop = "20px"; section.style.borderTop = "1px solid #ccc"; section.style.paddingTop = "10px"; // Título de la sección const title = document.createElement("h4"); title.textContent = "Gestión del Diccionario"; title.style.fontSize = "15px"; title.style.marginBottom = "10px"; section.appendChild(title); // Contenedor para los controles de añadir palabra const addControlsContainer = document.createElement("div"); addControlsContainer.style.display = "flex"; addControlsContainer.style.gap = "8px"; addControlsContainer.style.marginBottom = "8px"; addControlsContainer.style.alignItems = "center"; // Alinear verticalmente // Input para añadir nueva palabra const input = document.createElement("input"); input.type = "text"; input.placeholder = "Nueva palabra"; input.style.flexGrow = "1"; input.style.padding = "6px"; // Mejor padding input.style.border = "1px solid #ccc"; input.style.borderRadius = "3px"; addControlsContainer.appendChild(input); // Botón para añadir la palabra const addBtn = document.createElement("button"); addBtn.textContent = "Añadir"; addBtn.style.padding = "6px 10px"; // Mejor padding addBtn.style.cursor = "pointer"; addBtn.addEventListener("click", function () { const newWord = input.value.trim(); const validation = isValidExcludedWord(newWord); if (!validation.valid) { plnToast(validation.msg,3000); return; } if (newWord) { const lowerNewWord = newWord.toLowerCase(); const alreadyExists = Array.from(window.dictionaryWords).some(w => w.toLowerCase() === lowerNewWord); if (commonWords.includes(lowerNewWord)) { plnToast("La palabra es muy común y no debe agregarse a la lista.", 3000); return; } if (alreadyExists) { plnToast("La palabra ya está en la lista.", 3000); return; } window.dictionaryWords.add(lowerNewWord); input.value = ""; renderDictionaryList(document.getElementById("dictionaryWordsList")); } }); // Añadir tooltip al botón addControlsContainer.appendChild(addBtn); section.appendChild(addControlsContainer); // Contenedor para los botones de acción const actionButtonsContainer = document.createElement("div"); actionButtonsContainer.style.display = "flex"; actionButtonsContainer.style.gap = "8px"; actionButtonsContainer.style.marginBottom = "10px"; // Más espacio // Botón para importar desde XML const exportBtn = document.createElement("button"); exportBtn.textContent = "Exportar"; // Más corto exportBtn.title = "Exportar Diccionario a XML"; exportBtn.style.padding = "6px 10px"; exportBtn.style.cursor = "pointer"; exportBtn.addEventListener("click", exportDictionaryWordsList); actionButtonsContainer.appendChild(exportBtn); // Botón para importar desde XML const clearBtn = document.createElement("button"); clearBtn.textContent = "Limpiar"; // Más corto clearBtn.title = "Limpiar toda la lista"; clearBtn.style.padding = "6px 10px"; clearBtn.style.cursor = "pointer"; clearBtn.addEventListener("click", function () { if (confirm("¿Estás seguro de que deseas eliminar TODAS las palabras del diccionario?")) { window.dictionaryWords.clear(); renderDictionaryList(document.getElementById("dictionaryWordsList")); // Pasar el elemento UL } }); actionButtonsContainer.appendChild(clearBtn); section.appendChild(actionButtonsContainer); // Diccionario: búsqueda const search = document.createElement("input"); search.type = "text"; search.placeholder = "Buscar en diccionario..."; search.style.display = "block"; search.style.width = "calc(100% - 14px)"; search.style.padding = "6px"; search.style.border = "1px solid #ccc"; search.style.borderRadius = "3px"; search.style.marginTop = "5px"; // On search input, render filtered list search.addEventListener("input", () => { renderDictionaryList(document.getElementById("dictionaryWordsList"),search.value.trim()); }); section.appendChild(search); // Lista UL para mostrar palabras del diccionario const listContainerElement = document.createElement("ul"); listContainerElement.id = "dictionaryWordsList"; listContainerElement.style.maxHeight = "150px"; listContainerElement.style.overflowY = "auto"; listContainerElement.style.border = "1px solid #ddd"; listContainerElement.style.padding = "5px"; listContainerElement.style.margin = "0"; listContainerElement.style.background = "#fff"; listContainerElement.style.listStyle = "none"; section.appendChild(listContainerElement); // Renderizar la lista de palabras del diccionario const dropArea = document.createElement("div"); dropArea.textContent = "Arrastra aquí el archivo XML del diccionario"; dropArea.style.border = "2px dashed #ccc"; dropArea.style.borderRadius = "4px"; dropArea.style.padding = "15px"; dropArea.style.marginTop = "10px"; dropArea.style.textAlign = "center"; dropArea.style.background = "#f9f9f9"; dropArea.style.color = "#555"; // Añadir eventos de arrastrar y soltar dropArea.addEventListener("dragover", (e) => { e.preventDefault(); dropArea.style.background = "#e9e9e9"; dropArea.style.borderColor = "#aaa"; }); // Evento para cuando el ratón sale del área de arrastre dropArea.addEventListener("dragleave", () => { dropArea.style.background = "#f9f9f9"; dropArea.style.borderColor = "#ccc"; }); // Evento para cuando se suelta el archivo dropArea.addEventListener("drop", (e) => { e.preventDefault(); dropArea.style.background = "#f9f9f9"; dropArea.style.borderColor = "#ccc"; const file = e.dataTransfer.files[0]; if (file && (file.type === "text/xml" || file.name.endsWith(".xml"))) { const reader = new FileReader(); reader.onload = function (evt) { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(evt.target.result, "application/xml"); const parserError = xmlDoc.querySelector("parsererror"); if (parserError) { plnLog('error',"[WME PLN] Error parseando XML:", parserError.textContent); plnToast("Error al parsear el archivo XML del diccionario.", 3000); return; } const xmlWords = xmlDoc.querySelectorAll("word"); let newWordsAddedCount = 0; for (let i = 0; i < xmlWords.length; i++) { const val = xmlWords[i].textContent.trim(); if (val && !window.dictionaryWords.has(val)) { window.dictionaryWords.add(val); newWordsAddedCount++; } } if (newWordsAddedCount > 0) plnLog('swap', `[WME PLN] ${newWordsAddedCount} nuevas palabras añadidas desde XML.`); // Renderizar la lista en el panel renderDictionaryList(listContainerElement); } catch (err) { plnToast("Error procesando el diccionario XML.", 3000); } }; reader.readAsText(file); } else { plnToast("Por favor, arrastra un archivo XML válido.", 3000); } }); section.appendChild(dropArea); parentContainer.appendChild(section); renderDictionaryList(listContainerElement); }// createDictionaryManager // Crea el gestor de reemplazos function createReplacementsManager(parentContainer) { loadSwapWordsFromStorage(); parentContainer.innerHTML = ''; // Limpiar por si acaso function openSwapWordEditor(item, index) { // Crear el fondo del modal const modalOverlay = document.createElement("div"); modalOverlay.style.position = "fixed"; modalOverlay.style.top = "0"; modalOverlay.style.left = "0"; modalOverlay.style.width = "100%"; modalOverlay.style.height = "100%"; modalOverlay.style.background = "rgba(0,0,0,0.5)"; modalOverlay.style.zIndex = "20000"; modalOverlay.style.display = "flex"; modalOverlay.style.justifyContent = "center"; modalOverlay.style.alignItems = "center"; // Crear el contenido del modal const modalContent = document.createElement("div"); modalContent.style.background = "#fff"; modalContent.style.padding = "25px"; modalContent.style.borderRadius = "8px"; modalContent.style.boxShadow = "0 5px 15px rgba(0,0,0,0.3)"; modalContent.style.width = "400px"; modalContent.style.fontFamily = "sans-serif"; // Título del modal const title = document.createElement("h4"); title.textContent = "Editar Palabra Swap"; title.style.marginTop = "0"; title.style.marginBottom = "20px"; title.style.textAlign = "center"; modalContent.appendChild(title); // Input para la palabra const wordLabel = document.createElement("label"); wordLabel.textContent = "Palabra o Frase:"; wordLabel.style.display = "block"; wordLabel.style.marginBottom = "5px"; modalContent.appendChild(wordLabel); // Input de texto const wordInput = document.createElement("input"); wordInput.type = "text"; wordInput.value = item.word; wordInput.style.width = "calc(100% - 12px)"; wordInput.style.padding = "8px"; wordInput.style.marginBottom = "15px"; wordInput.setAttribute('spellcheck', 'false'); modalContent.appendChild(wordInput); // Radio buttons para la dirección const directionFieldset = document.createElement("fieldset"); directionFieldset.style.border = "1px solid #ccc"; directionFieldset.style.borderRadius = "5px"; directionFieldset.style.padding = "10px"; const legend = document.createElement("legend"); legend.textContent = "Mover a:"; directionFieldset.appendChild(legend); // Crear radio buttons ['start', 'end'].forEach(dir => { const container = document.createElement("div"); container.style.marginBottom = "5px"; const radio = document.createElement("input"); radio.type = "radio"; radio.name = "editSwapDirection"; radio.value = dir; radio.id = `editSwap_${dir}`; if (item.direction === dir) radio.checked = true; // Asociar label al radio const label = document.createElement("label"); label.htmlFor = `editSwap_${dir}`; label.textContent = ` ${dir === 'start' ? 'Al Inicio (Start ←A)' : 'Al Final (A→End)'}`; label.style.cursor = "pointer"; // Añadir al contenedor container.appendChild(radio); container.appendChild(label); directionFieldset.appendChild(container); }); modalContent.appendChild(directionFieldset); // Contenedor para los botones de acción const buttonContainer = document.createElement("div"); buttonContainer.style.display = "flex"; buttonContainer.style.justifyContent = "flex-end"; buttonContainer.style.gap = "10px"; buttonContainer.style.marginTop = "20px"; // Botón Cancelar const cancelBtn = document.createElement("button"); cancelBtn.textContent = "Cancelar"; cancelBtn.style.padding = "8px 15px"; cancelBtn.addEventListener("click", () => modalOverlay.remove()); buttonContainer.appendChild(cancelBtn); // Botón Guardar const saveBtn = document.createElement("button"); saveBtn.textContent = "Guardar Cambios"; saveBtn.style.padding = "8px 15px"; saveBtn.style.background = "#007bff"; saveBtn.style.color = "white"; saveBtn.style.border = "none"; saveBtn.style.borderRadius = "4px"; saveBtn.addEventListener("click", () => { const newWord = wordInput.value.trim(); const newDirection = document.querySelector('input[name="editSwapDirection"]:checked').value; // Validar que la palabra no esté vacía if (!newWord) { plnToast("La palabra no puede estar vacía."); return; } // Verificar si el nuevo nombre ya existe (excluyendo el item actual) if (newWord !== item.word && window.swapWords.some((sw, i) => i !== index && sw.word === newWord)) { plnToast("Esa palabra ya existe en la lista."); return; } // Actualizar el item en el array global window.swapWords[index].word = newWord; window.swapWords[index].direction = newDirection; saveSwapWordsToStorage(); renderSwapList(); modalOverlay.remove(); }); buttonContainer.appendChild(saveBtn); modalContent.appendChild(buttonContainer); modalOverlay.appendChild(modalContent); document.body.appendChild(modalOverlay); } // --- Contenedor principal --- const title = document.createElement("h4"); title.textContent = "Gestión de Reemplazos"; title.style.fontSize = "15px"; title.style.marginBottom = "10px"; parentContainer.appendChild(title); // --- Dropdown de modo de reemplazo --- const modeSelector = document.createElement("select"); modeSelector.id = "replacementModeSelector"; modeSelector.style.marginBottom = "10px"; modeSelector.style.marginTop = "5px"; // Añadir opciones al selector const optionWords = document.createElement("option"); optionWords.value = "words"; optionWords.textContent = "Reemplazos de palabras"; modeSelector.appendChild(optionWords); // Añadir opción para swap const optionSwap = document.createElement("option"); optionSwap.value = "swapStart"; optionSwap.textContent = "Palabras al inicio/final (swap)"; // Texto actualizado modeSelector.appendChild(optionSwap); parentContainer.appendChild(modeSelector); //Contenedor para reemplazos y controles const replacementsContainer = document.createElement("div"); replacementsContainer.id = "replacementsContainer"; // Sección para añadir nuevos reemplazos const addSection = document.createElement("div"); addSection.style.display = "flex"; addSection.style.gap = "8px"; addSection.style.marginBottom = "12px"; addSection.style.alignItems = "flex-end"; // Alinear inputs y botón // Contenedores para inputs de texto const fromInputContainer = document.createElement("div"); fromInputContainer.style.flexGrow = "1"; const fromLabel = document.createElement("label"); fromLabel.textContent = "Texto Original:"; fromLabel.style.display = "block"; fromLabel.style.fontSize = "12px"; fromLabel.style.marginBottom = "2px"; // Input para el texto original const fromInput = document.createElement("input"); fromInput.type = "text"; fromInput.placeholder = "Ej: Urb."; fromInput.style.width = "95%"; // Para que quepa bien fromInput.style.padding = "6px"; fromInput.style.border = "1px solid #ccc"; // Añadir label e input al contenedor fromInputContainer.appendChild(fromLabel); fromInputContainer.appendChild(fromInput); addSection.appendChild(fromInputContainer); // Contenedor para el texto de reemplazo const toInputContainer = document.createElement("div"); toInputContainer.style.flexGrow = "1"; const toLabel = document.createElement("label"); toLabel.textContent = "Texto de Reemplazo:"; toLabel.style.display = "block"; toLabel.style.fontSize = "12px"; toLabel.style.marginBottom = "2px"; // Input para el texto de reemplazo const toInput = document.createElement("input"); toInput.type = "text"; toInput.placeholder = "Ej: Urbanización"; toInput.style.width = "95%"; toInput.style.padding = "6px"; toInput.style.border = "1px solid #ccc"; toInputContainer.appendChild(toLabel); toInputContainer.appendChild(toInput); addSection.appendChild(toInputContainer); // Atributos para evitar corrección ortográfica fromInput.setAttribute('spellcheck', 'false'); toInput.setAttribute('spellcheck', 'false'); // Botón para añadir el reemplazo const addReplacementBtn = document.createElement("button"); addReplacementBtn.textContent = "Añadir"; addReplacementBtn.style.padding = "6px 10px"; addReplacementBtn.style.cursor = "pointer"; addReplacementBtn.style.height = "30px"; // Para alinear con los inputs addSection.appendChild(addReplacementBtn); // Elemento UL para la lista de reemplazos const listElement = document.createElement("ul"); listElement.id = "replacementsListElementID"; // ID ÚNICO para esta lista listElement.style.maxHeight = "150px"; listElement.style.overflowY = "auto"; listElement.style.border = "1px solid #ddd"; listElement.style.padding = "8px"; listElement.style.margin = "0 0 10px 0"; listElement.style.background = "#fff"; listElement.style.listStyle = "none"; // Event listener para el botón "Añadir" addReplacementBtn.addEventListener("click", () => { const fromValue = fromInput.value.trim(); const toValue = toInput.value.trim(); if (!fromValue) { plnToast("El campo 'Texto Original' es requerido.", 3000); return; } // Validar que no sea solo caracteres especiales if (fromValue === toValue) { plnToast("El texto original y el de reemplazo no pueden ser iguales.", 3000); return; } // Validar que no sea solo caracteres especiales if (replacementWords.hasOwnProperty(fromValue) && replacementWords[fromValue] !== toValue) { if (!confirm(`El reemplazo para "${fromValue}" ya existe ('${replacementWords[fromValue]}'). ¿Deseas sobrescribirlo con '${toValue}'?`)) return; } replacementWords[fromValue] = toValue; fromInput.value = ""; toInput.value = ""; // Renderiza toda la lista (más seguro y rápido en la práctica) renderReplacementsList(listElement); saveReplacementWordsToStorage(); }); // Botones de Acción y Drop Area (usarán la lógica compartida) const actionButtonsContainer = document.createElement("div"); actionButtonsContainer.style.display = "flex"; actionButtonsContainer.style.gap = "8px"; actionButtonsContainer.style.marginBottom = "10px"; // Botones de acción const exportButton = document.createElement("button"); exportButton.textContent = "Exportar Todo"; exportButton.title = "Exportar Excluidas y Reemplazos a XML"; exportButton.style.padding = "6px 10px"; exportButton.addEventListener("click", () => plnUiExportDataToXml("words")); // Exporta Excluidas/Reemplazos/Swap actionButtonsContainer.appendChild(exportButton); // Botón para exportar solo reemplazos const clearButton = document.createElement("button"); clearButton.textContent = "Limpiar Reemplazos"; clearButton.title = "Limpiar solo la lista de reemplazos"; clearButton.style.padding = "6px 10px"; clearButton.addEventListener("click", () => { if (confirm("¿Estás seguro de que deseas eliminar TODOS los reemplazos definidos?")) { replacementWords = {}; saveReplacementWordsToStorage(); renderReplacementsList(listElement); } }); actionButtonsContainer.appendChild(clearButton); // Botón para importar desde XML const dropArea = document.createElement("div"); dropArea.textContent = "Arrastra aquí el archivo XML (contiene Excluidas y Reemplazos)"; dropArea.style.border = "2px dashed #ccc"; dropArea.style.borderRadius = "4px"; dropArea.style.padding = "15px"; dropArea.style.marginTop = "10px"; dropArea.style.textAlign = "center"; dropArea.style.background = "#f9f9f9"; dropArea.style.color = "#555"; // Añadir estilos para el drop area dropArea.addEventListener("dragover", (e) => { e.preventDefault(); dropArea.style.background = "#e9e9e9"; }); // Cambiar el fondo al salir del área de arrastre dropArea.addEventListener("dragleave", () => { dropArea.style.background = "#f9f9f9"; }); // Manejar el evento de drop dropArea.addEventListener("drop", (e) => { e.preventDefault(); dropArea.style.background = "#f9f9f9"; plnUiHandleXmlFileDrop(e.dataTransfer.files[0]); // defaults to "words" }); // --- Ensamblar en replacementsContainer --- replacementsContainer.appendChild(addSection); replacementsContainer.appendChild(listElement); replacementsContainer.appendChild(actionButtonsContainer); replacementsContainer.appendChild(dropArea); parentContainer.appendChild(replacementsContainer); // --- Contenedor para swapStart/frases al inicio --- const swapContainer = document.createElement("div"); swapContainer.id = "swapContainer"; swapContainer.style.display = "none"; // === TÍTULO Y EXPLICACIONES === const swapTitle = document.createElement("h4"); swapTitle.textContent = "Palabras de Intercambio (Swap)"; swapTitle.style.fontSize = "14px"; swapTitle.style.marginBottom = "8px"; swapContainer.appendChild(swapTitle); // Caja de explicación const swapExplanationBox = document.createElement("div"); swapExplanationBox.style.background = "#f4f8ff"; swapExplanationBox.style.borderLeft = "4px solid #2d6df6"; swapExplanationBox.style.padding = "10px"; swapExplanationBox.style.margin = "10px 0"; swapExplanationBox.style.fontSize = "13px"; swapExplanationBox.style.lineHeight = "1.4"; swapExplanationBox.innerHTML = "<strong>🔄 ¿Qué hace esta lista?</strong><br>" + "Las palabras aquí se moverán al inicio o al final del nombre.<br>" + "<em>Ej:</em> \"Las Palmas <b>Urbanización</b>\" → \"<b>Urbanización</b> Las Palmas\" (si se configura 'Al Inicio')."; swapContainer.appendChild(swapExplanationBox); // Contenedor principal para los controles, ahora apilado verticalmente const swapInputContainer = document.createElement("div"); swapInputContainer.style.display = "flex"; swapInputContainer.style.flexDirection = "column"; // Apilado vertical swapInputContainer.style.gap = "8px"; swapInputContainer.style.marginBottom = "8px"; // Fila 1: Input de la palabra const swapInputDiv = document.createElement("div"); const swapInputLabel = document.createElement("label"); swapInputLabel.textContent = "Palabra a agregar:"; swapInputLabel.style.fontSize = "12px"; swapInputLabel.style.display = "block"; swapInputLabel.style.marginBottom = "2px"; const swapInput = document.createElement("input"); swapInput.type = "text"; swapInput.placeholder = "Ej: Urbanización"; swapInput.style.width = "calc(100% - 12px)"; // Ancho completo swapInput.style.padding = "6px"; swapInput.setAttribute('spellcheck', 'false'); swapInputDiv.appendChild(swapInputLabel); swapInputDiv.appendChild(swapInput); // Fila 2: Controles de dirección y botón de añadir const controlsRow = document.createElement("div"); controlsRow.style.display = "flex"; controlsRow.style.alignItems = "center"; controlsRow.style.gap = "10px"; // Contenedor para los radio buttons const directionContainer = document.createElement("div"); directionContainer.style.display = "flex"; directionContainer.style.gap = "15px"; directionContainer.style.padding = "5px 10px"; directionContainer.style.border = "1px solid #ccc"; directionContainer.style.borderRadius = "4px"; ['start', 'end'].forEach(dir => { const optionContainer = document.createElement('div'); optionContainer.style.display = 'flex'; optionContainer.style.alignItems = 'center'; const radio = document.createElement("input"); radio.type = "radio"; radio.name = "swapDirection"; radio.value = dir; radio.id = `swap_${dir}`; if (dir === 'start') radio.checked = true; radio.style.marginRight = "4px"; //Permitir seleccionar el radio al hacer clic en la etiqueta const label = document.createElement("label"); label.htmlFor = `swap_${dir}`; label.textContent = dir === 'start' ? 'Mover a Inicio' : 'Mover al Final'; // ETIQUETAS ACTUALIZADAS label.style.fontSize = "13px"; label.style.cursor = "pointer"; optionContainer.appendChild(radio); optionContainer.appendChild(label); directionContainer.appendChild(optionContainer); }); // Botón para añadir const swapBtn = document.createElement("button"); swapBtn.textContent = "Añadir"; swapBtn.style.padding = "6px 12px"; swapBtn.style.height = "32px"; // Ensamblar la fila 2 controlsRow.appendChild(directionContainer); controlsRow.appendChild(swapBtn); // Ensamblar el contenedor principal swapInputContainer.appendChild(swapInputDiv); swapInputContainer.appendChild(controlsRow); swapContainer.appendChild(swapInputContainer); // Añadir el contenedor principal al panel // === EVENT LISTENER PARA EL BOTÓN AÑADIR (sin cambios en la lógica) === swapBtn.addEventListener("click", () => { const val = swapInput.value.trim(); const direction = document.querySelector('input[name="swapDirection"]:checked').value; if (!val || /^[^a-zA-Z0-9]+$/.test(val)) { plnToast("No se permiten caracteres especiales solos o palabras vacías.", 3000); return; } if (window.swapWords.some(item => item.word === val)) { plnToast("Esa palabra ya existe en la lista.", 3000); return; } window.swapWords.push({ word: val, direction: direction }); saveSwapWordsToStorage(); swapInput.value = ""; renderSwapList(); }); // === CAMPO DE BÚSQUEDA === const searchSwapInput = document.createElement("input"); searchSwapInput.type = "text"; searchSwapInput.placeholder = "Buscar palabra..."; searchSwapInput.id = "searchSwapInput"; searchSwapInput.style.width = "calc(100% - 12px)"; searchSwapInput.style.padding = "6px"; searchSwapInput.style.marginBottom = "8px"; searchSwapInput.style.border = "1px solid #ccc"; searchSwapInput.addEventListener("input", () => renderSwapList()); swapContainer.appendChild(searchSwapInput); parentContainer.appendChild(swapContainer); // === LÓGICA DE RENDERIZADO DE LA LISTA (ACTUALIZADA) === function renderSwapList() { const searchInput = document.getElementById("searchSwapInput"); const swapList = swapContainer.querySelector("ul") || (() => { const ul = document.createElement("ul"); ul.id = "swapList"; ul.style.maxHeight = "120px"; ul.style.overflowY = "auto"; ul.style.border = "1px solid #ddd"; ul.style.padding = "8px"; ul.style.margin = "0"; ul.style.background = "#fff"; ul.style.listStyle = "none"; swapContainer.appendChild(ul); return ul; })(); swapList.innerHTML = ""; if (!window.swapWords || window.swapWords.length === 0) { const li = document.createElement("li"); li.textContent = "No hay palabras de intercambio definidas."; li.style.textAlign = "center"; li.style.color = "#777"; swapList.appendChild(li); return; } const searchTerm = searchInput ? searchInput.value.trim().toLowerCase() : ""; const filteredSwapWords = window.swapWords.filter(item => item.word.toLowerCase().includes(searchTerm)); filteredSwapWords.forEach((item, index) => { const li = document.createElement("li"); li.style.display = "flex"; li.style.justifyContent = "space-between"; li.style.alignItems = "center"; li.style.padding = "4px 2px"; li.style.borderBottom = "1px solid #f0f0f0"; // Mostrar la palabra con su dirección const wordSpan = document.createElement("span"); const directionIcon = item.direction === "start" ? "←" : "→"; const directionText = item.direction === "start" ? "Al Inicio" : "Al Final"; wordSpan.innerHTML = `<b>${item.word}</b> <small style="color: #666;">(${directionIcon} ${directionText})</small>`; // Contenedor para los botones const btnContainer = document.createElement("span"); btnContainer.style.display = "flex"; btnContainer.style.gap = "4px"; // Botón Editar const editBtn = document.createElement("button"); editBtn.innerHTML = "✏️"; editBtn.title = "Editar"; editBtn.style.border = "none"; editBtn.style.background = "transparent"; editBtn.style.cursor = "pointer"; editBtn.addEventListener("click", () => { const originalIndex = window.swapWords.findIndex(sw => sw.word === item.word); if (originalIndex > -1) { openSwapWordEditor(window.swapWords[originalIndex], originalIndex); } }); // Botón Eliminar const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = "🗑️"; deleteBtn.title = "Eliminar"; deleteBtn.style.border = "none"; deleteBtn.style.background = "transparent"; deleteBtn.style.cursor = "pointer"; deleteBtn.addEventListener("click", () => { if (confirm(`¿Eliminar la palabra swap '${item.word}'?`)) { const indexToDelete = window.swapWords.findIndex(sw => sw.word === item.word); if (indexToDelete > -1) { window.swapWords.splice(indexToDelete, 1); saveSwapWordsToStorage(); renderSwapList(); } } }); btnContainer.appendChild(editBtn); btnContainer.appendChild(deleteBtn); li.appendChild(wordSpan); li.appendChild(btnContainer); swapList.appendChild(li); }); } // Render inicial y listener del selector renderReplacementsList(listElement); renderSwapList(); modeSelector.addEventListener("change", () => { replacementsContainer.style.display = modeSelector.value === "words" ? "block" : "none"; swapContainer.style.display = modeSelector.value === "swapStart" ? "block" : "none"; }); }// Crea el gestor de diccionario //Permite crear la lista de palabras swap function renderSwapList() { const searchInput = document.getElementById("searchSwapInput"); const swapList = swapContainer.querySelector("ul") || (() => { const ul = document.createElement("ul"); ul.id = "swapList"; ul.style.maxHeight = "120px"; ul.style.overflowY = "auto"; ul.style.border = "1px solid #ddd"; ul.style.padding = "8px"; ul.style.margin = "0"; ul.style.background = "#fff"; ul.style.listStyle = "none"; swapContainer.appendChild(ul); return ul; })(); swapList.innerHTML = ""; if (!window.swapWords || window.swapWords.length === 0) { const li = document.createElement("li"); li.textContent = "No hay palabras de intercambio definidas."; li.style.textAlign = "center"; li.style.color = "#777"; swapList.appendChild(li); return; } const searchTerm = searchInput ? searchInput.value.trim().toLowerCase() : ""; const filteredSwapWords = window.swapWords.filter(item => item.word.toLowerCase().includes(searchTerm)); filteredSwapWords.forEach((item, index) => { const li = document.createElement("li"); li.style.display = "flex"; li.style.justifyContent = "space-between"; li.style.alignItems = "center"; li.style.padding = "4px 2px"; li.style.borderBottom = "1px solid #f0f0f0"; const wordSpan = document.createElement("span"); const directionIcon = item.direction === "start" ? "←" : "→"; const directionText = item.direction === "start" ? "Al Inicio" : "Al Final"; wordSpan.innerHTML = `<b>${item.word}</b> <small style="color: #666;">(${directionIcon} ${directionText})</small>`; const btnContainer = document.createElement("span"); btnContainer.style.display = "flex"; btnContainer.style.gap = "4px"; // Botón Editar const editBtn = document.createElement("button"); editBtn.innerHTML = "✏️"; editBtn.title = "Editar"; editBtn.style.border = "none"; editBtn.style.background = "transparent"; editBtn.style.cursor = "pointer"; editBtn.addEventListener("click", () => { const originalIndex = window.swapWords.findIndex(sw => sw.word === item.word); if (originalIndex > -1) { openSwapWordEditor(window.swapWords[originalIndex], originalIndex); } }); // Botón Eliminar const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = "🗑️"; deleteBtn.title = "Eliminar"; deleteBtn.style.border = "none"; deleteBtn.style.background = "transparent"; deleteBtn.style.cursor = "pointer"; deleteBtn.addEventListener("click", () => { if (confirm(`¿Eliminar la palabra swap '${item.word}'?`)) { const indexToDelete = window.swapWords.findIndex(sw => sw.word === item.word); if (indexToDelete > -1) { window.swapWords.splice(indexToDelete, 1); saveSwapWordsToStorage(); renderSwapList(); } } }); btnContainer.appendChild(editBtn); btnContainer.appendChild(deleteBtn); li.appendChild(wordSpan); li.appendChild(btnContainer); swapList.appendChild(li); }); }//renderSwapList // Renderiza la lista de palabras excluidas function renderExcludedWordsList(ulElement, filter = "") { // Asegurarse de que ulElement es un elemento UL válido if (!ulElement) { return; } // Asegurarse de que ulElement es válido const currentFilter = filter.toLowerCase(); ulElement.innerHTML = ""; // Asegurarse de que excludedWords es un Set const wordsToRender = Array.from(excludedWords).filter(word => word.toLowerCase().includes(currentFilter)) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); // Si no hay palabras para renderizar, mostrar mensaje if (wordsToRender.length === 0) { const li = document.createElement("li"); li.textContent = "No hay palabras excluidas."; li.style.textAlign = "center"; li.style.color = "#777"; ulElement.appendChild(li); return; } // Renderizar cada palabra wordsToRender.forEach(word => { const li = document.createElement("li"); li.style.display = "flex"; // Agregado para alinear texto y botones li.style.justifyContent = "space-between"; // Agregado para espacio entre texto y botones li.style.alignItems = "center"; // Agregado para centrado vertical li.style.padding = "5px"; li.style.borderBottom = "1px solid #ddd"; // Span para el texto de la palabra const wordSpan = document.createElement("span"); wordSpan.textContent = word; wordSpan.style.flexGrow = "1"; // Permite que el texto ocupe el espacio disponible wordSpan.style.marginRight = "10px"; // Espacio entre el texto y los botones li.appendChild(wordSpan); //Bloque para los botones de edición y eliminación --- const btnContainer = document.createElement("span"); btnContainer.style.display = "flex"; btnContainer.style.gap = "8px"; // Espacio entre los botones // Botón de edición const editBtn = document.createElement("button"); editBtn.innerHTML = "✏️"; // Icono de lápiz editBtn.title = "Editar"; editBtn.style.border = "none"; editBtn.style.background = "transparent"; editBtn.style.cursor = "pointer"; editBtn.style.padding = "2px"; editBtn.style.fontSize = "14px"; editBtn.addEventListener("click", () => { const newWord = prompt("Editar palabra:", word); if (newWord !== null && newWord.trim() !== word) { const validation = isValidExcludedWord(newWord.trim()); if (!validation.valid) { plnToast(validation.msg,3000); return; } // Eliminar la palabra antigua del Set y Map excludedWords.delete(word); const oldFirstChar = word.charAt(0).toLowerCase(); if (excludedWordsMap.has(oldFirstChar)) { excludedWordsMap.get(oldFirstChar).delete(word); if (excludedWordsMap.get(oldFirstChar).size === 0) { excludedWordsMap.delete(oldFirstChar); } } // Añadir la nueva palabra al Set y Map const trimmedNewWord = newWord.trim(); excludedWords.add(trimmedNewWord); const newFirstChar = trimmedNewWord.charAt(0).toLowerCase(); if (!excludedWordsMap.has(newFirstChar)) { excludedWordsMap.set(newFirstChar, new Set()); } excludedWordsMap.get(newFirstChar).add(trimmedNewWord); renderExcludedWordsList(ulElement, currentFilter); saveExcludedWordsToLocalStorage(); } }); btnContainer.appendChild(editBtn); // Botón de eliminación const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = "🗑️"; // Icono de bote de basura deleteBtn.title = "Eliminar"; deleteBtn.style.border = "none"; deleteBtn.style.background = "transparent"; deleteBtn.style.cursor = "pointer"; deleteBtn.style.padding = "2px"; deleteBtn.style.fontSize = "14px"; deleteBtn.addEventListener("click", () => { if (confirm(`¿Eliminar la palabra '${word}' de la lista de especiales?`)) { excludedWords.delete(word); const firstChar = word.charAt(0).toLowerCase(); if (excludedWordsMap.has(firstChar)) { excludedWordsMap.get(firstChar).delete(word); if (excludedWordsMap.get(firstChar).size === 0) { excludedWordsMap.delete(firstChar); } } renderExcludedWordsList(ulElement, currentFilter); saveExcludedWordsToLocalStorage(); } }); btnContainer.appendChild(deleteBtn); li.appendChild(btnContainer); ulElement.appendChild(li); });// }// renderExcludedWordsList // Función para renderizar la lista de lugares excluidos function renderExcludedPlacesList(ulElement, filter = "") { // Asegurarse de que ulElement es un elemento UL válido if (!ulElement) return; ulElement.innerHTML = ""; const lowerFilter = filter.toLowerCase(); // Ahora excludedPlaces es un Map<ID, Nombre>. Iteramos sobre sus entries. const placesToRender = Array.from(excludedPlaces.entries()).filter(([placeId, placeNameSaved]) => // Filtra por ID o por el nombre guardado placeId.toLowerCase().includes(lowerFilter) || placeNameSaved.toLowerCase().includes(lowerFilter)).sort((a, b) => { // Ordena alfabéticamente por el nombre guardado return a[1].toLowerCase().localeCompare(b[1].toLowerCase()); // Compara por el nombre (índice 1 del entry) }); // Si no hay lugares para renderizar, mostrar mensaje if (placesToRender.length === 0) { const li = document.createElement("li"); li.textContent = "No hay lugares excluidos."; li.style.textAlign = "center"; li.style.color = "#777"; li.style.padding = "5px"; ulElement.appendChild(li); return; } // Renderizar cada lugar placesToRender.forEach(([placeId, placeNameSaved]) => { // Ahora recibimos [ID, NombreGuardado] const li = document.createElement("li"); li.style.display = "flex"; li.style.justifyContent = "space-between"; li.style.alignItems = "center"; li.style.padding = "4px 2px"; li.style.borderBottom = "1px solid #f0f0f0"; // Muestra el nombre guardado, con un fallback si el nombre guardado está vacío. const displayName = placeNameSaved || `ID: ${placeId}`; const linkSpan = document.createElement("span"); linkSpan.style.flexGrow = "1"; linkSpan.style.marginRight = "10px"; const link = document.createElement("a"); link.href = "#"; link.textContent = displayName; // Muestra el nombre guardado link.title = `Abrir lugar en WME (ID: ${placeId})`; // El tooltip sigue mostrando el ID link.addEventListener("click", (e) => { e.preventDefault(); // Intenta obtener el lugar del modelo para seleccionarlo y centrarlo // Usamos W.model como fallback si wmeSDK.DataModel.Venues.getById no es eficiente aquí o no está diseñado para esta interacción const venueObj = W.model.venues.getObjectById(placeId); // <--- const venueSDKForUse = venueSDKForRender; // Objeto del SDK que pasamos desde processNextPlace if (venueObj) { if (W.map && typeof W.map.setCenter === 'function' && venueObj.getOLGeometry && venueObj.getOLGeometry().getCentroid) { W.map.setCenter(venueObj.getOLGeometry().getCentroid(), null, false, 0); } if (W.selectionManager && typeof W.selectionManager.select === 'function') { W.selectionManager.select(venueObj); // <--- REINTRODUCIMOS W.selectionManager.select } else if (W.selectionManager && typeof W.selectionManager.setSelectedModels === 'function') { W.selectionManager.setSelectedModels([venueObj]); // Fallback para versiones antiguas } } else { // Si el lugar no está en el modelo (fuera de vista), avisa y ofrece abrir en nueva pestaña. const confirmOpen = confirm(`Lugar '${displayName}' (ID: ${placeId}) no encontrado en el modelo actual. ¿Deseas abrirlo en una nueva pestaña del editor?`); if (confirmOpen) { const wmeUrl = `https://www.waze.com/editor?env=row&venueId=${placeId}`; window.open(wmeUrl, '_blank'); } } }); linkSpan.appendChild(link); li.appendChild(linkSpan); // Botón para eliminar el lugar de la lista de excluidos. const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = "🗑️"; deleteBtn.title = "Eliminar lugar de la lista de excluidos"; deleteBtn.style.border = "none"; deleteBtn.style.background = "transparent"; deleteBtn.style.cursor = "pointer"; deleteBtn.style.padding = "2px"; deleteBtn.style.fontSize = "14px"; deleteBtn.addEventListener("click", () => { // ************************************************************ // INICIO DE LA MODIFICACIÓN: Modal de confirmación "bonito" // ************************************************************ const confirmModal = document.createElement("div"); confirmModal.style.position = "fixed"; confirmModal.style.top = "50%"; confirmModal.style.left = "50%"; confirmModal.style.transform = "translate(-50%, -50%)"; confirmModal.style.background = "#fff"; confirmModal.style.border = "1px solid #aad"; confirmModal.style.padding = "28px 32px 20px 32px"; confirmModal.style.zIndex = "20000"; // Z-INDEX ALTO confirmModal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)"; confirmModal.style.fontFamily = "sans-serif"; confirmModal.style.borderRadius = "10px"; confirmModal.style.textAlign = "center"; confirmModal.style.minWidth = "340px"; // Ícono visual const iconElement = document.createElement("div"); iconElement.innerHTML = "⚠️"; // Ícono de advertencia iconElement.style.fontSize = "38px"; iconElement.style.marginBottom = "10px"; confirmModal.appendChild(iconElement); // Mensaje principal const messageTitle = document.createElement("div"); messageTitle.innerHTML = `<b>¿Eliminar de excluidos "${placeNameSaved}"?</b>`; messageTitle.style.fontSize = "20px"; messageTitle.style.marginBottom = "8px"; confirmModal.appendChild(messageTitle); // Mensaje explicativo const explanationDiv = document.createElement("div"); explanationDiv.textContent = `Este lugar volverá a aparecer en futuras búsquedas del normalizador.`; explanationDiv.style.fontSize = "15px"; explanationDiv.style.color = "#555"; explanationDiv.style.marginBottom = "18px"; confirmModal.appendChild(explanationDiv); // Botones de confirmación const buttonWrapper = document.createElement("div"); buttonWrapper.style.display = "flex"; buttonWrapper.style.justifyContent = "center"; buttonWrapper.style.gap = "18px"; // Botón Cancelar const cancelBtn = document.createElement("button"); cancelBtn.textContent = "Cancelar"; cancelBtn.style.padding = "7px 18px"; cancelBtn.style.background = "#eee"; cancelBtn.style.border = "none"; cancelBtn.style.borderRadius = "4px"; cancelBtn.style.cursor = "pointer"; cancelBtn.addEventListener("click", () => confirmModal.remove()); // Botón Confirmar Eliminación const confirmDeleteBtn = document.createElement("button"); confirmDeleteBtn.textContent = "Eliminar"; confirmDeleteBtn.style.padding = "7px 18px"; confirmDeleteBtn.style.background = "#d9534f"; // Rojo confirmDeleteBtn.style.color = "#fff"; confirmDeleteBtn.style.border = "none"; confirmDeleteBtn.style.borderRadius = "4px"; confirmDeleteBtn.style.cursor = "pointer"; confirmDeleteBtn.style.fontWeight = "bold"; confirmDeleteBtn.addEventListener("click", () => { // Aquí va la lógica que antes estaba directamente en el if(confirm) excludedPlaces.delete(placeId); // Sigue eliminando por ID renderExcludedPlacesList(ulElement, filter); // Vuelve a renderizar la lista después de eliminar. saveExcludedPlacesToLocalStorage(); // Guarda los cambios en localStorage. showTemporaryMessage("Lugar eliminado de la lista de excluidos.", 3000, 'success'); confirmModal.remove(); // Cerrar el modal después de la acción }); buttonWrapper.appendChild(cancelBtn); buttonWrapper.appendChild(confirmDeleteBtn); confirmModal.appendChild(buttonWrapper); document.body.appendChild(confirmModal); // Añadir el modal al body }); li.appendChild(deleteBtn); ulElement.appendChild(li); }); }// renderExcludedPlacesList // Renderizar lista de palabras del diccionario function renderDictionaryList(ulElement, filter = "") { // Asegurarse de que ulElement es válido if (!ulElement || !window.dictionaryWords) return; // Asegurarse de que ulElement es válido const currentFilter = filter.toLowerCase(); ulElement.innerHTML = ""; // Asegurarse de que dictionaryWords es un Set const wordsToRender = Array.from(window.dictionaryWords) .filter(word => word.toLowerCase().startsWith(currentFilter)) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); // Si no hay palabras que renderizar, mostrar mensaje if (wordsToRender.length === 0) { const li = document.createElement("li"); li.textContent = window.dictionaryWords.size === 0 ? "El diccionario está vacío." : "No hay coincidencias."; li.style.textAlign = "center"; li.style.color = "#777"; ulElement.appendChild(li); // Guardar diccionario también cuando está vacío try { localStorage.setItem( "dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords))); } catch (e) { plnLog('error', "[WME PLN] Error guardando el diccionario en localStorage:", e); } return; } // Renderizar cada palabra wordsToRender.forEach(word => { const li = document.createElement("li"); li.style.display = "flex"; li.style.justifyContent = "space-between"; li.style.alignItems = "center"; li.style.padding = "4px 2px"; li.style.borderBottom = "1px solid #f0f0f0"; // Span para la palabra const wordSpan = document.createElement("span"); wordSpan.textContent = word; wordSpan.style.maxWidth = "calc(100% - 60px)"; wordSpan.style.overflow = "hidden"; wordSpan.style.textOverflow = "ellipsis"; wordSpan.style.whiteSpace = "nowrap"; wordSpan.title = word; li.appendChild(wordSpan); // Contenedor para los iconos de acción const iconContainer = document.createElement("span"); iconContainer.style.display = "flex"; iconContainer.style.gap = "8px"; // Botón de edición y eliminación const editBtn = document.createElement("button"); editBtn.innerHTML = "✏️"; editBtn.title = "Editar"; editBtn.style.border = "none"; editBtn.style.background = "transparent"; editBtn.style.cursor = "pointer"; editBtn.style.padding = "2px"; editBtn.style.fontSize = "14px"; editBtn.addEventListener("click", () => { const newWord = prompt("Editar palabra:", word); if (newWord !== null && newWord.trim() !== word) { window.dictionaryWords.delete(word); window.dictionaryWords.add(newWord.trim()); renderDictionaryList(ulElement, currentFilter); } }); // Botón de eliminación const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = "🗑️"; deleteBtn.title = "Eliminar"; deleteBtn.style.border = "none"; deleteBtn.style.background = "transparent"; deleteBtn.style.cursor = "pointer"; deleteBtn.style.padding = "2px"; deleteBtn.style.fontSize = "14px"; deleteBtn.addEventListener("click", () => { // Confirmación antes de eliminar if (confirm(`¿Eliminar la palabra '${word}' del diccionario?`)) { window.dictionaryWords.delete(word); renderDictionaryList(ulElement, currentFilter); } }); iconContainer.appendChild(editBtn); iconContainer.appendChild(deleteBtn); li.appendChild(iconContainer); ulElement.appendChild(li); }); // Guardar el diccionario actualizado en localStorage después de cada render try { localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords))); } catch (e) { plnLog('error',"[WME PLN] Error guardando el diccionario en localStorage:", e); } }// renderDictionaryList // Renderiza la lista de reemplazos function renderReplacementsList(ulElement) { plnLog('swap', "[WME_PLN][DEBUG] renderReplacementsList llamada para:", ulElement ? ulElement.id : "Elemento UL nulo"); if (!ulElement) { plnLog('error',"[WME PLN] Elemento UL para reemplazos no proporcionado a renderReplacementsList."); return; } ulElement.innerHTML = ""; // Limpiar lista actual const entries = Object.entries(replacementWords); // Si no hay reemplazos, mostrar mensaje if (entries.length === 0) { const li = document.createElement("li"); li.textContent = "No hay reemplazos definidos."; li.style.textAlign = "center"; li.style.color = "#777"; li.style.padding = "5px"; ulElement.appendChild(li); return; } // Ordenar alfabéticamente por la palabra original (from) entries.sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase())); entries.forEach(([from, to]) => { const li = document.createElement("li"); li.style.display = "flex"; li.style.justifyContent = "space-between"; li.style.alignItems = "center"; li.style.padding = "4px 2px"; li.style.borderBottom = "1px solid #f0f0f0"; // Añadir un tooltip al elemento li const textContainer = document.createElement("div"); textContainer.style.flexGrow = "1"; textContainer.style.overflow = "hidden"; textContainer.style.textOverflow = "ellipsis"; textContainer.style.whiteSpace = "nowrap"; textContainer.title = `Reemplazar "${from}" con "${to}"`; // Crear los spans para mostrar el texto const fromSpan = document.createElement("span"); fromSpan.textContent = from; fromSpan.style.fontWeight = "bold"; textContainer.appendChild(fromSpan); // Añadir un espacio entre el "from" y el "to" const arrowSpan = document.createElement("span"); arrowSpan.textContent = " → "; arrowSpan.style.margin = "0 5px"; textContainer.appendChild(arrowSpan); // Span para el texto de reemplazo const toSpan = document.createElement("span"); toSpan.textContent = to; toSpan.style.color = "#007bff"; textContainer.appendChild(toSpan); // Añadir el contenedor de texto al li li.appendChild(textContainer); // Botón Editar const editBtn = document.createElement("button"); editBtn.innerHTML = "✏️"; editBtn.title = "Editar este reemplazo"; editBtn.style.border = "none"; editBtn.style.background = "transparent"; editBtn.style.cursor = "pointer"; editBtn.style.padding = "2px 4px"; editBtn.style.fontSize = "14px"; editBtn.style.marginLeft = "4px"; editBtn.addEventListener("click", () => { const newFrom = prompt("Editar texto original:", from); if (newFrom === null) return; const newTo = prompt("Editar texto de reemplazo:", to); if (newTo === null) return; if (!newFrom.trim()) { plnToast("El campo 'Texto Original' es requerido.", 3000); return; } if (newFrom === newTo) { plnToast("El texto original y el de reemplazo no pueden ser iguales.", 3000); return; } // Si cambia la clave, elimina la anterior if (newFrom !== from) delete replacementWords[from]; replacementWords[newFrom] = newTo; renderReplacementsList(ulElement); saveReplacementWordsToStorage(); }); // Botón Eliminar const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = "🗑️"; deleteBtn.title = `Eliminar este reemplazo`; deleteBtn.style.border = "none"; deleteBtn.style.background = "transparent"; deleteBtn.style.cursor = "pointer"; deleteBtn.style.padding = "2px 4px"; deleteBtn.style.fontSize = "14px"; deleteBtn.style.marginLeft = "4px"; deleteBtn.addEventListener("click", () => { if (confirm(`¿Estás seguro de eliminar el reemplazo:\n"${from}" → "${to}"?`)) { delete replacementWords[from]; renderReplacementsList(ulElement); saveReplacementWordsToStorage(); } }); // Contenedor para los botones de acción const btnContainer = document.createElement("span"); btnContainer.style.display = "flex"; btnContainer.style.gap = "4px"; btnContainer.appendChild(editBtn); btnContainer.appendChild(deleteBtn); // Añadir el contenedor de botones al li li.appendChild(btnContainer); ulElement.appendChild(li); }); }// renderReplacementsList // Carga las palabras excluidas desde localStorage function saveExcludedWordsToLocalStorage() { try { localStorage.setItem("excludedWordsList", JSON.stringify(Array.from(excludedWords))); plnLog('swap', "[WME PLN] Lista de palabras especiales guardada en localStorage."); } catch (e) { plnLog('error',"[WME PLN] Error guardando palabras especiales en localStorage:", e); } }// saveExcludedWordsToLocalStorage // Carga las palabras excluidas desde localStorage // Añadir esta función dentro de WME_PLN_module_lists.js function loadExcludedWordsFromLocalStorage() { if (!window.excludedWords) { const stored = localStorage.getItem('wme_excludedWords'); // Ojo: la clave era diferente if (stored) { try { const parsed = JSON.parse(stored); if (Array.isArray(parsed)) { window.excludedWords = new Set(parsed); } } catch (e) { plnLog('error','No se pudieron cargar las palabras excluidas:', e); } } // Asegurarse que window.excludedWords sea un Set si no existe if (!(window.excludedWords instanceof Set)) { window.excludedWords = new Set(); } } }// loadExcludedWordsFromLocalStorage // Función para guardar los IDs de lugares excluidos en localStorage function saveExcludedPlacesToLocalStorage() { try { // Convertir el Map a un array de arrays antes de stringify localStorage.setItem("excludedPlacesList", JSON.stringify(Array.from(excludedPlaces.entries()))); plnLog('swap', '[WME PLN] Lugares excluidos GUARDADOS EXITOSAMENTE.'); } catch (e) { plnLog('error','[WME PLN] Error guardando lugares excluidos en localStorage:', e); } }// saveExcludedPlacesToLocalStorage // Función para cargar los IDs de lugares excluidos desde localStorage function loadExcludedPlacesFromLocalStorage() { if (!window.excludedPlaces) { const storedData = localStorage.getItem('wme_excluded_places'); if (storedData) { try { // El formato guardado es un array de [id, nombre] const parsedArray = JSON.parse(storedData); if (Array.isArray(parsedArray)) { // Convertimos el array de nuevo a un Map window.excludedPlaces = new Map(parsedArray); } } catch (e) { plnLog('error','No se pudieron cargar los lugares excluidos:', e); } } // Si después de todo no es un Map, lo inicializamos como uno vacío if (!(window.excludedPlaces instanceof Map)) { window.excludedPlaces = new Map(); } } }// loadExcludedPlacesFromLocalStorage // Función para cargar palabras del diccionario desde Google Sheets (Hoja "Dictionary") async function loadDictionaryWordsFromSheet(forceReload = false) { const SPREADSHEET_ID = "1kJDEOn8pKLdqEyhIZ9DdcrHTb_GsoeXgIN4GisrpW2Y"; const API_KEY = "AIzaSyAQbvIQwSPNWfj6CcVEz5BmwfNkao533i8"; const RANGE = "Dictionary!A2:B"; // usa window.dictionaryWords y window.dictionaryIndex para almacenar las palabras y su índice // Si no existen, las inicializa como un Set y un objeto vacío if (!window.dictionaryWords) window.dictionaryWords = new Set(); if (!window.dictionaryIndex) window.dictionaryIndex = {}; const url = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/${RANGE}?key=${API_KEY}`; return new Promise((resolve) => { if (SPREADSHEET_ID === "TU_SPREADSHEET_ID" || API_KEY === "TU_API_KEY") { plnLog('warn','[WME PLN] SPREADSHEET_ID o API_KEY no configurados para el diccionario.'); resolve(); return; } // verifica si hay datos en caché // Si hay datos en caché y no se fuerza la recarga, los usa // Si no hay datos en caché o se fuerza la recarga, hace la solicitud const cachedData = localStorage.getItem("wme_pln_dictionary_cache"); if (!forceReload && cachedData) { try { const { data, timestamp } = JSON.parse(cachedData); // usar caché si tiene menos de 24 horas if (data && timestamp && (Date.now() - timestamp < 24 * 60 * 60 * 1000)) { plnLog('sdk', '[WME PLN] Usando datos en caché. Tiempo restante para expirar:', ((timestamp + 24 * 60 * 60 * 1000) - Date.now())/1000/60, 'minutos'); plnLog('swap', '[WME PLN] Usando diccionario en caché'); // restaura las palabras y el índice del diccionario desde la caché window.dictionaryWords = new Set(data.words); window.dictionaryIndex = data.index; resolve(); return; } } catch (e) { plnLog('warn','[WME PLN] Error al leer caché del diccionario:', e); } } makeRequest( { method: "GET", url: url, timeout: 10000, onload: function (response) { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); let newWordsAdded = 0; if (data.values) { data.values.forEach(row => { const word = (row[0] || '').trim(); if (word && !window.dictionaryWords.has(word.toLowerCase())) { window.dictionaryWords.add(word.toLowerCase()); const firstChar = word.charAt(0).toLowerCase(); if (!window.dictionaryIndex[firstChar]) window.dictionaryIndex[firstChar] = []; window.dictionaryIndex[firstChar].push(word.toLowerCase()); newWordsAdded++; } }); // Cache the dictionary try { localStorage.setItem("wme_pln_dictionary_cache", JSON.stringify( { data: { words: Array.from(window.dictionaryWords), index: window.dictionaryIndex }, timestamp: Date.now() })); } catch (e) { plnLog('warn','[WME PLN] Error al guardar caché del diccionario:', e); } // también guarda en localStorage para uso rápido try { localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords))); } catch (e) { plnLog('error',"[WME PLN] Error guardando diccionario en localStorage:", e); } plnLog('swap', `[WME PLN] Diccionario cargado: ${newWordsAdded} palabras nuevas añadidas.`); } } catch (e) { plnLog('error','[WME PLN] Error al procesar datos del diccionario:', e); } } resolve(); }, // Añade esto en ambas funciones, justo después del try/catch en onload: onerror: function (error) { plnLog('error','[WME PLN] Error de red al cargar datos desde Google Sheets:', error); plnLog('scan', '[WME PLN] URL que falló:', url); resolve(); // Resolver la promesa para no bloquear }, ontimeout: function () { plnLog('error','[WME PLN] Timeout al cargar diccionario'); resolve(); } }); }); }//loadDictionaryWordsFromSheet // Carga las palabras reemplazo function saveReplacementWordsToStorage() { try { localStorage.setItem("replacementWordsList", JSON.stringify(replacementWords)); plnLog('swap', "[WME PLN] Lista de reemplazos guardada en localStorage."); } catch (e) { plnLog('error',"[WME PLN] Error guardando lista de reemplazos en localStorage:", e); } }// saveReplacementWordsToStorage // Carga las palabras excluidas desde localStorage function loadReplacementWordsFromStorage() { const savedReplacements = localStorage.getItem("replacementWordsList"); if (savedReplacements) { try { replacementWords = JSON.parse(savedReplacements); if (typeof replacementWords !== 'object' || replacementWords === null) { // Asegurar que sea un objeto replacementWords = {}; } } catch (e) { plnLog('error',"[WME PLN] Error cargando lista de reemplazos desde localStorage:", e); replacementWords = {}; } } else { replacementWords = {}; // Inicializar si no hay nada guardado } plnLog('swap', "[WME PLN] Reemplazos cargados:", Object.keys(replacementWords).length, "reglas."); }// loadReplacementWordsFromStorage // Carga las palabras excluidas desde localStorage // Función para guardar las palabras swap en localStorage (formato nuevo) function saveSwapWordsToStorage() { try { localStorage.setItem("swapWords", JSON.stringify(window.swapWords || [])); plnLog('swap', "[WME PLN] SwapWords guardadas en localStorage:", window.swapWords ? window.swapWords.length : 0, "palabras"); } catch (e) { plnLog('error',"[WME PLN] Error guardando swapWords en localStorage:", e); } }// saveSwapWordsToStorage // Función para cargar las palabras swap desde localStorage con migración automática function loadSwapWordsFromStorage() { const stored = localStorage.getItem("swapWords"); // Si hay datos en localStorage, intentar parsearlos if (stored) { try { const parsed = JSON.parse(stored); // MIGRACIÓN AUTOMÁTICA: Verificar el formato de los datos if (Array.isArray(parsed) && parsed.length > 0) { // Verificar si es formato antiguo (array de strings) if (typeof parsed[0] === "string") { plnLog('swap', "[WME PLN] Detectado formato antiguo de swapWords. Migrando automáticamente..."); // Migrar formato antiguo a nuevo formato window.swapWords = parsed.map(word => ({ word: word, direction: "start" // Todas las palabras existentes se configuran como "start" por defecto })); // Guardar el nuevo formato inmediatamente saveSwapWordsToStorage(); plnLog('swap', `[WME PLN] Migración completada: ${window.swapWords.length} palabras migradas a formato 'start'.`); } else if (typeof parsed[0] === "object" && parsed[0].hasOwnProperty('word')) { // Ya está en formato nuevo window.swapWords = parsed; plnLog('swap', "[WME PLN] Formato nuevo de swapWords detectado. No se requiere migración."); } else { // Formato desconocido, inicializar vacío plnLog('warn',"[WME PLN] Formato desconocido en swapWords. Inicializando lista vacía."); window.swapWords = []; } } else { // Array vacío o null window.swapWords = []; } } catch (e) { plnLog('error',"[WME PLN] Error al parsear swapWords desde localStorage:", e); window.swapWords = []; } } else { // No hay datos guardados window.swapWords = []; } }// loadSwapWordsFromStorage