Enhance wildcard handling and tooltip functionality in WildcardManager; add clear button to text editor modal

This commit is contained in:
dadoirie
2025-08-31 02:01:49 +02:00
parent aa5a322cee
commit 27dc694885
2 changed files with 334 additions and 81 deletions
+47 -19
View File
@@ -479,11 +479,8 @@ class TextLoaderOperations:
content = payload.get('content', '')
wildcards_selections = payload.get('wildcards_selections', '')
if not content.strip():
return self.error_response("Empty files are not allowed")
# Parse wildcards from the new content
wildcards = self.node.parse_wildcards(content)
wildcards = self.node.parse_wildcards(content) if content.strip() else []
# Apply existing selections if any
try:
@@ -508,7 +505,8 @@ class TextLoaderOperations:
if wildcard.get('selected'):
selections[wildcard['index']] = {
'selected': wildcard['selected'],
'original': wildcard['original']
'original': wildcard['original'],
'position': wildcard.get('position', 0) # NEW: Include position
}
# NEW: Preserve mark if it exists
if wildcard.get('mark'):
@@ -526,7 +524,9 @@ class TextLoaderOperations:
wildcard_index = str(payload.get('wildcard_index', ''))
selected_value = payload.get('selected_value', '')
original_wildcard = payload.get('original_wildcard', '')
mark_value = payload.get('mark_value') # NEW: Handle mark value
mark_value = payload.get('mark_value')
wildcard_position = payload.get('wildcard_position', 0) # NEW: Get position
wildcard_content = payload.get('wildcard_content', '') # NEW: Get content
wildcards_selections = payload.get('wildcards_selections', '')
try:
@@ -534,19 +534,30 @@ class TextLoaderOperations:
except json.JSONDecodeError:
selections = {}
if selected_value:
selections[wildcard_index] = {
'selected': selected_value,
'original': original_wildcard
}
# NEW: Handle mark value
if mark_value is not None:
if mark_value: # Non-empty mark
selections[wildcard_index]['mark'] = mark_value
elif 'mark' in selections[wildcard_index]:
# Remove mark if empty value provided
del selections[wildcard_index]['mark']
else:
# FIXED: Preserve existing data when updating
if wildcard_index not in selections:
selections[wildcard_index] = {}
# Update selection data if provided
if selected_value is not None: # Allow empty string
selections[wildcard_index]['selected'] = selected_value
selections[wildcard_index]['original'] = original_wildcard
selections[wildcard_index]['position'] = wildcard_position
# Handle mark value separately to preserve existing selection data
if mark_value is not None:
if mark_value: # Non-empty mark
selections[wildcard_index]['mark'] = mark_value
selections[wildcard_index]['mark_content'] = wildcard_content
selections[wildcard_index]['mark_position'] = wildcard_position
else:
# Remove mark if empty value provided
selections[wildcard_index].pop('mark', None)
selections[wildcard_index].pop('mark_content', None)
selections[wildcard_index].pop('mark_position', None)
# Only remove the entire entry if it has no useful data
if (not selections[wildcard_index].get('selected') and not selections[wildcard_index].get('mark') and wildcard_index in selections):
selections.pop(wildcard_index, None)
return self.success_response({
@@ -554,6 +565,23 @@ class TextLoaderOperations:
"selections_json": json.dumps(selections, ensure_ascii=False)
})
def _cleanup_orphaned_marks(self, selections: Dict):
"""Remove mark data for selections that no longer have a selected value"""
keys_to_clean = []
for key, selection_data in selections.items():
if isinstance(selection_data, dict):
if not selection_data.get('selected') and selection_data.get('mark'):
# Mark exists but no selection - clean it up
keys_to_clean.append(key)
for key in keys_to_clean:
if 'mark' in selections[key]:
del selections[key]['mark']
if 'mark_content' in selections[key]:
del selections[key]['mark_content']
if 'mark_position' in selections[key]:
del selections[key]['mark_position']
def _clear_wildcard_branch(self, selections: Dict, wildcard_index: str):
keys_to_remove = [key for key in selections.keys() if key == wildcard_index or key.startswith(wildcard_index + '.')]
for key in keys_to_remove:
+287 -62
View File
@@ -101,8 +101,25 @@ class WildcardManager {
if (!markIcon) return;
const selections = JSON.parse(this.operations.originalSelections || '{}');
const pendingMark = this.operations.pendingSelections[wildcard.index]?.mark;
const savedMark = selections[wildcard.index]?.mark ?? '';
const wildcardId = this._createWildcardIdentifier(wildcard);
// Look for mark by wildcard identifier first, then fallback to index
let savedMark = '';
let pendingMark = this.operations.pendingSelections[wildcard.index]?.mark;
// First try to find by identifier in all selections
for (const [selectionIndex, selectionData] of Object.entries(selections)) {
const selectionId = this._getWildcardIdentifierFromSelections(selections, selectionIndex);
if (selectionId === wildcardId && selectionData.mark) {
savedMark = selectionData.mark;
break;
}
}
// Fallback to direct index lookup
if (!savedMark && selections[wildcard.index]?.mark) {
savedMark = selections[wildcard.index].mark;
}
this._setMarkIconState(markIcon, pendingMark, savedMark);
}
@@ -172,8 +189,25 @@ class WildcardManager {
const dropdown = this._createCustomDropdown(wildcard);
const selections = JSON.parse(this.operations.originalSelections || '{}');
const pendingMark = this.operations.pendingSelections[wildcard.index]?.mark;
const savedMark = selections[wildcard.index]?.mark ?? '';
const wildcardId = this._createWildcardIdentifier(wildcard);
// Look for mark by identifier first
let savedMark = '';
let pendingMark = this.operations.pendingSelections[wildcard.index]?.mark;
// Search for mark by identifier
for (const [selectionIndex, selectionData] of Object.entries(selections)) {
const selectionId = this._getWildcardIdentifierFromSelections(selections, selectionIndex);
if (selectionId === wildcardId && selectionData.mark) {
savedMark = selectionData.mark;
break;
}
}
// Fallback to direct index
if (!savedMark && selections[wildcard.index]?.mark) {
savedMark = selections[wildcard.index].mark;
}
const markIcon = document.createElement('span');
markIcon.className = 'wildcard-mark-icon';
@@ -184,26 +218,20 @@ class WildcardManager {
markIcon.onclick = (e) => {
e.stopPropagation();
// Check if there's already a tooltip for THIS specific wildcard
const existingTooltip = document.querySelector('.wildcard-mark-tooltip');
const isThisWildcard = existingTooltip &&
existingTooltip.dataset.wildcardIndex === wildcard.index.toString();
if (existingTooltip) {
if (isThisWildcard) {
// Close if it's the same wildcard
console.log('Closing tooltip for same wildcard');
this._closeActiveOverlays();
return;
} else {
// Close existing and open new one for different wildcard
console.log('Closing tooltip for different wildcard, opening new one');
this._closeActiveOverlays();
}
}
console.log('Showing new tooltip');
this.showMarkTooltip(container, wildcard, pendingMark);
this.showMarkTooltip(container, wildcard, pendingMark || savedMark);
};
row.appendChild(dropdown);
@@ -229,13 +257,9 @@ class WildcardManager {
}
showMarkTooltip(container, wildcard, markValue = '') {
console.log('showMarkTooltip called, activeOverlay before:', this.activeOverlay);
// Check if tooltip is already showing for this wildcard - improved check
if (this.activeOverlay &&
this.activeOverlay.classList.contains('wildcard-mark-tooltip') &&
this.activeOverlay.parentNode) {
console.log('Found existing tooltip, closing');
this._closeActiveOverlays();
return;
}
@@ -250,13 +274,25 @@ class WildcardManager {
tooltip.className = 'wildcard-mark-tooltip';
tooltip.dataset.wildcardIndex = wildcard.index;
// Set activeOverlay IMMEDIATELY after creating tooltip
this.activeOverlay = tooltip;
console.log('Set activeOverlay to:', this.activeOverlay);
const selections = JSON.parse(this.operations.originalSelections || '{}');
const wildcardId = this._createWildcardIdentifier(wildcard);
// Find mark by identifier or index
let savedMark = '';
for (const [selectionIndex, selectionData] of Object.entries(selections)) {
const selectionId = this._getWildcardIdentifierFromSelections(selections, selectionIndex);
if (selectionId === wildcardId && selectionData.mark) {
savedMark = selectionData.mark;
break;
}
}
if (!savedMark) {
savedMark = selections[wildcard.index]?.mark ?? '';
}
const pendingMark = this.operations.pendingSelections[wildcard.index]?.mark;
const savedMark = selections[wildcard.index]?.mark ?? '';
const input = document.createElement('input');
input.type = 'text';
@@ -277,10 +313,11 @@ class WildcardManager {
const addMark = () => {
let val = input.value.replace(/[^a-zA-Z]/g, '').toUpperCase();
this.operations.updatePendingMark(wildcard.index, val);
// MODIFIED: Store wildcard position and content for stable identification
this.operations.updatePendingMark(wildcard.index, val, wildcard);
this.activeOverlay = null;
input.blur();
tooltip.classList.remove('show');
setTimeout(() => {
tooltip.remove();
@@ -288,17 +325,31 @@ class WildcardManager {
if (markIcon) {
const selections = JSON.parse(this.operations.originalSelections || '{}');
const pendingMark = this.operations.pendingSelections[wildcard.index]?.mark;
const savedMark = selections[wildcard.index]?.mark ?? '';
// Find saved mark by identifier
let savedMark = '';
const wildcardId = this._createWildcardIdentifier(wildcard);
for (const [selectionIndex, selectionData] of Object.entries(selections)) {
const selectionId = this._getWildcardIdentifierFromSelections(selections, selectionIndex);
if (selectionId === wildcardId && selectionData.mark) {
savedMark = selectionData.mark;
break;
}
}
if (!savedMark) {
savedMark = selections[wildcard.index]?.mark ?? '';
}
this._setMarkIconState(markIcon, pendingMark, savedMark);
}
}, 200);
};
const deleteMark = () => {
this.operations.updatePendingMark(wildcard.index, '');
this.operations.updatePendingMark(wildcard.index, '', wildcard);
this.activeOverlay = null;
input.blur();
tooltip.classList.remove('show');
setTimeout(() => {
tooltip.remove();
@@ -306,7 +357,21 @@ class WildcardManager {
if (markIcon) {
const selections = JSON.parse(this.operations.originalSelections || '{}');
const pendingMark = this.operations.pendingSelections[wildcard.index]?.mark;
const savedMark = selections[wildcard.index]?.mark ?? '';
// Find saved mark by identifier
let savedMark = '';
const wildcardId = this._createWildcardIdentifier(wildcard);
for (const [selectionIndex, selectionData] of Object.entries(selections)) {
const selectionId = this._getWildcardIdentifierFromSelections(selections, selectionIndex);
if (selectionId === wildcardId && selectionData.mark) {
savedMark = selectionData.mark;
break;
}
}
if (!savedMark) {
savedMark = selections[wildcard.index]?.mark ?? '';
}
this._setMarkIconState(markIcon, pendingMark, savedMark);
}
}, 200);
@@ -325,14 +390,13 @@ class WildcardManager {
const icon = container.querySelector('.wildcard-mark-icon');
const modal = document.querySelector('.text-editor-modal');
// Append to modal to avoid clipping by sidebar overflow
modal.appendChild(tooltip);
// Add scroll listener to hide tooltip when scrolling
const sidebarContent = document.querySelector('.wildcard-dropdowns');
const handleScroll = () => {
if (tooltip && tooltip.parentNode) {
this.activeOverlay = null;
input.blur();
tooltip.classList.remove('show');
setTimeout(() => {
tooltip.remove();
@@ -349,7 +413,6 @@ class WildcardManager {
const iconRect = icon.getBoundingClientRect();
const modalRect = modal.getBoundingClientRect();
// Position relative to modal
tooltip.style.position = 'absolute';
tooltip.style.left = `${iconRect.right - modalRect.left - tooltip.offsetWidth}px`;
tooltip.style.top = `${iconRect.top - modalRect.top - tooltip.offsetHeight - 4}px`;
@@ -357,12 +420,16 @@ class WildcardManager {
requestAnimationFrame(() => {
tooltip.classList.add('show');
requestAnimationFrame(() => {
input.focus();
});
});
}, 0);
const handleClickOutside = (event) => {
if (!tooltip.contains(event.target) && event.target !== icon) {
this.activeOverlay = null;
input.blur();
tooltip.classList.remove('show');
setTimeout(() => {
tooltip.remove();
@@ -447,10 +514,6 @@ class WildcardManager {
return container;
}
_toggleCustomDropdown(container) {
return container;
}
_toggleCustomDropdown(container) {
const isOpen = container.classList.contains('open');
@@ -539,7 +602,8 @@ class WildcardManager {
async handleSelectionChange(wildcard, selectedValue, selectedIndex) {
try {
this.operations.updatePendingSelection(wildcard.index, selectedValue, wildcard.original);
// MODIFIED: Include position data
this.operations.updatePendingSelection(wildcard.index, selectedValue, wildcard.original, wildcard.position);
this.updateChildrenVisibility(wildcard, selectedIndex, false);
} catch (error) {
@@ -747,9 +811,14 @@ class WildcardManager {
try {
const pendingMarks = {};
Object.keys(this.operations.pendingSelections).forEach(wildcardIndex => {
const markValue = this.operations.pendingSelections[wildcardIndex]?.mark;
const selectionData = this.operations.pendingSelections[wildcardIndex];
const markValue = selectionData?.mark;
if (markValue !== undefined) {
pendingMarks[wildcardIndex] = markValue;
pendingMarks[wildcardIndex] = {
mark: markValue,
original: selectionData.original,
position: selectionData.position
};
}
});
@@ -762,7 +831,7 @@ class WildcardManager {
const section = container.querySelector(`[data-wildcard-index="${wildcardIndex}"]`);
const markIcon = section?.querySelector('.wildcard-mark-icon');
if (markIcon) {
const savedMark = pendingMarks[wildcardIndex];
const savedMark = pendingMarks[wildcardIndex].mark;
this._setMarkIconState(markIcon, undefined, savedMark);
}
});
@@ -773,26 +842,62 @@ class WildcardManager {
}
async _savePendingSelections() {
// Group updates: first do selections, then do marks to ensure marks are preserved
const selectionUpdates = [];
const markUpdates = [];
for (const [wildcardIndex, selectionData] of Object.entries(this.operations.pendingSelections)) {
const updateData = {
wildcard_index: wildcardIndex,
selected_value: selectionData.selected,
original_wildcard: selectionData.original,
wildcards_selections: this.textLoaderInstance.getHiddenWidgetValue("wildcards_selections")
};
// UPDATED: Ensure mark value is sent correctly
if (selectionData.mark !== undefined) {
updateData.mark_value = selectionData.mark;
// If there's a selection change, add it to selection updates
if (selectionData.selected !== undefined) {
selectionUpdates.push({
wildcard_index: wildcardIndex,
selected_value: selectionData.selected,
original_wildcard: selectionData.original,
wildcard_position: selectionData.position,
wildcard_content: selectionData.original
});
}
// If there's a mark change, add it to mark updates
if (selectionData.mark !== undefined) {
markUpdates.push({
wildcard_index: wildcardIndex,
selected_value: selectionData.selected || '',
original_wildcard: selectionData.original,
mark_value: selectionData.mark,
wildcard_position: selectionData.position,
wildcard_content: selectionData.original
});
}
}
let latestSelectionsJson = this.textLoaderInstance.getHiddenWidgetValue("wildcards_selections");
// Process selection updates first
for (const updateData of selectionUpdates) {
updateData.wildcards_selections = latestSelectionsJson;
const response = await fetchSend(this.constants.MESSAGE_ROUTE, this.node.id, "update_wildcard_selection", updateData);
if (response?.status === "success" && response.selections_json) {
this.textLoaderInstance.updateHiddenWidget("wildcards_selections", response.selections_json);
this.operations.originalSelections = response.selections_json;
latestSelectionsJson = response.selections_json;
}
}
// Process mark updates second, ensuring we preserve all existing marks
for (const updateData of markUpdates) {
updateData.wildcards_selections = latestSelectionsJson;
const response = await fetchSend(this.constants.MESSAGE_ROUTE, this.node.id, "update_wildcard_selection", updateData);
if (response?.status === "success" && response.selections_json) {
latestSelectionsJson = response.selections_json;
}
}
// Update with the final selections JSON that contains all changes
this.textLoaderInstance.updateHiddenWidget("wildcards_selections", latestSelectionsJson);
this.operations.originalSelections = latestSelectionsJson;
}
_flashSavedLabels(container) {
@@ -834,6 +939,24 @@ class WildcardManager {
_updateWildcardStructure(wildcards) {
this.lastWildcardStructure = this._extractWildcardStructure(wildcards);
}
// NEW: Create stable wildcard identifier
_createWildcardIdentifier(wildcard) {
// Combine original content and position for a stable identifier
const content = wildcard.original || '';
const position = wildcard.position || 0;
// Create a hash-like identifier that's stable regardless of index changes
return `${content}@${position}`;
}
// NEW: Get wildcard identifier from selections
_getWildcardIdentifierFromSelections(selections, wildcardIndex) {
const selection = selections[wildcardIndex];
if (selection && selection.original !== undefined && selection.position !== undefined) {
return `${selection.original}@${selection.position}`;
}
return null;
}
}
class Operations {
@@ -856,7 +979,7 @@ class Operations {
this.hasUnsavedTextChanges = currentText !== this.originalText;
}
updatePendingSelection(wildcardIndex, selectedValue, originalWildcard) {
updatePendingSelection(wildcardIndex, selectedValue, originalWildcard, wildcardPosition) {
let existingMark;
const pendingMark = this.pendingSelections[wildcardIndex]?.mark;
@@ -865,7 +988,23 @@ class Operations {
} else {
try {
const selections = JSON.parse(this.originalSelections || '{}');
existingMark = selections[wildcardIndex]?.mark;
// NEW: Search by identifier first
const wildcardId = `${originalWildcard}@${wildcardPosition}`;
for (const [selectionIndex, selectionData] of Object.entries(selections)) {
const selectionId = selectionData.original && selectionData.position !== undefined
? `${selectionData.original}@${selectionData.position}`
: null;
if (selectionId === wildcardId && selectionData.mark) {
existingMark = selectionData.mark;
break;
}
}
// Fallback to direct index lookup
if (existingMark === undefined) {
existingMark = selections[wildcardIndex]?.mark;
}
} catch (e) {
existingMark = undefined;
}
@@ -877,6 +1016,7 @@ class Operations {
this.pendingSelections[wildcardIndex].selected = selectedValue;
this.pendingSelections[wildcardIndex].original = originalWildcard;
this.pendingSelections[wildcardIndex].position = wildcardPosition; // NEW: Store position
if (existingMark !== undefined && existingMark !== '') {
this.pendingSelections[wildcardIndex].mark = existingMark;
@@ -885,24 +1025,51 @@ class Operations {
this.hasUnsavedSelectionChanges = true;
}
updatePendingMark(wildcardIndex, markValue) {
// MODIFIED: Update to include wildcard metadata
updatePendingMark(wildcardIndex, markValue, wildcard = null) {
let originalMark = '';
let selections = {};
try {
selections = JSON.parse(this.originalSelections || '{}');
originalMark = selections[wildcardIndex]?.mark || '';
// NEW: Search by identifier if wildcard provided
if (wildcard) {
const wildcardId = `${wildcard.original}@${wildcard.position}`;
for (const [selectionIndex, selectionData] of Object.entries(selections)) {
const selectionId = selectionData.original && selectionData.position !== undefined
? `${selectionData.original}@${selectionData.position}`
: null;
if (selectionId === wildcardId && selectionData.mark) {
originalMark = selectionData.mark;
break;
}
}
}
// Fallback to direct lookup
if (!originalMark) {
originalMark = selections[wildcardIndex]?.mark || '';
}
} catch (e) {}
if (!this.pendingSelections[wildcardIndex]) {
const savedSelection = selections[wildcardIndex]?.selected || '';
const savedOriginal = selections[wildcardIndex]?.original || '';
const savedOriginal = wildcard?.original || selections[wildcardIndex]?.original || '';
const savedPosition = wildcard?.position ?? selections[wildcardIndex]?.position;
this.pendingSelections[wildcardIndex] = {
selected: savedSelection,
original: savedOriginal
original: savedOriginal,
position: savedPosition // NEW: Store position
};
}
// NEW: Update position if wildcard provided
if (wildcard) {
this.pendingSelections[wildcardIndex].original = wildcard.original;
this.pendingSelections[wildcardIndex].position = wildcard.position;
}
this.pendingSelections[wildcardIndex].mark = markValue;
this.hasUnsavedSelectionChanges = (markValue !== originalMark) ||
@@ -939,7 +1106,19 @@ class Operations {
currentSelections = {};
}
Object.assign(currentSelections, this.pendingSelections);
// FIXED: Merge each pending selection individually to preserve existing marks
for (const [wildcardIndex, pendingData] of Object.entries(this.pendingSelections)) {
if (!currentSelections[wildcardIndex]) {
currentSelections[wildcardIndex] = {};
}
// Merge the pending data with existing data
currentSelections[wildcardIndex] = {
...currentSelections[wildcardIndex],
...pendingData
};
}
this.textLoaderInstance.updateHiddenWidget("wildcards_selections", JSON.stringify(currentSelections));
this.originalSelections = JSON.stringify(currentSelections);
@@ -1056,6 +1235,10 @@ class ButtonManager {
return this.elements.createButton('text-editor-save-button', 'Save', () => this.handleSave());
}
createClearButton() {
return this.elements.createButton('text-editor-clear-button', 'Clear', () => this.handleClear());
}
createResetButton() {
return this.elements.createButton('wildcard-reset-button', 'Reset', () => this.handleReset());
}
@@ -1067,11 +1250,6 @@ class ButtonManager {
async handleSave() {
const textarea = this.elements.elements.textarea;
const saveButton = document.querySelector('.text-editor-save-button');
if (!textarea.value.trim()) {
alert('Empty content is not allowed');
return;
}
this.setButtonState(saveButton, 'Saving...', true);
@@ -1098,6 +1276,43 @@ class ButtonManager {
}
}
async handleClear() {
if (window.confirm('Are you sure you want to clear all text content? This will remove all text and wildcards.')) {
const clearButton = document.querySelector('.text-editor-clear-button');
this.setButtonState(clearButton, 'Clearing...', true);
try {
// Clear the textarea
const textarea = this.elements.elements.textarea;
textarea.value = '';
// Save the empty content
await this.operations.saveContent('');
// Update UI to show no wildcards
this.wildcardManager.createWildcardUI(this.elements.elements.wildcardDropdowns, []);
this.elements.autoExpandSidebar(0);
// Show notification
this.wildcardManager.showNotification(
this.elements.elements.wildcardDropdowns,
'Text content cleared'
);
this.setButtonState(clearButton, 'Cleared!', true);
setTimeout(() => {
this.setButtonState(clearButton, 'Clear', false);
}, 1000);
} catch (error) {
console.error('Error clearing content:', error);
alert('Error clearing content');
this.setButtonState(clearButton, 'Clear', false);
}
}
}
async handleSaveSelections() {
try {
await this.wildcardManager.saveSelections(this.elements.elements.wildcardDropdowns);
@@ -1204,8 +1419,18 @@ export function createTextEditorModal(node, textContent, constants, textLoaderIn
wildcardControls.insertBefore(sidebarButtonsContainer, elements.elements.wildcardDropdowns);
elements.elements.sidebarContent.appendChild(wildcardControls);
// MODIFIED: Create main button container with both Clear and Save buttons
const mainButtonsContainer = document.createElement('div');
Object.assign(mainButtonsContainer.style, {
display: 'flex',
gap: '10px'
});
mainButtonsContainer.appendChild(buttonManager.createClearButton());
mainButtonsContainer.appendChild(buttonManager.createSaveButton());
buttonContainer.appendChild(document.createElement('div'));
buttonContainer.appendChild(buttonManager.createSaveButton());
buttonContainer.appendChild(mainButtonsContainer);
mainSection.appendChild(buttonContainer);
contentDiv.appendChild(mainSection);