diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js
index 14cc4c584f..26a43dbefc 100644
--- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js
+++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js
@@ -770,7 +770,7 @@ function RemoteFunctions(config = {}) {
let rect = element.getBoundingClientRect();
marker.style.position = "fixed";
- marker.style.zIndex = "2147483646";
+ marker.style.zIndex = "2147483647";
marker.style.borderRadius = "2px";
marker.style.pointerEvents = "none";
@@ -1159,6 +1159,7 @@ function RemoteFunctions(config = {}) {
_clearDropMarkers();
window._currentDraggedElement = this.element;
dismissUIAndCleanupState();
+ dismissImageRibbonGallery();
// Add drag image styling
event.dataTransfer.effectAllowed = "move";
});
@@ -1273,52 +1274,52 @@ function RemoteFunctions(config = {}) {
const styles = `
:host {
- all: initial;
+ all: initial !important;
}
.phoenix-more-options-box {
- background-color: #4285F4;
- color: white;
- border-radius: 3px;
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
- font-size: 12px;
- font-family: Arial, sans-serif;
- z-index: 2147483647;
- position: absolute;
+ background-color: #4285F4 !important;
+ color: white !important;
+ border-radius: 3px !important;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) !important;
+ font-size: 12px !important;
+ font-family: Arial, sans-serif !important;
+ z-index: 2147483647 !important;
+ position: absolute !important;
left: -1000px;
top: -1000px;
- box-sizing: border-box;
+ box-sizing: border-box !important;
}
.node-options {
- display: flex;
- align-items: center;
+ display: flex !important;
+ align-items: center !important;
}
.node-options span {
- padding: 4px 3.9px;
- cursor: pointer;
- display: flex;
- align-items: center;
- border-radius: 0;
+ padding: 4px 3.9px !important;
+ cursor: pointer !important;
+ display: flex !important;
+ align-items: center !important;
+ border-radius: 0 !important;
}
.node-options span:first-child {
- border-radius: 3px 0 0 3px;
+ border-radius: 3px 0 0 3px !important;
}
.node-options span:last-child {
- border-radius: 0 3px 3px 0;
+ border-radius: 0 3px 3px 0 !important;
}
.node-options span:hover {
- background-color: rgba(255, 255, 255, 0.15);
+ background-color: rgba(255, 255, 255, 0.15) !important;
}
.node-options span > svg {
- width: 16px;
- height: 16px;
- display: block;
+ width: 16px !important;
+ height: 16px !important;
+ display: block !important;
}
`;
@@ -1535,37 +1536,37 @@ function RemoteFunctions(config = {}) {
const styles = `
:host {
- all: initial;
+ all: initial !important;
}
.phoenix-node-info-box {
- background-color: #4285F4;
- color: white;
- border-radius: 3px;
- padding: 5px 8px;
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
- font-size: 12px;
- font-family: Arial, sans-serif;
- z-index: 2147483647;
- position: absolute;
+ background-color: #4285F4 !important;
+ color: white !important;
+ border-radius: 3px !important;
+ padding: 5px 8px !important;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) !important;
+ font-size: 12px !important;
+ font-family: Arial, sans-serif !important;
+ z-index: 2147483647 !important;
+ position: absolute !important;
left: ${leftPos}px;
top: -1000px;
- max-width: 300px;
- box-sizing: border-box;
- pointer-events: none;
+ max-width: 300px !important;
+ box-sizing: border-box !important;
+ pointer-events: none !important;
}
.tag-name {
- font-weight: bold;
+ font-weight: bold !important;
}
.id-name,
.class-name {
- margin-top: 3px;
+ margin-top: 3px !important;
}
.exceeded-classes {
- opacity: 0.8;
+ opacity: 0.8 !important;
}
`;
@@ -1682,96 +1683,96 @@ function RemoteFunctions(config = {}) {
const styles = `
:host {
- all: initial;
+ all: initial !important;
}
.phoenix-ai-prompt-box {
- position: absolute;
- background: white;
- border: 1px solid #4285F4;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- font-family: Arial, sans-serif;
- z-index: 2147483647;
- width: ${boxWidth}px;
- padding: 0;
- box-sizing: border-box;
+ position: absolute !important;
+ background: white !important;
+ border: 1px solid #4285F4 !important;
+ border-radius: 8px !important;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important;
+ font-family: Arial, sans-serif !important;
+ z-index: 2147483647 !important;
+ width: ${boxWidth}px !important;
+ padding: 0 !important;
+ box-sizing: border-box !important;
}
.phoenix-ai-prompt-input-container {
- position: relative;
+ position: relative !important;
}
.phoenix-ai-prompt-textarea {
- width: 100%;
- height: ${boxHeight}px;
- border: none;
- border-radius: 8px;
- padding: 12px 40px 12px 16px;
- font-size: 14px;
- font-family: Arial, sans-serif;
- resize: none;
- outline: none;
- box-sizing: border-box;
- background: #f9f9f9;
+ width: 100% !important;
+ height: ${boxHeight}px !important;
+ border: none !important;
+ border-radius: 8px !important;
+ padding: 12px 40px 12px 16px !important;
+ font-size: 14px !important;
+ font-family: Arial, sans-serif !important;
+ resize: none !important;
+ outline: none !important;
+ box-sizing: border-box !important;
+ background: #f9f9f9 !important;
}
.phoenix-ai-prompt-textarea:focus {
- background: white;
+ background: white !important;
}
.phoenix-ai-prompt-textarea::placeholder {
- color: #999;
+ color: #999 !important;
}
.phoenix-ai-prompt-send-button {
- width: 28px;
- height: 28px;
- border: none;
- border-radius: 50%;
- background: #4285F4;
- color: white;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 14px;
- transition: background-color 0.2s;
- line-height: 0.5;
+ width: 28px !important;
+ height: 28px !important;
+ border: none !important;
+ border-radius: 50% !important;
+ background: #4285F4 !important;
+ color: white !important;
+ cursor: pointer !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ font-size: 14px !important;
+ transition: background-color 0.2s !important;
+ line-height: 0.5 !important;
}
.phoenix-ai-prompt-send-button:hover:not(:disabled) {
- background: #4285F4;
+ background: #4285F4 !important;
}
.phoenix-ai-prompt-send-button:disabled {
- background: #dadce0;
- color: #9aa0a6;
- cursor: not-allowed;
+ background: #dadce0 !important;
+ color: #9aa0a6 !important;
+ cursor: not-allowed !important;
}
.phoenix-ai-bottom-controls {
- border-top: 1px solid #e0e0e0;
- padding: 8px 16px;
- background: #f9f9f9;
- border-radius: 0 0 8px 8px;
- display: flex;
- align-items: center;
- justify-content: space-between;
+ border-top: 1px solid #e0e0e0 !important;
+ padding: 8px 16px !important;
+ background: #f9f9f9 !important;
+ border-radius: 0 0 8px 8px !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: space-between !important;
}
.phoenix-ai-model-select {
- padding: 4px 8px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 12px;
- background: white;
- outline: none;
- cursor: pointer;
+ padding: 4px 8px !important;
+ border: 1px solid #ddd !important;
+ border-radius: 4px !important;
+ font-size: 12px !important;
+ background: white !important;
+ outline: none !important;
+ cursor: pointer !important;
}
.phoenix-ai-model-select:focus {
- border-color: #4285F4;
+ border-color: #4285F4 !important;
}
`;
@@ -1911,6 +1912,996 @@ function RemoteFunctions(config = {}) {
}
};
+ // image ribbon gallery cache, to store the last query and its results
+ // then next time we can load it from cache itself instead of making a new API call
+ const _imageGalleryCache = {
+ currentQuery: null,
+ allImages: [],
+ totalPages: 1,
+ currentPage: 1,
+ maxImages: 50
+ };
+
+ /**
+ * when user clicks on an image we call this,
+ * this creates a image ribbon gallery at the bottom of the live preview
+ */
+ function ImageRibbonGallery(element) {
+ this.element = element;
+ this.remove = this.remove.bind(this);
+ this.currentPage = 1;
+ this.totalPages = 1;
+ this.allImages = [];
+ this.imagesPerPage = 10;
+ this.scrollPosition = 0;
+ this.maxWidth = '800px'; // when current image dimension is not defined we use this as unsplash images are very large
+ this.maxHeight = '600px';
+
+ this.create();
+ }
+
+ ImageRibbonGallery.prototype = {
+ _style: function () {
+ this.body = window.document.createElement("div");
+ this._shadow = this.body.attachShadow({mode: 'closed'});
+
+ this._shadow.innerHTML = `
+
+
+
+
+
‹
+
+
+ Loading images...
+
+
+
›
+
+
+ `;
+ },
+
+ _getDefaultQuery: function() {
+ // this are the default queries, so when image ribbon gallery is shown, we select a random query and show it
+ const qualityQueries = [
+ 'nature', 'minimal', 'workspace', 'abstract', 'coffee',
+ 'mountains', 'city', 'flowers', 'ocean', 'sunset',
+ 'architecture', 'forest', 'travel', 'technology', 'sky',
+ 'landscape', 'creative', 'design', 'art', 'modern',
+ 'food', 'patterns', 'colors', 'photography', 'studio',
+ 'light', 'winter', 'summer', 'vintage', 'geometric',
+ 'water', 'beach', 'space', 'garden', 'textures',
+ 'urban', 'portrait', 'music', 'books', 'home',
+ 'cozy', 'aesthetic', 'autumn', 'spring', 'clouds'
+ ];
+
+ const randIndex = Math.floor(Math.random() * qualityQueries.length);
+ return qualityQueries[randIndex];
+ },
+
+ _fetchImages: function(searchQuery, page = 1, append = false) {
+ this._currentSearchQuery = searchQuery;
+
+ if (!append && this._loadFromCache(searchQuery)) { // try cache first
+ return;
+ }
+ if (append && this._loadPageFromCache(searchQuery, page)) { // try to load new page from cache
+ return;
+ }
+ // if unable to load from cache, we make the API call
+ this._fetchFromAPI(searchQuery, page, append);
+ },
+
+ _fetchFromAPI: function(searchQuery, page, append) {
+ // when we fetch from API, we clear the cache and then store a fresh copy
+ if (searchQuery !== _imageGalleryCache.currentQuery) {
+ this._clearCache();
+ }
+
+ const apiUrl = `https://images.phcode.dev/api/images/search?q=${encodeURIComponent(searchQuery)}&per_page=10&page=${page}&safe=true`;
+
+ if (!append) {
+ this._showLoading();
+ }
+
+ fetch(apiUrl)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`API request failed: ${response.status}`);
+ }
+ return response.json();
+ })
+ .then(data => {
+ if (data.results && data.results.length > 0) {
+ if (append) {
+ this.allImages = this.allImages.concat(data.results);
+ this._renderImages(data.results, true); // true means need to append new images at the end
+ } else {
+ this.allImages = data.results;
+ this._renderImages(this.allImages, false); // false means its a new search
+ }
+ this.totalPages = data.total_pages || 1;
+ this.currentPage = page;
+ this._updateNavButtons();
+ this._updateSearchInput(searchQuery);
+ this._updateCache(searchQuery, data, append);
+ } else if (!append) {
+ this._showError('No images found');
+ }
+
+ if (append) {
+ this._isLoadingMore = false;
+ this._hideLoadingMore();
+ }
+ })
+ .catch(error => {
+ console.error('Failed to fetch images:', error);
+ if (!append) {
+ this._showError('Failed to load images');
+ } else {
+ this._isLoadingMore = false;
+ this._hideLoadingMore();
+ }
+ });
+ },
+
+ _updateCache: function(searchQuery, data, append) {
+ // Update cache with new data for current query
+ _imageGalleryCache.currentQuery = searchQuery;
+ _imageGalleryCache.totalPages = data.total_pages || 1;
+ _imageGalleryCache.currentPage = this.currentPage;
+
+ if (append) {
+ // Append new results to existing cache
+ const newImages = _imageGalleryCache.allImages.concat(data.results);
+
+ if (newImages.length > _imageGalleryCache.maxImages) { // max = 50
+ _imageGalleryCache.allImages = newImages.slice(-_imageGalleryCache.maxImages);
+ } else {
+ _imageGalleryCache.allImages = newImages;
+ }
+ } else {
+ // new search replace cache
+ _imageGalleryCache.allImages = data.results;
+ }
+ },
+
+ _clearCache: function() {
+ // clear current cache when switching to new query
+ _imageGalleryCache.currentQuery = null;
+ _imageGalleryCache.allImages = [];
+ _imageGalleryCache.totalPages = 1;
+ _imageGalleryCache.currentPage = 1;
+ },
+
+ _updateSearchInput: function(searchQuery) {
+ // write the current query in the search input
+ const searchInput = this._shadow.querySelector('.phoenix-ribbon-search input');
+ if (searchInput && searchQuery) {
+ searchInput.value = searchQuery;
+ searchInput.placeholder = searchQuery;
+ }
+ },
+
+ _loadFromCache: function(searchQuery) {
+ // Check if we can load from cache for this query
+ if (searchQuery === _imageGalleryCache.currentQuery && _imageGalleryCache.allImages.length > 0) {
+ this.allImages = _imageGalleryCache.allImages;
+ this.totalPages = _imageGalleryCache.totalPages;
+ this.currentPage = _imageGalleryCache.currentPage;
+
+ this._renderImages(this.allImages, false);
+ this._updateNavButtons();
+ this._updateSearchInput(searchQuery);
+ return true; // Successfully loaded from cache
+ }
+ return false; // unable to load from cache
+ },
+
+ _loadPageFromCache: function(searchQuery, page) {
+ // check if this page is in cache
+ if (searchQuery === _imageGalleryCache.currentQuery && page <= Math.ceil(_imageGalleryCache.allImages.length / 10)) {
+ const startIdx = (page - 1) * 10;
+ const endIdx = startIdx + 10;
+ const pageImages = _imageGalleryCache.allImages.slice(startIdx, endIdx);
+
+ if (pageImages.length > 0) {
+ this.allImages = this.allImages.concat(pageImages);
+ this._renderImages(pageImages, true);
+ this.currentPage = page;
+ this._updateNavButtons();
+ this._isLoadingMore = false;
+ this._hideLoadingMore();
+ return true; // Successfully loaded page from cache
+ }
+ }
+ return false;
+ },
+
+ _handleNavLeft: function() {
+ const container = this._shadow.querySelector('.phoenix-ribbon-strip');
+ if (!container) { return; }
+
+ const containerWidth = container.clientWidth;
+ const scrollAmount = containerWidth;
+
+ this.scrollPosition = Math.max(0, this.scrollPosition - scrollAmount);
+ container.scrollTo({ left: this.scrollPosition, behavior: 'smooth' });
+ this._updateNavButtons();
+ },
+
+ _handleNavRight: function() {
+ const container = this._shadow.querySelector('.phoenix-ribbon-strip');
+ if (!container) { return; }
+
+ const containerWidth = container.clientWidth;
+ const totalWidth = container.scrollWidth;
+ const scrollAmount = containerWidth;
+
+ // if we're near the end, we need to load more images
+ const nearEnd = (this.scrollPosition + containerWidth + scrollAmount) >= totalWidth - 100;
+ if (nearEnd && this.currentPage < this.totalPages && !this._isLoadingMore) {
+ this._isLoadingMore = true;
+ this._showLoadingMore();
+ this._fetchImages(this._currentSearchQuery, this.currentPage + 1, true);
+ }
+
+ this.scrollPosition = Math.min(totalWidth - containerWidth, this.scrollPosition + scrollAmount);
+ container.scrollTo({ left: this.scrollPosition, behavior: 'smooth' });
+ this._updateNavButtons();
+ },
+
+ _updateNavButtons: function() {
+ // this function is responsible to update the nav buttons
+ // when we're at the very left, we hide the nav-left button completely
+ // when we're at the very right and no more pages available, we hide the nav-right button
+ const navLeft = this._shadow.querySelector('.phoenix-ribbon-nav.left');
+ const navRight = this._shadow.querySelector('.phoenix-ribbon-nav.right');
+ const container = this._shadow.querySelector('.phoenix-ribbon-strip');
+
+ if (!navLeft || !navRight || !container) { return; }
+
+ // show/hide left button
+ if (this.scrollPosition <= 0) {
+ navLeft.style.display = 'none';
+ } else {
+ navLeft.style.display = 'block';
+ }
+
+ // show/hide right button
+ const containerWidth = container.clientWidth;
+ const totalWidth = container.scrollWidth;
+ const atEnd = (this.scrollPosition + containerWidth) >= totalWidth - 10;
+ const hasMorePages = this.currentPage < this.totalPages;
+
+ if (atEnd && !hasMorePages) {
+ navRight.style.display = 'none';
+ } else {
+ navRight.style.display = 'block';
+ }
+ },
+
+ _showLoading: function() {
+ const rowElement = this._shadow.querySelector('.phoenix-ribbon-row');
+ if (!rowElement) { return; }
+
+ rowElement.innerHTML = 'Loading images...';
+ rowElement.className = 'phoenix-ribbon-row phoenix-ribbon-loading';
+ },
+
+ _showLoadingMore: function() {
+ const rowElement = this._shadow.querySelector('.phoenix-ribbon-row');
+ if (!rowElement) { return; }
+
+ // when loading more images we need to show the message at the end of the image ribbon
+ const loadingIndicator = window.document.createElement('div');
+ loadingIndicator.className = 'phoenix-loading-more';
+ loadingIndicator.style.cssText = `
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ min-width: 120px !important;
+ height: 116px !important;
+ margin-left: 2px !important;
+ background: rgba(255,255,255,0.03) !important;
+ border-radius: 8px !important;
+ color: #e8eaf0 !important;
+ font-size: 12px !important;
+ border: 1px dashed rgba(255,255,255,0.1) !important;
+ `;
+ loadingIndicator.textContent = 'Loading...';
+ rowElement.appendChild(loadingIndicator);
+ },
+
+ _hideLoadingMore: function() {
+ const loadingIndicator = this._shadow.querySelector('.phoenix-loading-more');
+ if (loadingIndicator) {
+ loadingIndicator.remove();
+ }
+ },
+
+ _attachEventHandlers: function() {
+ const ribbonContainer = this._shadow.querySelector('.phoenix-image-ribbon');
+ const searchInput = this._shadow.querySelector('.phoenix-ribbon-search input');
+ const searchButton = this._shadow.querySelector('.phoenix-ribbon-search-btn');
+ const closeButton = this._shadow.querySelector('.phoenix-ribbon-close');
+ const navLeft = this._shadow.querySelector('.phoenix-ribbon-nav.left');
+ const navRight = this._shadow.querySelector('.phoenix-ribbon-nav.right');
+ const selectImageBtn = this._shadow.querySelector('.phoenix-select-image-btn');
+ const fileInput = this._shadow.querySelector('.phoenix-file-input');
+
+ if (searchInput && searchButton) {
+ const performSearch = (e) => {
+ e.stopPropagation();
+ const query = searchInput.value.trim();
+ if (query) {
+ // reset pagination when searching
+ this.currentPage = 1;
+ this.allImages = [];
+ this.scrollPosition = 0;
+ this._fetchImages(query);
+ }
+ };
+
+ searchButton.addEventListener('click', performSearch);
+ searchInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ performSearch(e);
+ }
+ });
+
+ searchInput.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+ }
+
+ if (selectImageBtn && fileInput) {
+ selectImageBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ fileInput.click();
+ });
+
+ fileInput.addEventListener('change', (e) => {
+ e.stopPropagation();
+ const file = e.target.files[0];
+ if (file) {
+ this._handleLocalImageSelection(file);
+ fileInput.value = '';
+ }
+ });
+ }
+
+ if (closeButton) {
+ closeButton.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.remove();
+ });
+ }
+
+ if (navLeft) {
+ navLeft.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this._handleNavLeft();
+ });
+ }
+
+ if (navRight) {
+ navRight.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this._handleNavRight();
+ });
+ }
+
+ // Prevent clicks anywhere inside the ribbon from bubbling up
+ if (ribbonContainer) {
+ ribbonContainer.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+ }
+ },
+
+ // append true means load more images (user clicked on nav-right)
+ // append false means its a new query
+ _renderImages: function(images, append = false) {
+ const rowElement = this._shadow.querySelector('.phoenix-ribbon-row');
+ if (!rowElement) { return; }
+
+ const container = this._shadow.querySelector('.phoenix-ribbon-strip');
+ const savedScrollPosition = container ? container.scrollLeft : 0;
+
+ // if not appending we clear the phoenix ribbon
+ if (!append) {
+ rowElement.innerHTML = '';
+ rowElement.className = 'phoenix-ribbon-row';
+ } else {
+ // when appending we add the new images at the end
+ const loadingIndicator = this._shadow.querySelector('.phoenix-loading-more');
+ if (loadingIndicator) {
+ loadingIndicator.remove();
+ }
+ }
+
+ // Create thumbnails from API data
+ images.forEach(image => {
+ const thumbDiv = window.document.createElement('div');
+ thumbDiv.className = 'phoenix-ribbon-thumb';
+
+ const img = window.document.createElement('img');
+ img.src = image.thumb_url || image.url;
+ img.alt = image.alt_text || 'Unsplash image';
+ img.loading = 'lazy';
+
+ // show hovered image along with dimensions
+ thumbDiv.addEventListener('mouseenter', () => {
+ this.element.style.width = this._originalImageStyle.width || this.maxWidth;
+ this.element.style.height = this._originalImageStyle.height || this.maxHeight;
+
+ this.element.style.objectFit = this._originalImageStyle.objectFit || 'cover';
+ this.element.src = image.url || image.thumb_url;
+ });
+
+ // show original image when hover ends
+ thumbDiv.addEventListener('mouseleave', () => {
+ this.element.src = this._originalImageSrc;
+ });
+
+ // attribution overlay, we show this only in the image ribbon gallery
+ const attribution = window.document.createElement('div');
+ attribution.className = 'phoenix-ribbon-attribution';
+
+ const photographer = window.document.createElement('a');
+ photographer.className = 'photographer';
+ photographer.href = image.photographer_url;
+ photographer.target = '_blank';
+ photographer.rel = 'noopener noreferrer';
+ photographer.textContent = (image.user && image.user.name) || 'Anonymous';
+ photographer.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+
+ const source = window.document.createElement('a');
+ source.className = 'source';
+ source.href = image.unsplash_url;
+ source.target = '_blank';
+ source.rel = 'noopener noreferrer';
+ source.textContent = 'on Unsplash';
+ source.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+
+ attribution.appendChild(photographer);
+ attribution.appendChild(source);
+
+ // download icon
+ const downloadIcon = window.document.createElement('div');
+ downloadIcon.className = 'phoenix-download-icon';
+ downloadIcon.title = config.strings.imageGalleryUseImage;
+ downloadIcon.innerHTML = ``;
+
+ // when the image is clicked we download the image
+ thumbDiv.addEventListener('click', (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ // prevent multiple downloads of the same image
+ if (thumbDiv.classList.contains('downloading')) { return; }
+
+ // show download indicator
+ this._showDownloadIndicator(thumbDiv);
+
+ const filename = this._generateFilename(image);
+ const extnName = ".jpg";
+
+ const targetWidth = this._originalImageStyle.width || this.maxWidth;
+ const targetHeight = this._originalImageStyle.height || this.maxHeight;
+ const widthNum = parseInt(targetWidth);
+ const heightNum = parseInt(targetHeight);
+
+ const downloadUrl = image.url ? `${image.url}?w=${widthNum}&h=${heightNum}&fit=crop` : image.thumb_url;
+ this._useImage(downloadUrl, filename, extnName, false, thumbDiv);
+ });
+
+ thumbDiv.appendChild(img);
+ thumbDiv.appendChild(attribution);
+ thumbDiv.appendChild(downloadIcon);
+ rowElement.appendChild(thumbDiv);
+ });
+
+ if (append && container && savedScrollPosition > 0) {
+ setTimeout(() => {
+ container.scrollLeft = savedScrollPosition;
+ }, 0);
+ }
+ },
+
+ _showError: function(message) {
+ const rowElement = this._shadow.querySelector('.phoenix-ribbon-row');
+ if (!rowElement) { return; }
+
+ rowElement.innerHTML = message;
+ rowElement.className = 'phoenix-ribbon-row phoenix-ribbon-error';
+ },
+
+ // file name with which we need to save the image
+ _generateFilename: function(image) {
+ const photographerName = (image.user && image.user.name) || 'Anonymous';
+ const searchTerm = this._currentSearchQuery || 'image';
+
+ // clean the search term and the photograper name to write in file name
+ const cleanSearchTerm = searchTerm.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
+ const cleanPhotographerName = photographerName.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
+
+ return `${cleanSearchTerm}-by-${cleanPhotographerName}`;
+ },
+
+ _useImage: function(imageUrl, filename, extnName, isLocalFile, thumbDiv) {
+ // send the message to the editor instance to save the image and update the source code
+ const tagId = this.element.getAttribute("data-brackets-id");
+
+ const messageData = {
+ livePreviewEditEnabled: true,
+ useImage: true,
+ imageUrl: imageUrl,
+ filename: filename,
+ extnName: extnName,
+ element: this.element,
+ tagId: Number(tagId)
+ };
+
+ // if this is a local file we need some more data before sending it to the editor
+ if (isLocalFile) {
+ messageData.isLocalFile = true;
+ // Convert data URL to binary data array for local files
+ const byteCharacters = atob(imageUrl.split(',')[1]);
+ const byteNumbers = new Array(byteCharacters.length);
+ for (let i = 0; i < byteCharacters.length; i++) {
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
+ }
+ messageData.imageData = byteNumbers;
+ }
+
+ window._Brackets_MessageBroker.send(messageData);
+
+ // if thumbDiv is provided, hide the download indicator after a reasonable timeout
+ // this is to make sure that the indicator is always removed even if there's no explicit success callback
+ if (thumbDiv) {
+ setTimeout(() => {
+ this._hideDownloadIndicator(thumbDiv);
+ }, 3000);
+ }
+ },
+
+ _handleLocalImageSelection: function(file) {
+ if (!file || !file.type.startsWith('image/')) {
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const imageDataUrl = e.target.result;
+
+ const originalName = file.name;
+ const nameWithoutExt = originalName.substring(0, originalName.lastIndexOf('.')) || originalName;
+ const extension = originalName.substring(originalName.lastIndexOf('.')) || '.jpg';
+
+ // we clean the file name because the file might have some chars which might not be compatible
+ const cleanName = nameWithoutExt.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
+ const filename = cleanName || 'selected-image';
+
+ // Use the unified _useImage method with isLocalFile flag
+ this._useImage(imageDataUrl, filename, extension, true, null);
+
+ // Close the ribbon after successful selection
+ this.remove();
+ };
+
+ reader.onerror = (error) => {
+ console.error('Something went wrong when reading the image:', error);
+ };
+
+ reader.readAsDataURL(file);
+ },
+
+
+ create: function() {
+ this.remove(); // remove existing ribbon if already present
+
+ // when image ribbon gallery is created we get the original image along with its dimensions
+ // so that on hover in we can show the hovered image and on hover out we can restore the original image
+ this._originalImageSrc = this.element.src;
+ this._originalImageStyle = {
+ width: window.getComputedStyle(this.element).width,
+ height: window.getComputedStyle(this.element).height,
+ objectFit: window.getComputedStyle(this.element).objectFit
+ };
+
+ this._style();
+ window.document.body.appendChild(this.body);
+ this._attachEventHandlers();
+
+ const queryToUse = _imageGalleryCache.currentQuery || this._getDefaultQuery();
+ this._fetchImages(queryToUse);
+ setTimeout(() => this._updateNavButtons(), 0);
+ },
+
+ remove: function () {
+ _imageRibbonGallery = null;
+ if (this.body && this.body.parentNode && this.body.parentNode === window.document.body) {
+ window.document.body.removeChild(this.body);
+ this.body = null;
+ }
+ },
+
+ _showDownloadIndicator: function(thumbDiv) {
+ // add downloading class
+ thumbDiv.classList.add('downloading');
+
+ // create download indicator
+ const indicator = window.document.createElement('div');
+ indicator.className = 'phoenix-download-indicator';
+
+ const spinner = window.document.createElement('div');
+ spinner.className = 'phoenix-download-spinner';
+
+ indicator.appendChild(spinner);
+ thumbDiv.appendChild(indicator);
+ },
+
+ _hideDownloadIndicator: function(thumbDiv) {
+ // remove downloading class
+ thumbDiv.classList.remove('downloading');
+
+ // remove download indicator
+ const indicator = thumbDiv.querySelector('.phoenix-download-indicator');
+ if (indicator) {
+ indicator.remove();
+ }
+ }
+ };
+
function Highlight(color, trigger) {
this.color = color;
this.trigger = !!trigger;
@@ -2105,7 +3096,7 @@ function RemoteFunctions(config = {}) {
"top": offset.top + "px",
"width": elementBounds.width + "px",
"height": elementBounds.height + "px",
- "z-index": 2000000,
+ "z-index": 2147483647,
"margin": 0,
"padding": 0,
"position": "absolute",
@@ -2219,6 +3210,7 @@ function RemoteFunctions(config = {}) {
var _nodeInfoBox;
var _nodeMoreOptionsBox;
var _aiPromptBox;
+ var _imageRibbonGallery;
var _setup = false;
function onMouseOver(event) {
@@ -2252,6 +3244,12 @@ function RemoteFunctions(config = {}) {
return getHighlightMode() !== "click";
}
+ // helper function to check if image ribbon gallery should be shown
+ function shouldShowImageRibbon() {
+ if (_imageRibbonGallery) { return false; }
+ return config.imageRibbon !== false;
+ }
+
// helper function to clear element background highlighting
function clearElementBackground(element) {
if (element._originalBackgroundColor !== undefined) {
@@ -2323,6 +3321,7 @@ function RemoteFunctions(config = {}) {
function _selectElement(element) {
// dismiss all UI boxes and cleanup previous element state when selecting a different element
dismissUIAndCleanupState();
+ dismissImageRibbonGallery();
if(!isElementEditable(element)) {
return false;
}
@@ -2336,6 +3335,13 @@ function RemoteFunctions(config = {}) {
_nodeMoreOptionsBox = null;
}
+ // if the selected element is an image, show the image ribbon gallery (make sure its enabled in preferences)
+ if(element && element.tagName.toLowerCase() === 'img' && shouldShowImageRibbon()) {
+ if (!_imageRibbonGallery) {
+ _imageRibbonGallery = new ImageRibbonGallery(element);
+ }
+ }
+
element._originalOutline = element.style.outline;
element.style.outline = "1px solid #4285F4";
@@ -2354,22 +3360,16 @@ function RemoteFunctions(config = {}) {
/**
* This function handles the click event on the live preview DOM element
- * it is to show the advanced DOM manipulation options in the live preview
+ * this just stops the propagation because otherwise users might not be able to edit buttons or hyperlinks etc
* @param {Event} event
*/
function onClick(event) {
- dismissAIPromptBox();
const element = event.target;
- // when user clicks on the HTML, BODY tags or elements inside HEAD, we want to remove the boxes
- if(_nodeMoreOptionsBox && !isElementEditable(element)) {
- dismissUIAndCleanupState();
- } else if(isElementEditable(element)) {
+ if(isElementEditable(element)) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
-
- _selectElement(element);
}
}
@@ -2402,9 +3402,6 @@ function RemoteFunctions(config = {}) {
}
function onKeyDown(event) {
- if ((event.key === "Escape" || event.key === "Esc")) {
- dismissUIAndCleanupState();
- }
if (!_setup && _validEvent(event)) {
window.document.addEventListener("keyup", onKeyUp);
window.document.addEventListener("mouseover", onMouseOver);
@@ -2465,6 +3462,7 @@ function RemoteFunctions(config = {}) {
// if no valid element present we dismiss the boxes
if (!foundValidElement) {
dismissUIAndCleanupState();
+ dismissImageRibbonGallery();
}
}
@@ -2845,11 +3843,21 @@ function RemoteFunctions(config = {}) {
const highlightModeChanged = oldHighlightMode !== newHighlightMode;
const isProStatusChanged = oldConfig.isProUser !== config.isProUser;
const highlightSettingChanged = oldConfig.highlight !== config.highlight;
+ const imageRibbonJustEnabled = !oldConfig.imageRibbon && config.imageRibbon;
// Handle significant configuration changes
if (highlightModeChanged || isProStatusChanged || highlightSettingChanged) {
_handleConfigurationChange();
}
+
+ // if user enabled the image ribbon setting and an image is selected, then we show the image ribbon
+ if (imageRibbonJustEnabled && previouslyClickedElement &&
+ previouslyClickedElement.tagName.toLowerCase() === 'img') {
+ if (!_imageRibbonGallery) {
+ _imageRibbonGallery = new ImageRibbonGallery(previouslyClickedElement);
+ }
+ }
+
_updateEventListeners();
return JSON.stringify(config);
@@ -2934,6 +3942,18 @@ function RemoteFunctions(config = {}) {
return false;
}
+ /**
+ * to dismiss the image ribbon gallery if its available
+ */
+ function dismissImageRibbonGallery() {
+ if (_imageRibbonGallery) {
+ _imageRibbonGallery.remove();
+ _imageRibbonGallery = null;
+ return true;
+ }
+ return false;
+ }
+
/**
* Helper function to dismiss all UI boxes at once
* @return {boolean} true if any boxes were dismissed, false otherwise
@@ -3156,6 +4176,7 @@ function RemoteFunctions(config = {}) {
"finishEditing" : finishEditing,
"hasVisibleLivePreviewBoxes" : hasVisibleLivePreviewBoxes,
"dismissUIAndCleanupState" : dismissUIAndCleanupState,
+ "dismissImageRibbonGallery" : dismissImageRibbonGallery,
"registerHandlers" : registerHandlers
};
}
diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js
index 3b2381dc09..f1b3de2435 100644
--- a/src/LiveDevelopment/LiveDevMultiBrowser.js
+++ b/src/LiveDevelopment/LiveDevMultiBrowser.js
@@ -715,6 +715,16 @@ define(function (require, exports, module) {
function dismissLivePreviewBoxes() {
if (_protocol) {
_protocol.evaluate("_LD.dismissUIAndCleanupState()");
+ _protocol.evaluate("_LD.dismissImageRibbonGallery()");
+ }
+ }
+
+ /**
+ * Dismiss image ribbon gallery if it's open
+ */
+ function dismissImageRibbonGallery() {
+ if (_protocol) {
+ _protocol.evaluate("_LD.dismissImageRibbonGallery()");
}
}
@@ -804,6 +814,7 @@ define(function (require, exports, module) {
exports.redrawHighlight = redrawHighlight;
exports.hasVisibleLivePreviewBoxes = hasVisibleLivePreviewBoxes;
exports.dismissLivePreviewBoxes = dismissLivePreviewBoxes;
+ exports.dismissImageRibbonGallery = dismissImageRibbonGallery;
exports.registerHandlers = registerHandlers;
exports.updateConfig = updateConfig;
exports.init = init;
diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js
index 1b40ba6727..ddddb8c465 100644
--- a/src/LiveDevelopment/LivePreviewEdit.js
+++ b/src/LiveDevelopment/LivePreviewEdit.js
@@ -28,6 +28,9 @@ define(function (require, exports, module) {
const HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation");
const LiveDevMultiBrowser = require("LiveDevelopment/LiveDevMultiBrowser");
const CodeMirror = require("thirdparty/CodeMirror/lib/codemirror");
+ const ProjectManager = require("project/ProjectManager");
+ const FileSystem = require("filesystem/FileSystem");
+ const PathUtils = require("thirdparty/path-utils/path-utils");
/**
* This function syncs text content changes between the original source code
@@ -593,6 +596,204 @@ define(function (require, exports, module) {
// write the AI implementation here...@abose
}
+ /**
+ * this is a helper function to make sure that when saving a new image, there's no existing file with the same name
+ * @param {String} basePath - this is the base path where the image will be saved
+ * @param {String} filename - the name of the image file
+ * @param {String} extnName - the name of the image extension. (defaults to "jpg")
+ * @returns {String} - the new file name
+ */
+ function getUniqueFilename(basePath, filename, extnName) {
+ let counter = 0;
+ let uniqueFilename = filename + extnName;
+
+ function checkAndIncrement() {
+ const filePath = basePath + uniqueFilename;
+ const file = FileSystem.getFileForPath(filePath);
+
+ return new Promise((resolve) => {
+ file.exists((err, exists) => {
+ if (exists) {
+ counter++;
+ uniqueFilename = `${filename}-${counter}${extnName}`;
+ checkAndIncrement().then(resolve);
+ } else {
+ resolve(uniqueFilename);
+ }
+ });
+ });
+ }
+
+ return checkAndIncrement();
+ }
+
+ /**
+ * This function updates the src attribute of an image element in the source code
+ * @param {Number} tagId - the data-brackets-id of the image element
+ * @param {String} newSrcValue - the new src value to set
+ */
+ function _updateImageSrcAttribute(tagId, newSrcValue) {
+ const editor = _getEditorAndValidate(tagId);
+ if (!editor) {
+ return;
+ }
+
+ const range = _getElementRange(editor, tagId);
+ if (!range) {
+ return;
+ }
+
+ const { startPos, endPos } = range;
+ const elementText = editor.getTextBetween(startPos, endPos);
+
+ // parse it using DOM parser so that we can update the src attribute
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(elementText, "text/html");
+ const imgElement = doc.querySelector('img');
+
+ if (imgElement) {
+ imgElement.setAttribute('src', newSrcValue);
+ const updatedElementText = imgElement.outerHTML;
+
+ editor.document.batchOperation(function () {
+ editor.replaceRange(updatedElementText, startPos, endPos);
+ });
+ }
+ }
+
+ /**
+ * Helper function to update image src attribute and dismiss ribbon gallery
+ *
+ * @param {Number} tagId - the data-brackets-id of the image element
+ * @param {String} targetPath - the full path where the image was saved
+ * @param {String} filename - the filename of the saved image
+ */
+ function _updateImageAndDismissRibbon(tagId, targetPath, filename) {
+ const editor = _getEditorAndValidate(tagId);
+ if (editor) {
+ const htmlFilePath = editor.document.file.fullPath;
+ const relativePath = PathUtils.makePathRelative(targetPath, htmlFilePath);
+ _updateImageSrcAttribute(tagId, relativePath);
+ } else {
+ _updateImageSrcAttribute(tagId, filename);
+ }
+
+ // dismiss the image ribbon gallery
+ const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
+ if (currLiveDoc && currLiveDoc.protocol && currLiveDoc.protocol.evaluate) {
+ currLiveDoc.protocol.evaluate("_LD.dismissImageRibbonGallery()");
+ }
+ }
+
+ /**
+ * helper function to handle 'upload from computer'
+ * @param {Object} message - the message object
+ * @param {String} filename - the file name with which we need to save the image
+ * @param {Directory} projectRoot - the project root in which the image is to be saved
+ */
+ function _handleUseThisImageLocalFiles(message, filename, projectRoot) {
+ const { tagId, imageData } = message;
+
+ const uint8Array = new Uint8Array(imageData);
+ const targetPath = projectRoot.fullPath + filename;
+
+ window.fs.writeFile(targetPath, window.Filer.Buffer.from(uint8Array),
+ { encoding: window.fs.BYTE_ARRAY_ENCODING }, (err) => {
+ if (err) {
+ console.error('Failed to save image:', err);
+ } else {
+ _updateImageAndDismissRibbon(tagId, targetPath, filename);
+ }
+ });
+ }
+
+ /**
+ * helper function to handle 'use this image' button click on remote images
+ * @param {Object} message - the message object
+ * @param {String} filename - the file name with which we need to save the image
+ * @param {Directory} projectRoot - the project root in which the image is to be saved
+ */
+ function _handleUseThisImageRemote(message, filename, projectRoot) {
+ const { imageUrl, tagId } = message;
+
+ fetch(imageUrl)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ return response.arrayBuffer();
+ })
+ .then(arrayBuffer => {
+ const uint8Array = new Uint8Array(arrayBuffer);
+ const targetPath = projectRoot.fullPath + filename;
+
+ window.fs.writeFile(targetPath, window.Filer.Buffer.from(uint8Array),
+ { encoding: window.fs.BYTE_ARRAY_ENCODING }, (err) => {
+ if (err) {
+ console.error('Failed to save image:', err);
+ } else {
+ _updateImageAndDismissRibbon(tagId, targetPath, filename);
+ }
+ });
+ })
+ .catch(error => {
+ console.error('Failed to fetch image:', error);
+ });
+ }
+
+ /**
+ * This function is called when 'use this image' button is clicked in the image ribbon gallery
+ * or user loads an image file from the computer
+ * this is responsible to download the image in the appropriate place
+ * and also change the src attribute of the element (by calling appropriate helper functions)
+ * @param {Object} message - the message object which stores all the required data for this operation
+ */
+ function _handleUseThisImage(message) {
+ const filename = message.filename;
+ const extnName = message.extnName || "jpg";
+
+ const projectRoot = ProjectManager.getProjectRoot();
+ if (!projectRoot) { return; }
+
+ // phoenix-assets folder, all the images will be stored inside this
+ const phoenixAssetsPath = projectRoot.fullPath + "phoenix-code-assets/";
+ const phoenixAssetsDir = FileSystem.getDirectoryForPath(phoenixAssetsPath);
+
+ // check if the phoenix-assets dir exists
+ // if present, download the image inside it, if not create the dir and then download the image inside it
+ phoenixAssetsDir.exists((err, exists) => {
+ if (err) { return; }
+
+ if (!exists) {
+ phoenixAssetsDir.create((err) => {
+ if (err) {
+ console.error('Error creating phoenix-code-assets directory:', err);
+ return;
+ }
+ _downloadImageToPhoenixAssets(message, filename, extnName, phoenixAssetsDir);
+ });
+ } else {
+ _downloadImageToPhoenixAssets(message, filename, extnName, phoenixAssetsDir);
+ }
+ });
+ }
+
+ /**
+ * Helper function to download image to phoenix-assets folder
+ */
+ function _downloadImageToPhoenixAssets(message, filename, extnName, phoenixAssetsDir) {
+ getUniqueFilename(phoenixAssetsDir.fullPath, filename, extnName).then((uniqueFilename) => {
+ // check if the image is loaded from computer or from remote
+ if (message.isLocalFile && message.imageData) {
+ _handleUseThisImageLocalFiles(message, uniqueFilename, phoenixAssetsDir);
+ } else {
+ _handleUseThisImageRemote(message, uniqueFilename, phoenixAssetsDir);
+ }
+ }).catch(error => {
+ console.error('Something went wrong when trying to use this image', error);
+ });
+ }
+
/**
* This is the main function that is exported.
* it will be called by LiveDevProtocol when it receives a message from RemoteFunctions.js
@@ -623,6 +824,12 @@ define(function (require, exports, module) {
return;
}
+ // use this image
+ if (message.useImage && message.imageUrl && message.filename) {
+ _handleUseThisImage(message);
+ return;
+ }
+
if (!message.element || !message.tagId) {
// check for undo
if (message.undoLivePreviewOperation || message.redoLivePreviewOperation) {
diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js
index 451427b2da..902321ae2e 100644
--- a/src/LiveDevelopment/main.js
+++ b/src/LiveDevelopment/main.js
@@ -70,6 +70,7 @@ define(function main(require, exports, module) {
},
isProUser: isProUser,
elemHighlights: "hover", // default value, this will get updated when the extension loads
+ imageRibbon: true, // default value, this will get updated when the extension loads
// this strings are used in RemoteFunctions.js
// we need to pass this through config as remoteFunctions runs in browser context and cannot
// directly reference Strings file
@@ -79,7 +80,11 @@ define(function main(require, exports, module) {
duplicate: Strings.LIVE_DEV_MORE_OPTIONS_DUPLICATE,
delete: Strings.LIVE_DEV_MORE_OPTIONS_DELETE,
ai: Strings.LIVE_DEV_MORE_OPTIONS_AI,
- aiPromptPlaceholder: Strings.LIVE_DEV_AI_PROMPT_PLACEHOLDER
+ aiPromptPlaceholder: Strings.LIVE_DEV_AI_PROMPT_PLACEHOLDER,
+ imageGalleryUseImage: Strings.LIVE_DEV_IMAGE_GALLERY_USE_IMAGE,
+ imageGallerySelectFromComputer: Strings.LIVE_DEV_IMAGE_GALLERY_SELECT_FROM_COMPUTER,
+ imageGalleryChooseFolder: Strings.LIVE_DEV_IMAGE_GALLERY_CHOOSE_FOLDER,
+ imageGallerySearchPlaceholder: Strings.LIVE_DEV_IMAGE_GALLERY_SEARCH_PLACEHOLDER
}
};
// Status labels/styles are ordered: error, not connected, progress1, progress2, connected.
@@ -365,6 +370,20 @@ define(function main(require, exports, module) {
}
}
+ // this function is responsible to update image picker config
+ // called from live preview extension when preference changes
+ function updateImageRibbonConfig() {
+ const prefValue = PreferencesManager.get("livePreviewImagePicker");
+ config.imageRibbon = prefValue !== false; // default to true if undefined
+
+ if (MultiBrowserLiveDev && MultiBrowserLiveDev.status >= MultiBrowserLiveDev.STATUS_ACTIVE) {
+ if (!prefValue) { MultiBrowserLiveDev.dismissImageRibbonGallery(); } // to remove any existing image ribbons
+
+ MultiBrowserLiveDev.updateConfig(JSON.stringify(config));
+ MultiBrowserLiveDev.registerHandlers();
+ }
+ }
+
// init commands
CommandManager.register(Strings.CMD_LIVE_HIGHLIGHT, Commands.FILE_LIVE_HIGHLIGHT, togglePreviewHighlight);
CommandManager.register(Strings.CMD_RELOAD_LIVE_PREVIEW, Commands.CMD_RELOAD_LIVE_PREVIEW, _handleReloadLivePreviewCommand);
@@ -393,6 +412,7 @@ define(function main(require, exports, module) {
exports.togglePreviewHighlight = togglePreviewHighlight;
exports.setLivePreviewEditFeaturesActive = setLivePreviewEditFeaturesActive;
exports.updateElementHighlightConfig = updateElementHighlightConfig;
+ exports.updateImageRibbonConfig = updateImageRibbonConfig;
exports.getConnectionIds = MultiBrowserLiveDev.getConnectionIds;
exports.getLivePreviewDetails = MultiBrowserLiveDev.getLivePreviewDetails;
exports.hideHighlight = MultiBrowserLiveDev.hideHighlight;
diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js
index 4942e15225..cb2fd9350a 100644
--- a/src/extensionsIntegrated/Phoenix-live-preview/main.js
+++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js
@@ -113,6 +113,12 @@ define(function (require, exports, module) {
description: Strings.LIVE_DEV_SETTINGS_ELEMENT_HIGHLIGHT_PREFERENCE
});
+ // live preview image picker preference (whether to show image gallery when clicking images)
+ const PREFERENCE_PROJECT_IMAGE_RIBBON = "livePreviewImagePicker";
+ PreferencesManager.definePreference(PREFERENCE_PROJECT_IMAGE_RIBBON, "boolean", true, {
+ description: Strings.LIVE_PREVIEW_EDIT_IMAGE_RIBBON
+ });
+
const LIVE_PREVIEW_PANEL_ID = "live-preview-panel";
const LIVE_PREVIEW_IFRAME_ID = "panel-live-preview-frame";
const LIVE_PREVIEW_IFRAME_HTML = `
@@ -419,6 +425,7 @@ define(function (require, exports, module) {
if (isEditFeaturesActive) {
items.push("---");
items.push(Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON);
+ items.push(Strings.LIVE_PREVIEW_EDIT_IMAGE_RIBBON);
}
const rawMode = PreferencesManager.get(PREFERENCE_LIVE_PREVIEW_MODE) || _getDefaultMode();
@@ -444,6 +451,12 @@ define(function (require, exports, module) {
return `✓ ${Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON}`;
}
return `${'\u00A0'.repeat(4)}${Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON}`;
+ } else if (item === Strings.LIVE_PREVIEW_EDIT_IMAGE_RIBBON) {
+ const isImageRibbonEnabled = PreferencesManager.get(PREFERENCE_PROJECT_IMAGE_RIBBON) !== false;
+ if(isImageRibbonEnabled) {
+ return `✓ ${item}`;
+ }
+ return `${'\u00A0'.repeat(4)}${item}`;
}
return item;
});
@@ -492,6 +505,15 @@ define(function (require, exports, module) {
const newMode = currentMode !== "click" ? "click" : "hover";
PreferencesManager.set(PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT, newMode);
return; // Don't dismiss highlights for this option
+ } else if (item === Strings.LIVE_PREVIEW_EDIT_IMAGE_RIBBON) {
+ // Don't allow image ribbon toggle if edit features are not active
+ if (!isEditFeaturesActive) {
+ return;
+ }
+ // Toggle image ribbon preference
+ const currentEnabled = PreferencesManager.get(PREFERENCE_PROJECT_IMAGE_RIBBON);
+ PreferencesManager.set(PREFERENCE_PROJECT_IMAGE_RIBBON, !currentEnabled);
+ return; // Don't dismiss highlights for this option
}
// need to dismiss the previous highlighting and stuff
@@ -1296,8 +1318,15 @@ define(function (require, exports, module) {
LiveDevelopment.updateElementHighlightConfig();
});
+ // Handle image ribbon preference changes from this extension
+ PreferencesManager.on("change", PREFERENCE_PROJECT_IMAGE_RIBBON, function() {
+ LiveDevelopment.updateImageRibbonConfig();
+ });
+
// Initialize element highlight config on startup
LiveDevelopment.updateElementHighlightConfig();
+ // Initialize image ribbon config on startup
+ LiveDevelopment.updateImageRibbonConfig();
LiveDevelopment.openLivePreview();
LiveDevelopment.on(LiveDevelopment.EVENT_OPEN_PREVIEW_URL, _openLivePreviewURL);
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js
index 7b6f551094..3770cad5bc 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -189,12 +189,17 @@ define({
"LIVE_DEV_MORE_OPTIONS_DUPLICATE": "Duplicate",
"LIVE_DEV_MORE_OPTIONS_DELETE": "Delete",
"LIVE_DEV_MORE_OPTIONS_AI": "Edit with AI",
+ "LIVE_DEV_IMAGE_GALLERY_USE_IMAGE": "Use this image",
+ "LIVE_DEV_IMAGE_GALLERY_SELECT_FROM_COMPUTER": "Select image from computer",
+ "LIVE_DEV_IMAGE_GALLERY_CHOOSE_FOLDER": "Choose download folder",
+ "LIVE_DEV_IMAGE_GALLERY_SEARCH_PLACEHOLDER": "Search images...",
"LIVE_DEV_AI_PROMPT_PLACEHOLDER": "Ask Phoenix AI to modify this element...",
"LIVE_PREVIEW_CUSTOM_SERVER_BANNER": "Getting preview from your custom server {0}",
"LIVE_PREVIEW_MODE_PREVIEW": "Preview Mode",
"LIVE_PREVIEW_MODE_HIGHLIGHT": "Highlight Mode",
"LIVE_PREVIEW_MODE_EDIT": "Edit Mode",
"LIVE_PREVIEW_EDIT_HIGHLIGHT_ON": "Edit Highlights on Hover",
+ "LIVE_PREVIEW_EDIT_IMAGE_RIBBON": "Show Image Picker on Image click",
"LIVE_PREVIEW_MODE_PREFERENCE": "{0} shows only the webpage, {1} connects the webpage to your code - click on elements to jump to their code and vice versa, {2} provides highlighting along with advanced element manipulation",
"LIVE_PREVIEW_CONFIGURE_MODES": "Configure Live Preview Modes",
"LIVE_PREVIEW_PRO_FEATURE_TITLE": "Pro Feature",