Files

586 lines
20 KiB
JavaScript

export async function showPinterestModal(node, constants, fetchSend, mappings) {
const modal = new PinterestModal(node, constants, fetchSend, mappings);
return await modal.show();
}
class SpinnerManager {
constructor() {
this.spinnerOverlay = null;
this.spinnerStyleId = 'pinterest-spinner-styles';
this.spinnerTimeout = null;
this.keydownHandler = null;
}
showSpinner(delay = 500) {
this.spinnerTimeout = setTimeout(() => {
this.createSpinnerOverlay();
this.addSpinnerStyles();
document.body.appendChild(this.spinnerOverlay);
this.addEscKeyHandler();
}, delay);
}
hideSpinner() {
if (this.spinnerTimeout) {
clearTimeout(this.spinnerTimeout);
this.spinnerTimeout = null;
}
this.removeEscKeyHandler();
this.removeSpinnerOverlay();
this.removeSpinnerStyles();
}
createSpinnerOverlay() {
this.spinnerOverlay = document.createElement("div");
this.spinnerOverlay.id = "pinterest-spinner-overlay";
const loadingContainer = this.createLoadingContainer();
this.spinnerOverlay.appendChild(loadingContainer);
}
createLoadingContainer() {
const container = document.createElement("div");
container.className = "modal-loading-container";
const spinner = document.createElement("div");
spinner.className = "modal-loading-spinner";
container.appendChild(spinner);
return container;
}
removeSpinnerOverlay() {
if (this.spinnerOverlay) {
this.spinnerOverlay.remove();
this.spinnerOverlay = null;
}
}
showError(errorMessage) {
console.error("Pinterest Modal Error:", errorMessage);
const loadingContainer = this.spinnerOverlay?.querySelector('.modal-loading-container');
if (loadingContainer) {
loadingContainer.innerHTML = `
<div class="modal-loading-error">${errorMessage}</div>
<div class="modal-error-actions">
<button class="modal-close-button">Close</button>
</div>
`;
// Add close button functionality
const closeButton = loadingContainer.querySelector('.modal-close-button');
closeButton.addEventListener('click', () => {
this.hideSpinner();
});
}
}
addEscKeyHandler() {
this.keydownHandler = (event) => {
if (event.key === 'Escape') {
this.hideSpinner();
}
};
document.addEventListener('keydown', this.keydownHandler);
}
removeEscKeyHandler() {
if (this.keydownHandler) {
document.removeEventListener('keydown', this.keydownHandler);
this.keydownHandler = null;
}
}
addSpinnerStyles() {
if (document.getElementById(this.spinnerStyleId)) {
return;
}
const style = document.createElement('style');
style.id = this.spinnerStyleId;
style.textContent = this.getSpinnerStyles();
document.head.appendChild(style);
}
removeSpinnerStyles() {
const style = document.getElementById(this.spinnerStyleId);
if (style) {
style.remove();
}
}
getSpinnerStyles() {
return `
@keyframes modal-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#pinterest-spinner-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
}
.modal-loading-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px;
background: rgb(25, 25, 25);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
width: 200px;
min-height: 120px;
}
.modal-loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid rgb(25, 25, 25);
border-radius: 50%;
animation: modal-spin 1s linear infinite;
}
.modal-loading-error {
color: #ff6b6b;
font-size: 14px;
text-align: center;
margin-bottom: 15px;
}
.modal-error-actions {
margin-top: 15px;
}
.modal-close-button {
background: #ff6b6b;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s;
}
.modal-close-button:hover {
background: #ff5252;
}
`;
}
}
class ModalUIBuilder {
constructor(extensionName) {
this.extensionName = extensionName;
}
createOverlay() {
const existingOverlay = document.getElementById("pinterest-modal-overlay");
if (existingOverlay) {
existingOverlay.remove();
}
const overlay = document.createElement("div");
overlay.id = "pinterest-modal-overlay";
return overlay;
}
createModal() {
const modal = document.createElement("div");
modal.id = "pinterest-modal";
return modal;
}
async ensureCSSLoaded(extensionName) {
const cssPath = `/extensions/${extensionName}/common/css/dn_pinterest_modal.css`;
if (document.querySelector(`link[href="${cssPath}"]`)) {
return;
}
return new Promise(resolve => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = cssPath;
link.onload = resolve;
link.onerror = () => {
console.warn(`Failed to load CSS: ${cssPath}`);
resolve();
};
document.head.appendChild(link);
});
}
}
class OverlayEventManager {
constructor(overlay, onClose) {
this.overlay = overlay;
this.onClose = onClose;
this.mouseDownOnOverlay = false;
}
setupEventListeners() {
this.overlay.addEventListener("mousedown", (event) => {
this.mouseDownOnOverlay = event.target === this.overlay;
});
this.overlay.addEventListener("mouseup", (event) => {
if (this.mouseDownOnOverlay && event.target === this.overlay) {
this.onClose();
}
this.mouseDownOnOverlay = false;
});
// Add ESC key listener for closing modal
this.keydownHandler = (event) => {
if (event.key === 'Escape') {
this.onClose();
}
};
document.addEventListener('keydown', this.keydownHandler);
}
removeEventListeners() {
if (this.keydownHandler) {
document.removeEventListener('keydown', this.keydownHandler);
this.keydownHandler = null;
}
}
}
export class PinterestModal {
constructor(node, constants, fetchSend, mappings) {
this.node = node;
this.constants = constants;
this.fetchSend = fetchSend;
this.mappings = mappings;
this.spinnerManager = new SpinnerManager();
this.uiBuilder = new ModalUIBuilder(constants.EXTENSION_NAME);
this.overlay = null;
this.modal = null;
this.imageGrid = null;
this.pinUrls = [];
this.loadedImages = new Set();
this.intersectionObserver = null;
this.selectedImageUrl = null;
this.imagesPerBatch = 20; // Number of images to load initially and per batch
this.overlayEventManager = null;
}
async show() {
this.spinnerManager.showSpinner();
try {
// Get configs from node
const configsWidget = this.node.widgets.find(w => w.name === "node_configs");
const configs = configsWidget ? JSON.parse(configsWidget.value) : {};
// Debug: Log configs being read by modal
console.log("Modal reading configs:", configs);
// Extract required values from configs
const username = configs.username || "";
let board = configs.board;
let section = configs.section;
const api_requests = configs.api_requests;
// Debug: Log values being sent to backend
console.log("Modal sending to backend - username:", username, "board:", board, "section:", section, "api_requests:", api_requests);
// Convert board display name to actual name if needed
if (board && board !== "all" && this.mappings && this.mappings.displayToBoardName) {
console.log("Modal board before conversion:", board);
console.log("Modal available board mappings:", this.mappings.displayToBoardName);
board = this.mappings.displayToBoardName[board] || board;
console.log("Modal converted board to:", board);
}
// Convert section display name to actual name if needed
if (section && section !== "included" && section !== "excluded" && this.mappings && this.mappings.displayToSectionName) {
console.log("Modal section before conversion:", section);
console.log("Modal available section mappings:", this.mappings.displayToSectionName);
section = this.mappings.displayToSectionName[section] || section;
console.log("Modal converted section to:", section);
}
// Get the pinterest pins from backend
const response = await this.fetchSend(this.constants.MESSAGE_ROUTE, this.node.id, "get_pinterest_pins", {
username: username,
board_name: board,
section: section,
api_requests: api_requests
});
this.pinUrls = response.pin_urls;
console.log("Pin URLs:", this.pinUrls.length);
// Check if the array is empty and show error message
if (!this.pinUrls || this.pinUrls.length === 0) {
console.log("No images found, showing error message");
this.spinnerManager.showError("No images found for the selected board/section");
// Auto-close spinner after 3 seconds on error
setTimeout(() => {
this.spinnerManager.hideSpinner();
}, 3000);
return null;
}
this.spinnerManager.hideSpinner();
await this.loadResourcesAndInitialize();
// Return a promise that resolves when an image is selected or modal is closed
return new Promise((resolve) => {
this.imageSelectionResolve = resolve;
});
} catch (error) {
console.error("Error loading modal resources:", error);
this.spinnerManager.showError("Error loading modal");
// Auto-close spinner after 3 seconds on error
setTimeout(() => {
this.spinnerManager.hideSpinner();
}, 3000);
return null;
}
}
async loadResourcesAndInitialize() {
await this.uiBuilder.ensureCSSLoaded(this.constants.EXTENSION_NAME);
this.spinnerManager.hideSpinner();
this.createModalComponents();
this.setupEventHandlers();
this.setupIntersectionObserver();
this.initializeImageGrid();
document.body.appendChild(this.overlay);
}
createModalComponents() {
this.overlay = this.uiBuilder.createOverlay();
this.modal = this.uiBuilder.createModal();
this.overlay.appendChild(this.modal);
// Create modal content
this.modalContent = document.createElement('div');
this.modalContent.className = 'pinterest-modal-content';
this.modal.appendChild(this.modalContent);
}
setupEventHandlers() {
this.overlayEventManager = new OverlayEventManager(this.overlay, () => this.close());
this.overlayEventManager.setupEventListeners();
}
initializeImageGrid() {
this.imageGrid = document.createElement('div');
this.imageGrid.className = 'pinterest-image-grid';
this.modalContent.appendChild(this.imageGrid);
// Create shadow elements
this.shadowTop = document.createElement('div');
this.shadowTop.className = 'pinterest-shadow-top';
this.shadowBottom = document.createElement('div');
this.shadowBottom.className = 'pinterest-shadow-bottom';
this.modalContent.appendChild(this.shadowTop);
this.modalContent.appendChild(this.shadowBottom);
// Add scroll event listener as a backup for fast scrolling
this.imageGrid.addEventListener('scroll', this.handleScroll.bind(this), { passive: true });
// Load initial batch of images
this.loadImageBatch(0, this.imagesPerBatch);
}
setupIntersectionObserver() {
this.intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const index = parseInt(entry.target.dataset.index);
this.loadImage(index);
}
});
}, {
root: this.imageGrid,
rootMargin: '200px', // Increased margin for earlier loading
threshold: 0.01 // Lower threshold for earlier detection
});
}
handleScroll() {
// Throttle scroll events
if (this.scrollTimeout) {
return;
}
this.scrollTimeout = setTimeout(() => {
this.checkVisiblePlaceholders();
this.scrollTimeout = null;
}, 100);
}
checkVisiblePlaceholders() {
const containers = this.imageGrid.querySelectorAll('.pinterest-image-container:not([data-loaded])');
const gridRect = this.imageGrid.getBoundingClientRect();
containers.forEach(container => {
const rect = container.getBoundingClientRect();
// Check if container is visible or close to visible
if (rect.top < gridRect.bottom + 300 && rect.bottom > gridRect.top - 300) {
const index = parseInt(container.dataset.index);
if (!this.loadedImages.has(index)) {
this.loadImage(index);
}
}
});
}
loadImageBatch(startIndex, count) {
const endIndex = Math.min(startIndex + count, this.pinUrls.length);
for (let i = startIndex; i < endIndex; i++) {
this.createImagePlaceholder(i);
}
}
createImagePlaceholder(index) {
const container = document.createElement('div');
container.className = 'pinterest-image-container';
container.dataset.index = index;
const placeholder = document.createElement('div');
placeholder.className = 'pinterest-image-loading';
container.appendChild(placeholder);
this.imageGrid.appendChild(container);
// Observe this placeholder for lazy loading
this.intersectionObserver.observe(container);
}
loadImage(index) {
if (this.loadedImages.has(index)) return;
const container = this.imageGrid.querySelector(`[data-index="${index}"]`);
if (!container) return;
// Mark as loading to prevent duplicate loading
if (container.dataset.loading === 'true') return;
container.dataset.loading = 'true';
const imageUrl = this.pinUrls[index];
const img = document.createElement('img');
img.className = 'pinterest-image';
img.dataset.index = index;
img.onload = () => {
const placeholder = container.querySelector('.pinterest-image-loading');
if (placeholder) {
placeholder.replaceWith(img);
}
// Set the container height to match the image height exactly
const renderedHeight = img.offsetHeight;
container.style.height = `${renderedHeight}px`;
// Calculate row span for grid layout (minimum 1)
const rowSpan = Math.max(1, Math.ceil(renderedHeight / 10));
container.style.gridRowEnd = `span ${rowSpan}`;
this.loadedImages.add(index);
container.dataset.loaded = 'true';
delete container.dataset.loading;
// Load more images if we're near the end
if (this.loadedImages.size % this.imagesPerBatch === 0 &&
this.loadedImages.size < this.pinUrls.length) {
this.loadImageBatch(this.loadedImages.size, this.imagesPerBatch);
}
};
img.onerror = () => {
console.error(`Failed to load image: ${imageUrl}`);
// Remove the placeholder on error
container.remove();
delete container.dataset.loading;
};
img.src = imageUrl;
// Add click handler
img.addEventListener('click', () => this.selectImage(imageUrl, container));
}
selectImage(imageUrl, container) {
// Remove previous selection
const prevSelected = this.imageGrid.querySelector('.pinterest-image-selected');
if (prevSelected) {
prevSelected.classList.remove('pinterest-image-selected');
}
// Add selection to current image
container.classList.add('pinterest-image-selected');
this.selectedImageUrl = imageUrl;
console.log(`Selected image: ${imageUrl}`);
// Close the modal after selection and return the selected URL
setTimeout(() => this.close(this.selectedImageUrl), 300);
}
close(selectedImageUrl = null) {
this.spinnerManager.hideSpinner();
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
// Clean up scroll event listener
if (this.imageGrid && this.scrollTimeout) {
this.imageGrid.removeEventListener('scroll', this.handleScroll);
clearTimeout(this.scrollTimeout);
}
// Remove event listeners
if (this.overlayEventManager) {
this.overlayEventManager.removeEventListeners();
}
if (this.overlay) {
this.overlay.remove();
}
// Remove shadow elements
if (this.shadowTop) {
this.shadowTop.remove();
}
if (this.shadowBottom) {
this.shadowBottom.remove();
}
// Resolve the promise with the selected image URL if provided
if (this.imageSelectionResolve) {
this.imageSelectionResolve(selectedImageUrl);
}
}
}